v3 : relecture de 7ffa4fd — un refus du nœud lâche le bulletin, le reçu ne dit vérifié que ce que scelle l'empreinte

polkadot-api lève InvalidTxError (JSON indenté) quand le nœud refuse la transaction :
elle est désormais lue comme « la chaîne a dit non » (rien n'est parti, le bulletin est
lâché), au lieu de rester en attente 24 h. La page /bulletins ne présente comme vérifiés
que la version, la révision et le choix ; l'article, le titre et le bloc du reçu sont dits
hors du sceau. ADR 0008 : trois affirmations corrigées (indexeur sans filtre, débit de la
tâche de fond, lectures qui n'utilisent pas encore shown_tally).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-31 05:46:28 +01:00
co-authored by Claude Opus 5.5
parent 8ff5a2b573
commit 9c8f4cc568
8 changed files with 49 additions and 14 deletions
+4 -1
View File
@@ -126,7 +126,10 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins
// Checked here, in the browser, without the server.
await page.getByLabel('Fichier du reçu').setInputFiles(file);
await expect(page.getByText('Le reçu tient : ses votes et son sel donnent bien ce commentaire.')).toBeVisible();
await expect(page.getByText(`${NEVER_ALONE} : contre`)).toBeVisible();
// Only what the seal covers is said verified: the version, its revision and the choice.
await expect(page.locator('ul.lines li').first()).toContainText(`Version ${receipt.lines[0].proposalId}, révision ${receipt.lines[0].revision} : contre`);
// The article and its title are written in the receipt, outside the seal (relecture of 7ffa4fd).
await expect(page.getByText(`${NEVER_ALONE} — noté dans le reçu, hors du sceau`, { exact: false })).toBeVisible();
// One choice changed: it no longer holds.
const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'for' })) };
await page.getByLabel('Ou colle le reçu').fill(JSON.stringify(changed));
+3 -2
View File
@@ -298,9 +298,10 @@ export const en: Messages = {
'ballots.verify.paste': 'Or paste the receipt',
'ballots.verify.go': 'Check',
'ballots.verify.ok': 'The receipt holds: its votes and its salt give this very comment.',
'ballots.verify.line': '{article} · {title}: {choice}',
'ballots.verify.line': 'Version {version}, revision {revision}: {choice}',
'ballots.verify.line.label': '{label} — written in the receipt, outside the seal',
'ballots.verify.where': 'Look for this comment in the history of your wallet: it is your vote, written on the Ğ1 chain.',
'ballots.verify.where.block': 'Look for this comment in the history of your wallet, or on the Ğ1 chain at block {block}: it is your vote.',
'ballots.verify.where.block': 'Look for this comment in the history of your wallet, or on the Ğ1 chain (the receipt says block {block}, outside the seal): if it is there, signed by your account, it is your vote.',
'ballots.verify.bad': 'The receipt does not hold: its votes and its salt do not give its comment.',
'ballots.verify.computed': 'Recomputed:',
'ballots.verify.expected': 'In the receipt:',
+3 -2
View File
@@ -298,9 +298,10 @@ export const es: Messages = {
'ballots.verify.paste': 'O pega el recibo',
'ballots.verify.go': 'Comprobar',
'ballots.verify.ok': 'El recibo es válido: sus votos y su sal dan exactamente este comentario.',
'ballots.verify.line': '{article} · {title}: {choice}',
'ballots.verify.line': 'Versión {version}, revisión {revision}: {choice}',
'ballots.verify.line.label': '{label} — anotado en el recibo, fuera del sello',
'ballots.verify.where': 'Busca este comentario en el historial de tu monedero: es tu voto, inscrito en la cadena Ğ1.',
'ballots.verify.where.block': 'Busca este comentario en el historial de tu monedero, o en la cadena Ğ1 en el bloque {block}: es tu voto.',
'ballots.verify.where.block': 'Busca este comentario en el historial de tu monedero, o en la cadena Ğ1 (el recibo indica el bloque {block}, fuera del sello): si figura allí, firmado por tu cuenta, es tu voto.',
'ballots.verify.bad': 'El recibo no es válido: sus votos y su sal no dan su comentario.',
'ballots.verify.computed': 'Recalculado:',
'ballots.verify.expected': 'En el recibo:',
+3 -2
View File
@@ -301,9 +301,10 @@ export const fr = {
'ballots.verify.paste': 'Ou colle le reçu',
'ballots.verify.go': 'Vérifier',
'ballots.verify.ok': 'Le reçu tient : ses votes et son sel donnent bien ce commentaire.',
'ballots.verify.line': '{article} · {title} : {choice}',
'ballots.verify.line': 'Version {version}, révision {revision} : {choice}',
'ballots.verify.line.label': '{label} — noté dans le reçu, hors du sceau',
'ballots.verify.where': "Cherche ce commentaire dans l'historique de ton portefeuille : c'est ton vote, inscrit sur la chaîne Ğ1.",
'ballots.verify.where.block': "Cherche ce commentaire dans l'historique de ton portefeuille, ou sur la chaîne Ğ1 au bloc {block} : c'est ton vote.",
'ballots.verify.where.block': "Cherche ce commentaire dans l'historique de ton portefeuille, ou sur la chaîne Ğ1 (le reçu indique le bloc {block}, hors du sceau) : s'il y figure, signé de ton compte, c'est ton vote.",
'ballots.verify.bad': 'Le reçu ne tient pas : ses votes et son sel ne donnent pas son commentaire.',
'ballots.verify.computed': 'Recalculé :',
'ballots.verify.expected': 'Dans le reçu :',
+7 -1
View File
@@ -73,6 +73,12 @@ export function injectedNames(page: Page): string[] {
/** What went wrong, in one of a few words the interface translates. */
export function failure(e: unknown): Failure {
if (e instanceof WalletError) return e.code;
// polkadot-api's refusal by the node's pool (stale, bad proof, too late…): never sent.
// Its message is indented JSON, so the reason is read from the error itself.
if (e instanceof Error && e.name === 'InvalidTxError') {
const reason = JSON.stringify((e as Error & { error?: unknown }).error ?? e.message).toLowerCase();
return /payment|funds|balance|existential/.test(reason) ? 'funds' : 'chain';
}
let text: string;
if (e instanceof Error) text = e.message;
else {
@@ -86,7 +92,7 @@ export function failure(e: unknown): Failure {
if (/cancel|reject|denied|declined/.test(text)) return 'cancelled';
if (/payment|funds|balance|existential|pay some fees/.test(text)) return 'funds';
// Refused by the node's pool (stale, bad proof…): it will never be included.
if (/"type":"invalid"|invalid transaction/.test(text)) return 'chain';
if (/"type":\s*"invalid"|invalid transaction/.test(text)) return 'chain';
return 'wallet';
}
+7 -1
View File
@@ -96,7 +96,10 @@
<code class="comment">{check.comment}</code>
<ul class="lines">
{#each check.lines as l (l.proposalId)}
<li>{t('ballots.verify.line', { article: l.article ?? '', title: l.title ?? l.proposalId, choice: t(`vote.choice.${l.choice}`) })}</li>
<li>
{t('ballots.verify.line', { choice: t(`vote.choice.${l.choice}`), version: l.proposalId, revision: i18n.number(l.revision) })}
{#if l.article || l.title}<span class="faint small unsealed">{t('ballots.verify.line.label', { label: [l.article, l.title].filter(Boolean).join(' · ') })}</span>{/if}
</li>
{/each}
</ul>
<p class="faint small">
@@ -196,6 +199,9 @@
gap: 0.25rem;
font-size: 0.9rem;
}
.unsealed {
display: block;
}
.ok {
font-weight: 800;
color: var(--moss);
+12
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { InvalidTxError } from 'polkadot-api';
import { amountText, geckoLink } from '../../src/lib/wallet/gecko';
import { failure, injectedNames, mayLetGo, testExtrinsic } from '../../src/lib/wallet/injected';
@@ -35,6 +36,17 @@ describe('wallets of the page', () => {
expect(failure({ code: 1010, message: 'Invalid Transaction' })).toBe('chain');
});
it('a refusal of the node, as polkadot-api raises it, is the chain saying no (relecture of 7ffa4fd)', () => {
// The real class: its message is indented JSON (`"type": "Invalid"`, with a space).
for (const reason of ['Stale', 'AncientBirthBlock', 'BadProof', 'ExhaustsResources', 'Future']) {
const e = new InvalidTxError({ type: 'Invalid', value: { type: reason } });
expect(failure(e), reason).toBe('chain');
expect(mayLetGo(failure(e), true), reason).toBe(true);
}
expect(failure(new InvalidTxError({ type: 'Invalid', value: { type: 'Payment' } }))).toBe('funds');
expect(failure(new InvalidTxError({ type: 'Unknown', value: { type: 'CannotLookup' } }))).toBe('chain');
});
it('a ballot is let go when nothing left the page, or when the chain said no — kept when it may still land', () => {
// Before the signature (the node down before sending, a refusal, the window closed): nothing left.
for (const f of ['node', 'wallet', 'cancelled', 'funds', 'account', 'chain'] as const) expect(mayLetGo(f, false), f).toBe(true);