From b260596cfd8e639b86b9db19c5e1b084a7683872 Mon Sep 17 00:00:00 2001 From: Yvv Date: Fri, 30 Oct 2026 22:47:26 +0100 Subject: [PATCH] =?UTF-8?q?v3=20:=20le=20vote=20d=C3=A9pli=C3=A9=20sous=20?= =?UTF-8?q?l'article,=20le=20panier,=20la=20signature=20par=20le=20portefe?= =?UTF-8?q?uille=20=C4=9E1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) : niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est (enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté), inertie et formule. Le reste d'une version (coquilles, traductions, signalements, historique) passe dans l'onglet « Détails » du panneau. - Le panier : article par article, « tout pour / tout contre » par section ou pour tout le document, jamais pour un article à plusieurs versions ; un seul geste (une écriture, ou une transaction). - La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page attend la confirmation. Un bulletin que le portefeuille n'a pas signé est lâché aussitôt (DELETE /api/ballots/{id}). - Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à la couleur d'accent de la page (3:1 au moins), distinct du choix. - e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et C4, C5, C6, C11, C17, C19, C20, M1 mis à jour. Co-Authored-By: Claude Opus 5.5 --- crates/ld-server/src/ballots.rs | 27 +- crates/ld-server/src/lib.rs | 2 +- crates/ld-server/tests/ballots.rs | 31 + web/e2e/chain-mock.mjs | 120 ++ web/e2e/entree.spec.ts | 22 +- web/e2e/helpers.ts | 65 + web/e2e/mobile.spec.ts | 94 +- web/e2e/reader.spec.ts | 89 +- web/e2e/serve.sh | 16 +- web/e2e/vote.spec.ts | 206 +++ web/package-lock.json | 1503 +++++++++++++++++ web/package.json | 2 + web/src/lib/basket.ts | 83 + web/src/lib/components/BallotPanel.svelte | 306 ++++ web/src/lib/components/ChainSign.svelte | 421 +++++ web/src/lib/components/ChoicePicker.svelte | 73 + web/src/lib/components/DeepList.svelte | 64 +- web/src/lib/components/DocPanel.svelte | 50 +- web/src/lib/components/Sidebar.svelte | 38 +- web/src/lib/components/UnitBlock.svelte | 25 +- ...{VoteView.svelte => VersionDetails.svelte} | 138 +- web/src/lib/components/VoteFold.svelte | 284 ++++ web/src/lib/doc.svelte.ts | 8 +- web/src/lib/i18n/en.ts | 85 +- web/src/lib/i18n/es.ts | 85 +- web/src/lib/i18n/fr.ts | 85 +- web/src/lib/picks.svelte.ts | 61 + web/src/lib/types.ts | 53 +- web/src/lib/vote.ts | 43 + web/src/lib/wallet/gecko.ts | 32 + web/src/lib/wallet/injected.ts | 206 +++ .../routes/g/[group]/docs/[doc]/+page.svelte | 150 +- web/tests/unit/basket.test.ts | 75 + web/tests/unit/lexicon.test.ts | 2 + web/tests/unit/versions.test.ts | 2 +- web/tests/unit/wallet.test.ts | 41 + 36 files changed, 4353 insertions(+), 234 deletions(-) create mode 100644 web/e2e/chain-mock.mjs create mode 100644 web/e2e/vote.spec.ts create mode 100644 web/src/lib/basket.ts create mode 100644 web/src/lib/components/BallotPanel.svelte create mode 100644 web/src/lib/components/ChainSign.svelte create mode 100644 web/src/lib/components/ChoicePicker.svelte rename web/src/lib/components/{VoteView.svelte => VersionDetails.svelte} (75%) create mode 100644 web/src/lib/components/VoteFold.svelte create mode 100644 web/src/lib/picks.svelte.ts create mode 100644 web/src/lib/vote.ts create mode 100644 web/src/lib/wallet/gecko.ts create mode 100644 web/src/lib/wallet/injected.ts create mode 100644 web/tests/unit/basket.test.ts create mode 100644 web/tests/unit/wallet.test.ts diff --git a/crates/ld-server/src/ballots.rs b/crates/ld-server/src/ballots.rs index 3a1db7b..561b1f3 100644 --- a/crates/ld-server/src/ballots.rs +++ b/crates/ld-server/src/ballots.rs @@ -310,6 +310,29 @@ pub async fn status( Ok(Json(json!({ "ballot": view(&state, &row).await? }))) } +/// `DELETE /api/ballots/{id}` — the wallet refused to sign, or could not pay: +/// nothing left for the chain, so the ballot is let go at once rather than shown +/// as waiting for a day. Only a pending ballot, only by the session that asked. +pub async fn drop( + State(state): State, + User(user): User, + Path(id): Path, +) -> AppResult> { + let row = load(&state, &id).await?; + if row.requested_by != user.id { + return Err(AppError::NotFound); + } + sqlx::query( + "UPDATE ballots SET status = 'expired', reason = 'dropped', settled_at = ? WHERE id = ? AND status = 'pending'", + ) + .bind(util::now()) + .bind(&id) + .execute(&state.db) + .await?; + let row = load(&state, &id).await?; + Ok(Json(json!({ "ballot": view(&state, &row).await? }))) +} + async fn view(state: &AppState, row: &BallotRow) -> AppResult { let voter: Option = match &row.voter_id { Some(v) => { @@ -579,6 +602,7 @@ pub struct MyBallot { /// Per version: the most recent pending ballot of `me`; else its most recent /// ballot if that one was refused or let go (a confirmed one shows as its vote). +/// A ballot the wallet never signed (dropped) says nothing. pub async fn my_ballots(state: &AppState, me: &str, doc_id: &str) -> AppResult> { #[derive(sqlx::FromRow)] struct Row { @@ -591,7 +615,8 @@ pub async fn my_ballots(state: &AppState, me: &str, doc_id: &str) -> AppResult( "SELECT l.proposal_id, b.id, b.status, b.reason, l.choice FROM ballots b JOIN ballot_lines l ON l.ballot_id = b.id - WHERE b.doc_id = ? AND b.requested_by = ? ORDER BY b.created_at, b.rowid", + WHERE b.doc_id = ? AND b.requested_by = ? AND IFNULL(b.reason, '') <> 'dropped' + ORDER BY b.created_at, b.rowid", ) .bind(doc_id) .bind(me) diff --git a/crates/ld-server/src/lib.rs b/crates/ld-server/src/lib.rs index 4074829..1a1a614 100644 --- a/crates/ld-server/src/lib.rs +++ b/crates/ld-server/src/lib.rs @@ -121,7 +121,7 @@ pub fn api_router() -> Router { .route("/docs/{id}/proposals", post(proposals::create)) .route("/docs/{id}/votes", post(ballots::vote_many)) .route("/docs/{id}/ballots", post(ballots::create)) - .route("/ballots/{id}", get(ballots::status)) + .route("/ballots/{id}", get(ballots::status).delete(ballots::drop)) .route("/proposals/{id}", get(proposals::detail).patch(proposals::edit)) .route("/proposals/{id}/submit", post(proposals::submit)) .route("/proposals/{id}/withdraw", post(proposals::withdraw)) diff --git a/crates/ld-server/tests/ballots.rs b/crates/ld-server/tests/ballots.rs index 9f0e269..b05e7d1 100644 --- a/crates/ld-server/tests/ballots.rs +++ b/crates/ld-server/tests/ballots.rs @@ -568,6 +568,37 @@ async fn a_ballot_not_seen_in_time_is_let_go() { assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["status"], "expired"); } +#[tokio::test] +async fn a_ballot_the_wallet_refused_to_sign_is_let_go_at_once() { + let indexer = Indexer::start().await; + let (mut c, state) = Client::new(chain_config(&indexer)).await; + c.login("Chiara").await; + let doc = c.doc("monnaie-libre").await; + let i1 = version(&doc, "I1", "inForce"); + let first = c.ballot(&doc, &[(&i1, Some("against"))], Some(ALICE)).await; + let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await; + assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], b["id"]); + // Another session may not let it go. + let mut other = c.other(); + other.login("Yvv").await; + let (s, _) = other.call("DELETE", &format!("/api/ballots/{}", b["id"].as_str().expect("id")), None).await; + assert_eq!(s, StatusCode::NOT_FOUND); + // Its own session does: no longer waiting, and it says nothing under the article. + let dropped = c.ok("DELETE", &format!("/api/ballots/{}", b["id"].as_str().expect("id")), None).await; + assert_eq!( + (dropped["ballot"]["status"].as_str(), dropped["ballot"]["reason"].as_str()), + (Some("expired"), Some("dropped")) + ); + assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], first["id"]); + // A confirmed ballot is never let go. + indexer.pay(100, first["comment"].as_str().expect("c"), alice()); + ballots::sweep(&state).await.expect("sweep"); + let kept = c.ok("DELETE", &format!("/api/ballots/{}", first["id"].as_str().expect("id")), None).await; + assert_eq!(kept["ballot"]["status"], "confirmed"); + let now = c.doc("monnaie-libre").await; + assert_eq!((find(&now, &i1)["myVote"].as_str(), find(&now, &i1)["myBallot"].is_null()), (Some("against"), true)); +} + #[tokio::test] async fn a_session_sees_its_own_ballots_only() { let indexer = Indexer::start().await; diff --git a/web/e2e/chain-mock.mjs b/web/e2e/chain-mock.mjs new file mode 100644 index 0000000..7ea7e5a --- /dev/null +++ b/web/e2e/chain-mock.mjs @@ -0,0 +1,120 @@ +// A simulated Ğ1 chain for the end-to-end tests (ADR 0008): never a real node, +// never a real transaction. Two doors: +// +// - `/rpc`, the « node »: the page, in test mode, sends what the fake wallet +// signed (JSON as hex, see `testExtrinsic` in src/lib/wallet/injected.ts); +// each one is included in a new block, as the transfer and the remark of the +// real `utility.batchAll` would be; +// - `/v1/graphql`, the « indexer »: answers the server's query +// `txComments(condition: {hash})` in the shape of indexer.duniter.org. +// +// And `/control/*` for the tests: `hide` (what is included next stays unseen by +// the indexer, as an indexer late on the chain), `show` (stop hiding), `release` +// (the indexer catches up), `silent` (the indexer stops answering) and `state`. +// +// Listens on an ephemeral port on 127.0.0.1, written to the file given as argument. + +import { createServer } from 'node:http'; +import { writeFileSync } from 'node:fs'; +import { blake2b } from '@noble/hashes/blake2.js'; + +// Substrate's well-known development keys as Ğ1 addresses, with the identity each +// one has on this simulated chain. Bob has an identity, but is not a member. +const IDENTITIES = { + g1Pn1geRZnHcVdWdKkoYwQrAARDTXK1D7H3zuXj7GNayT1thu: { index: 9001, name: 'Alice', isMember: true }, + g1NCrQaFReZcbUW8pKsazfWFCKNSdrTomnDraUMybJc4DFF5g: { index: 9002, name: 'Bob', isMember: false }, + g1NFWUkPTyZWE77YUrfBKfAhV9W81qmgdD17v1NGhb1EjYFqi: { index: 9003, name: 'Charlie', isMember: true }, + g1QBoGEqZuxL77pF4rMa25f1cH6HAxwgBq66zZu76JPqsaVgj: { index: 9005, name: 'Eve', isMember: true }, +}; + +const state = { block: 3_400_000, comments: [], hiding: false, silent: false, submitted: 0 }; + +const hex = (bytes) => Buffer.from(bytes).toString('hex'); +const remarkHash = (remark) => `0x${hex(blake2b(Buffer.from(remark, 'utf8'), { dkLen: 32 }))}`; + +function send(res, status, body, cors = false) { + const headers = { 'content-type': 'application/json' }; + if (cors) Object.assign(headers, { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type' }); + res.writeHead(status, headers); + res.end(body === undefined ? '' : JSON.stringify(body)); +} + +async function read(req) { + let raw = ''; + for await (const chunk of req) raw += chunk; + try { + return JSON.parse(raw || '{}'); + } catch { + return {}; + } +} + +/** One signed extrinsic of the test mode: a new block, its transfer and its remark. */ +function include(fields) { + state.block += 1; + state.submitted += 1; + const comment = { + id: `${String(state.block).padStart(10, '0')}-${hex(Buffer.from(String(state.block))).slice(-5)}-000002`, + blockNumber: state.block, + remark: fields.remark, + author: { id: fields.address, identity: IDENTITIES[fields.address] ?? null }, + transfersByCommentId: { nodes: [{ fromId: fields.address, toId: fields.dest, amount: String(fields.amount) }] }, + hidden: state.hiding, + }; + state.comments.push(comment); + return comment; +} + +const server = createServer(async (req, res) => { + const url = new URL(req.url ?? '/', 'http://mock'); + if (req.method === 'OPTIONS') return send(res, 204, undefined, true); + + if (url.pathname === '/rpc' && req.method === 'POST') { + const body = await read(req); + if (body.method !== 'author_submitExtrinsic') return send(res, 200, { jsonrpc: '2.0', id: body.id, error: { code: -32601, message: 'Method not found' } }, true); + let fields; + try { + fields = JSON.parse(Buffer.from(String(body.params?.[0] ?? '').slice(2), 'hex').toString('utf8')); + } catch { + fields = null; + } + if (!fields?.signature || !fields.remark || !fields.address) { + return send(res, 200, { jsonrpc: '2.0', id: body.id, error: { code: 1010, message: 'Invalid Transaction' } }, true); + } + const c = include(fields); + return send(res, 200, { jsonrpc: '2.0', id: body.id, result: `0x${hex(blake2b(Buffer.from(c.id), { dkLen: 32 }))}` }, true); + } + + if (url.pathname === '/v1/graphql' && req.method === 'POST') { + const body = await read(req); + const hash = body.variables?.hash; + // Only the query of the ballots; the others (size of the web of trust) get nothing, as from a dead indexer. + if (state.silent || !hash || !String(body.query ?? '').includes('txComments')) return send(res, 502, { error: 'unavailable' }); + const nodes = state.comments + .filter((c) => !c.hidden && remarkHash(c.remark) === hash) + .sort((a, b) => a.blockNumber - b.blockNumber) + .map(({ hidden: _, ...c }) => c); + return send(res, 200, { data: { txComments: { nodes } } }); + } + + if (url.pathname.startsWith('/control/') && req.method === 'POST') { + const what = url.pathname.slice('/control/'.length); + if (what === 'hide') state.hiding = true; + else if (what === 'show') state.hiding = false; + else if (what === 'release') { + state.hiding = false; + for (const c of state.comments) c.hidden = false; + } else if (what === 'silent') state.silent = url.searchParams.get('on') !== '0'; + else if (what !== 'state') return send(res, 404, { error: 'unknown' }, true); + return send(res, 200, { block: state.block, submitted: state.submitted, hidden: state.comments.filter((c) => c.hidden).length }, true); + } + + send(res, 404, { error: 'not_found' }, true); +}); + +server.listen(0, '127.0.0.1', () => { + const { port } = server.address(); + const file = process.argv[2]; + if (file) writeFileSync(file, String(port)); + console.log(`chain mock on 127.0.0.1:${port}`); +}); diff --git a/web/e2e/entree.spec.ts b/web/e2e/entree.spec.ts index ce6ac02..73a8854 100644 --- a/web/e2e/entree.spec.ts +++ b/web/e2e/entree.spec.ts @@ -1,5 +1,5 @@ import { expect, test, type Page } from '@playwright/test'; -import { DOC, person, signIn } from './helpers'; +import { chain, CHARLIE, DOC, person, signIn } from './helpers'; const ML = '/g/monnaie-libre'; const path = (p: string) => (u: URL) => u.pathname === p; @@ -145,15 +145,23 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to await expect(rows.first()).toContainText('33 modules à voter'); await expect(rows.first()).toContainText('dont 33 sans ton vote'); // A vote on one version of E3 (it has a variant): one module fewer without her vote. « Contre » - // counts as much, and leaves the previews of C9 unchanged (the server is shared). - const group = await (await page.request.get('/api/groups/monnaie-libre')).json(); - const doc = await (await page.request.get(`/api/docs/${group.docs[0].id}`)).json(); - const e3 = doc.units.find((u: { code: string }) => u.code === 'E3'); - const r = await page.request.post(`/api/proposals/${e3.proposals[1].id}/vote`, { - data: { choice: 'against' }, + // counts as much, and leaves the previews of C9 unchanged (the server is shared). In Monnaie + // libre the vote goes through the Ğ1 chain: the ballot, then what her wallet (Charlie's + // account) would send, on the simulated chain. + const { doc, node } = await chain(page); + const e3 = doc.units.find((u) => u.code === 'E3'); + const r = await page.request.post(`/api/docs/${doc.doc.id}/ballots`, { + data: { lines: [{ proposalId: e3?.proposals[1]?.id, choice: 'against' }], author: CHARLIE.address }, headers: { 'x-requested-with': 'libredecision' }, }); expect(r.ok()).toBe(true); + const { ballot } = await r.json(); + const fields = { address: CHARLIE.address, dest: ballot.account, amount: String(ballot.amount), remark: ballot.comment, signature: '0x01' }; + const extrinsic = `0x${Buffer.from(JSON.stringify(fields)).toString('hex')}`; + await page.request.post(node, { data: { jsonrpc: '2.0', id: 1, method: 'author_submitExtrinsic', params: [extrinsic] } }); + await expect + .poll(async () => (await (await page.request.get(`/api/ballots/${ballot.id}`)).json()).ballot.status, { timeout: 15_000 }) + .toBe('confirmed'); await page.reload(); await expect(rows.first()).toContainText('33 modules à voter'); await expect(rows.first()).toContainText('dont 32 sans ton vote'); diff --git a/web/e2e/helpers.ts b/web/e2e/helpers.ts index 3b041b2..b9c95a8 100644 --- a/web/e2e/helpers.ts +++ b/web/e2e/helpers.ts @@ -3,6 +3,13 @@ import type { Browser, Page } from '@playwright/test'; export const DOC = '/g/monnaie-libre/docs/acte-engagement-certification'; export const TEST_DOC = '/g/essai/docs/acte-engagement-certification'; +/** The vote account of the e2e server (serve.sh): Dave's development key. */ +export const VOTE_ACCOUNT = 'g1L5EYvvSBaYuQCPW78bHxYcvHfmWAqHYe3Gsrp9SV4kWZaEM'; +/** Wallets of the simulated chain (chain-mock.mjs): Alice and Charlie are members, Bob is not. */ +export const ALICE = { address: 'g1Pn1geRZnHcVdWdKkoYwQrAARDTXK1D7H3zuXj7GNayT1thu', name: 'Alice' }; +export const BOB = { address: 'g1NCrQaFReZcbUW8pKsazfWFCKNSdrTomnDraUMybJc4DFF5g', name: 'Bob' }; +export const CHARLIE = { address: 'g1NFWUkPTyZWE77YUrfBKfAhV9W81qmgdD17v1NGhb1EjYFqi', name: 'Charlie' }; + export async function signIn(page: Page, name: string, admin = false) { const r = await page.request.post('/api/auth/dev', { data: { name, admin }, @@ -17,3 +24,61 @@ export async function person(browser: Browser, name: string, admin = false): Pro await signIn(page, name, admin); return page; } + +/** + * A fake wallet extension, as duniter-connect offers itself to pages + * (`window.injectedWeb3`): it lists `accounts` and signs whatever it is given, + * recording it in `window.__ldSigned`; `window.__ldRefuse = true` makes it refuse. + * It signs nothing real: the page, in test mode, sends to the simulated node. + */ +export async function wallet(page: Page, accounts: { address: string; name: string }[]) { + await page.addInitScript((list) => { + const w = window as unknown as Record & { __ldSigned: unknown[]; __ldRefuse?: boolean }; + w.__ldSigned = []; + w.injectedWeb3 = { + 'duniter-connect': { + version: 'e2e', + enable: async () => ({ + accounts: { get: async () => list, subscribe: () => () => undefined }, + signer: { + signPayload: async (payload: Record) => { + if (w.__ldRefuse) throw new Error('Cancelled'); + w.__ldSigned.push(payload); + return { id: w.__ldSigned.length, signature: `0x${'01'.repeat(64)}` }; + }, + }, + }), + }, + }; + }, accounts); +} + +/** What the fake wallet signed, read back (the test extrinsic is JSON as hex). */ +export async function signed(page: Page): Promise<{ address: string; dest: string; amount: string; remark: string }[]> { + return page.evaluate(() => + ((window as unknown as { __ldSigned: { method: string }[] }).__ldSigned ?? []).map((p) => { + const bytes = (p.method.slice(2).match(/../g) ?? []).map((b) => parseInt(b, 16)); + return JSON.parse(new TextDecoder().decode(Uint8Array.from(bytes))); + }), + ); +} + +/** A screenshot for the visual check, when `LD_SHOTS` names a directory (never in the loop). */ +export async function shot(page: Page, name: string) { + const dir = process.env.LD_SHOTS; + if (dir) await page.screenshot({ path: `${dir}/${name}.png` }); +} + +/** The control door of the simulated chain, found where the page finds its node. */ +export async function chain(page: Page, docPath = DOC) { + const [, , group] = docPath.split('/'); + const g = await (await page.request.get(`/api/groups/${group}`)).json(); + const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json(); + const node: string = d.vote.chain.testNode; + const base = node.replace(/\/rpc$/, ''); + return { + doc: d as { doc: { id: string }; units: { code: string; inForce: { id: string } | null; proposals: { id: string }[] }[] }, + node, + control: async (what: 'hide' | 'show' | 'release' | 'state' | 'silent') => (await page.request.post(`${base}/control/${what}`)).json(), + }; +} diff --git a/web/e2e/mobile.spec.ts b/web/e2e/mobile.spec.ts index d7ebfee..15ffc4f 100644 --- a/web/e2e/mobile.spec.ts +++ b/web/e2e/mobile.spec.ts @@ -1,7 +1,31 @@ -import { expect, test } from '@playwright/test'; -import { DOC, signIn } from './helpers'; +import { expect, test, type Locator } from '@playwright/test'; +import { DOC, shot, signIn } from './helpers'; -test('M1 — mobile : logo dans la barre, menu en tiroir, actions visibles, panneau en feuille, réglages lisibles', async ({ page }) => { +/** WCAG contrast ratio of two CSS colours `rgb(r, g, b)`. */ +function contrast(a: string, b: string): number { + const lum = (c: string) => { + const [r, g, bl] = (c.match(/[\d.]+/g) ?? []).slice(0, 3).map((x) => { + const v = Number(x) / 255; + return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (bl ?? 0); + }; + const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x); + return ((hi ?? 0) + 0.05) / ((lo ?? 0) + 0.05); +} + +/** The colour the ring of a swatch is drawn on: the first opaque background around it. */ +async function backdrop(el: Locator): Promise { + return el.evaluate((node) => { + for (let n: Element | null = node.parentElement; n; n = n.parentElement) { + const bg = getComputedStyle(n).backgroundColor; + if (bg && bg !== 'transparent' && !/rgba\(.*,\s*0\)$/.test(bg)) return bg; + } + return getComputedStyle(document.body).backgroundColor; + }); +} + +test('M1 — mobile : logo dans la barre, menu en tiroir, vote déplié, panier, réglages lisibles, pastilles au clavier', async ({ page }) => { await signIn(page, 'Chiara'); await page.goto('/'); const brand = page.locator('header.topbar a.brand'); @@ -13,15 +37,67 @@ test('M1 — mobile : logo dans la barre, menu en tiroir, actions visibles, pann const drawer = page.getByRole('dialog', { name: 'Boîte à outils' }); await expect(drawer.locator('.tool', { hasText: 'Protocoles' })).toBeVisible(); await expect(drawer.locator('.tool', { hasText: 'Docs de référence' }).locator('.count')).toHaveText('1'); - await expect(drawer.getByRole('radiogroup', { name: 'Ambiance' }).getByRole('radio')).toHaveCount(4); + const moods = drawer.getByRole('radiogroup', { name: 'Ambiance' }); + await expect(moods.getByRole('radio')).toHaveCount(4); + + // The swatches, by keyboard: one stop for Tab, the arrows move and choose; the focus ring + // reads at 3:1 or more on the page whatever the swatch, and is not the mark of the chosen one. + await expect(moods.locator('[tabindex="0"]')).toHaveCount(1); + const start = (await page.locator('html').getAttribute('data-mood')) ?? ''; + await moods.getByRole('radio', { checked: true }).focus(); + for (let i = 0; i < 4; i++) { + await page.keyboard.press('ArrowRight'); + const now = moods.getByRole('radio', { checked: true }); + await expect(now).toBeFocused(); + await expect(moods.locator('[tabindex="0"]')).toHaveCount(1); + const ring = await now.evaluate((el) => { + const cs = getComputedStyle(el); + return { style: cs.outlineStyle, color: cs.outlineColor, width: parseFloat(cs.outlineWidth) }; + }); + expect(ring.style).toBe('dashed'); + expect(ring.width).toBeGreaterThanOrEqual(2); + const mood = await page.locator('html').getAttribute('data-mood'); + expect(contrast(ring.color, await backdrop(moods)), `${mood}: ${ring.color}`).toBeGreaterThanOrEqual(3); + if (i === 0) await shot(page, '7-pastilles-focus-mobile'); + } + await expect(page.locator('html')).toHaveAttribute('data-mood', start); + await page.keyboard.press('ArrowLeft'); + await expect(page.locator('html')).not.toHaveAttribute('data-mood', start); + await page.keyboard.press('ArrowRight'); + await expect(page.locator('html')).toHaveAttribute('data-mood', start); + // The chosen one, without the focus: its own mark, a ring of ink, never the dashed one. + const chosen = moods.getByRole('radio', { checked: true }); + await chosen.evaluate((el) => (el as HTMLElement).blur()); + const mark = await chosen.evaluate((el) => ({ outline: getComputedStyle(el).outlineStyle, shadow: getComputedStyle(el).boxShadow })); + expect(mark.outline).toBe('none'); + expect(mark.shadow).toContain('0px 0px 0px 2px'); await drawer.getByRole('button', { name: 'Fermer' }).first().click(); + + // The vote unfolds under the article, on a phone too. const first = page.locator('section.text article.unit').first(); await first.getByRole('button', { name: 'Le vote' }).click(); - await expect(page.locator('aside.panel').getByText('Seuil : 50 voix pour')).toBeVisible(); + const fold = first.locator('section.fold'); + await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible(); // The settings of the formula read on a phone: the table of the five levels holds in the sheet. - await page.locator('aside.panel').getByRole('button', { name: 'Réglages de la formule' }).click(); - await expect(page.locator('aside.panel').getByRole('tab', { name: 'Réglages' })).toBeInViewport(); - await expect(page.locator('aside.panel table tbody tr')).toHaveCount(5); - const fits = await page.locator('aside.panel .scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1); + await fold.getByRole('button', { name: 'Réglages de la formule' }).click(); + const panel = page.locator('aside.panel'); + await expect(panel.getByRole('tab', { name: 'Réglages' })).toBeInViewport(); + await expect(panel.locator('table tbody tr')).toHaveCount(5); + const fits = await panel.locator('.scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1); expect(fits).toBe(true); + await panel.getByRole('button', { name: 'Fermer le panneau' }).click(); + + // The basket holds on a phone: its send button stays in sight, nothing overflows. + await page.getByRole('button', { name: /^Voter en lot/ }).click(); + await expect(panel.getByRole('heading', { name: 'Panier de vote' })).toBeVisible(); + await panel.getByRole('group', { name: 'Ton choix pour « Unicité du compte »' }).getByRole('button', { name: 'Pour' }).click(); + await panel.getByRole('group', { name: 'Ton choix pour « Réciprocité »' }).getByRole('button', { name: 'Contre' }).click(); + const send = panel.getByRole('button', { name: 'Signer et envoyer (2)' }); + await expect(send).toBeInViewport(); + const holds = await panel.locator('.body').evaluate((el) => el.scrollWidth <= el.clientWidth + 1); + expect(holds).toBe(true); + await shot(page, '3-panier-mobile'); + // Taken back out of the basket: nothing was sent. + await panel.getByRole('group', { name: 'Tout le document' }).getByRole('button', { name: 'Vider' }).click(); + await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible(); }); diff --git a/web/e2e/reader.spec.ts b/web/e2e/reader.spec.ts index c4ee3dc..b105d87 100644 --- a/web/e2e/reader.spec.ts +++ b/web/e2e/reader.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { DOC, person, signIn, TEST_DOC } from './helpers'; +import { BOB, DOC, person, signIn, TEST_DOC, wallet } from './helpers'; test('C1 — l’accueil sans session : le titre, le leitmotiv en trois lignes, la connexion, le menu complet', async ({ page }) => { await page.goto('/'); @@ -49,24 +49,29 @@ test('C3 — le texte en vigueur : raison d’être et deux engagements, seed a await expect(page.locator('ul.deep button.row')).toHaveCount(30); }); -test('C4 — les actions sont contextuelles, le vote s’ouvre en panneau', async ({ page }) => { +test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => { await signIn(page, 'Chiara'); await page.goto(DOC); const first = page.locator('section.text article.unit').first(); await first.hover(); - await first.getByRole('button', { name: 'Le vote' }).click(); - const panel = page.locator('aside.panel'); - await expect(panel.getByText('Seuil : 50 voix pour')).toBeVisible(); - await expect(panel.getByText('Il manque 50 voix pour.')).toBeVisible(); - await panel.getByText('La formule du seuil').click(); - await expect(panel.getByText(/W — électorat/)).toBeVisible(); + const toggle = first.getByRole('button', { name: 'Le vote' }); + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + const fold = first.locator('section.fold'); + await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible(); + await expect(fold.getByText('Il manque 50 voix pour.')).toBeVisible(); + await fold.getByText('La formule du seuil').click(); + await expect(fold.getByText(/W — électorat/)).toBeVisible(); // The level of the article sets M and G: the Raison d'être is at 3 / 5. - await expect(panel.getByText('M — majorité à pleine participation : 55 % — inertie 3 / 5 · moyenne')).toBeVisible(); - await expect(panel.getByRole('button', { name: 'Réglages de la formule' })).toBeVisible(); - // The panel lives in the URL. - await expect(page).toHaveURL(/onglet=vote/); - await page.keyboard.press('Escape'); - await expect(panel).toHaveCount(0); + await expect(fold.getByText('M — majorité à pleine participation : 55 % — inertie 3 / 5 · moyenne')).toBeVisible(); + await expect(fold.getByRole('button', { name: 'Réglages de la formule' })).toBeVisible(); + // No window opens: the vote unfolds in place, lives in the URL, and folds back. + await expect(page.locator('aside.panel')).toHaveCount(0); + await expect(page).toHaveURL(/vote=/); + await toggle.click(); + await expect(fold).toHaveCount(0); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await expect(page).not.toHaveURL(/vote=/); }); test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (groupe d’essai)', async ({ browser }) => { @@ -75,12 +80,13 @@ test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (gr for (const p of [yvv, chiara]) { await p.goto(TEST_DOC); await p.locator('ul.deep button.row', { hasText: 'Les deux garanties réciproques' }).click(); - await p.locator('aside.panel').getByRole('button', { name: 'Pour', exact: true }).click(); + await p.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click(); await expect(p.getByText('Vote enregistré.')).toBeVisible(); } await expect(chiara.getByText('« Les deux garanties réciproques » est entré en vigueur.')).toBeVisible(); - await chiara.keyboard.press('Escape'); await expect(chiara.locator('section.text article.unit')).toHaveCount(4); + // In force now: its vote stays unfolded, under the text. + await expect(chiara.locator('section.text section.fold')).toContainText('2 pour · 0 contre'); }); test('C6 — une coquille se corrige sans vote', async ({ page }) => { @@ -89,7 +95,10 @@ test('C6 — une coquille se corrige sans vote', async ({ page }) => { const unicity = page.locator('section.text article.unit', { hasText: 'Unicité du compte' }); await unicity.hover(); await unicity.getByRole('button', { name: 'Le vote' }).click(); + // What is not the vote lives in the panel, tab « Détails ». + await unicity.getByRole('button', { name: 'Coquilles, traductions, signalements, historique' }).click(); const panel = page.locator('aside.panel'); + await expect(panel.getByRole('tab', { name: 'Détails' })).toHaveAttribute('aria-selected', 'true'); await panel.getByText('Corriger une coquille').click(); const body = panel.getByLabel('Texte', { exact: true }); // Typographic apostrophes: the letters and the signs stay, only their style changes. @@ -151,7 +160,7 @@ test('C10 — trustinWallet : la gestion des accès est réservée', async ({ br await expect(yvv.locator('.link code')).toContainText('/invitation#'); }); -test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de la toile votent', async ({ browser }) => { +test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de la toile votent, par la chaîne', async ({ browser }) => { const yvv = await person(browser, 'Yvv', true); const r = await yvv.request.post('/api/access/invitations', { data: { label: 'Invitée', groups: ['monnaie-libre', 'essai'] }, @@ -160,17 +169,28 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de const { link } = (await r.json()) as { link: string }; const ctx = await browser.newContext({ locale: 'fr-FR' }); const invited = await ctx.newPage(); + // Her wallet holds Bob's account: an identity, not a member. + await wallet(invited, [BOB]); await invited.goto(`/invitation#${link.split('#')[1]}`); await expect(invited).toHaveURL((u) => u.pathname === '/'); await invited.goto(DOC); await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click(); - const panel = invited.locator('aside.panel'); - await expect(panel.getByText('Ici, seuls les membres de la toile de confiance Ğ1 votent', { exact: false })).toBeVisible(); - await expect(panel.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0); - // In the test group, the same person votes. + const fold = invited.locator('section.fold'); + // An invitation gives no voice: the voice is that of the Ğ1 identity of the wallet that signs. + await expect(fold.getByText("Ta voix compte pour l'identité Ğ1 du portefeuille qui signe", { exact: false })).toBeVisible(); + await fold.getByRole('button', { name: 'Pour', exact: true }).click(); + const dialog = invited.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); + await expect( + dialog.getByText("Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1."), + ).toBeVisible({ timeout: 20_000 }); + await dialog.getByRole('button', { name: 'Fermer' }).click(); + await expect(fold.getByText("Ton dernier vote n'a pas été compté", { exact: false })).toBeVisible(); + await expect(fold.getByText('0 pour · 0 contre')).toBeVisible(); + // In the test group, the same person votes, directly. await invited.goto(TEST_DOC); await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click(); - await expect(invited.locator('aside.panel').getByRole('button', { name: 'Pour', exact: true })).toBeVisible(); + await expect(invited.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible(); + await expect(invited.locator('section.fold').getByText('Ta voix compte', { exact: false })).toHaveCount(0); }); test('C12 — lien d’équipe : chaque personne qui l’ouvre a sa propre identité', async ({ browser }) => { @@ -237,10 +257,11 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page await expect(versions).toHaveCount(2); await expect(versions.nth(0)).toContainText('variante 1.0.0'); await expect(versions.nth(1)).toContainText('variante 0.4.0-draft'); - // A version opens its vote, as any proposal. + // A version unfolds its vote below it, as any proposal. await versions.nth(1).click(); - await expect(page.locator('aside.panel').getByText('Seuil : 50 voix pour')).toBeVisible(); - await expect(page).toHaveURL(/version=/); + await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true'); + await expect(page.locator('section.fold').getByText('Seuil : 50 voix pour')).toBeVisible(); + await expect(page).toHaveURL(/vote=/); await e3.click(); await expect(e3).toHaveAttribute('aria-expanded', 'false'); await expect(versions).toHaveCount(0); @@ -273,8 +294,8 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => { const e1 = page.locator('section.text article.unit', { hasText: 'Unicité du compte' }); await e1.hover(); await e1.getByRole('button', { name: 'Le vote' }).click(); - await expect(panel.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible(); - await panel.getByRole('button', { name: 'Réglages de la formule' }).click(); + await expect(e1.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible(); + await e1.getByRole('button', { name: 'Réglages de la formule' }).click(); await expect(panel.locator('tr.current')).toHaveCount(1); await expect(panel.locator('tr.current th')).toContainText('4'); await expect(panel.locator('tr.current th')).toContainText('cet article'); @@ -287,15 +308,17 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => { const e2 = page.locator('section.text article.unit', { hasText: 'Réciprocité' }); await e2.hover(); await e2.getByRole('button', { name: 'Le vote' }).click(); - await panel.getByText("Proposer un changement d'inertie").click(); - const levels = panel.getByRole('radiogroup', { name: 'Nouvelle inertie' }).getByRole('radio'); + await e2.getByText("Proposer un changement d'inertie").click(); + const levels = e2.getByRole('radiogroup', { name: 'Nouvelle inertie' }).getByRole('radio'); await expect(levels).toHaveCount(5); await expect(levels.nth(3)).toBeDisabled(); await expect(levels.nth(3)).toContainText('actuelle'); - await expect(panel.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 4 / 5 · haute.")).toBeVisible(); + await expect(e2.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 4 / 5 · haute.")).toBeVisible(); await levels.nth(4).click(); await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true'); - await panel.getByRole('button', { name: 'Soumettre au vote' }).click(); - await expect(panel.getByRole('heading', { name: 'inertie vers 5 / 5 · très haute' })).toBeVisible(); - await expect(panel.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible(); + await e2.getByRole('button', { name: 'Soumettre au vote' }).click(); + // Its own vote unfolds, in the list of proposals. + const change = page.getByRole('region', { name: 'Le vote de « inertie vers 5 / 5 · très haute »' }); + await expect(change).toBeVisible(); + await expect(change.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible(); }); diff --git a/web/e2e/serve.sh b/web/e2e/serve.sh index 3cf3501..15e69e2 100755 --- a/web/e2e/serve.sh +++ b/web/e2e/serve.sh @@ -1,11 +1,23 @@ #!/bin/sh -# Throwaway server for the end-to-end tests: fresh database, dev login, built interface. +# Throwaway server for the end-to-end tests: fresh database, dev login, built interface, +# and a simulated Ğ1 chain (e2e/chain-mock.mjs, an ephemeral local port): the vote account +# is a well-known development key, the page sends to the fake node, the server reads the +# fake indexer. Never a real transaction. set -eu here=$(cd "$(dirname "$0")/../.." && pwd) data=/tmp/ld-e2e rm -rf "$data" mkdir -p "$data" cd "$here" +node web/e2e/chain-mock.mjs "$data/chain-mock.port" > "$data/chain-mock.log" 2>&1 & +i=0 +while [ ! -s "$data/chain-mock.port" ]; do + i=$((i + 1)) + [ "$i" -gt 100 ] && { echo "chain mock did not start" >&2; exit 1; } + sleep 0.1 +done +chain="http://127.0.0.1:$(cat "$data/chain-mock.port")" LD_DATA_DIR="$data" LD_SEED_DIR=seeds LD_WEB_DIR=web/build LD_BIND=127.0.0.1:8092 LD_DEV_LOGIN=1 \ - LD_PUBLIC_URL=http://127.0.0.1:8092 LD_RPC_ENDPOINTS=http://127.0.0.1:9 LD_INDEXERS=http://127.0.0.1:9 \ + LD_PUBLIC_URL=http://127.0.0.1:8092 LD_RPC_ENDPOINTS=http://127.0.0.1:9 LD_INDEXERS="$chain/v1/graphql" \ + LD_VOTE_ACCOUNT=g1L5EYvvSBaYuQCPW78bHxYcvHfmWAqHYe3Gsrp9SV4kWZaEM LD_VOTE_TEST_NODE="$chain/rpc" \ exec ./target/debug/ld-server diff --git a/web/e2e/vote.spec.ts b/web/e2e/vote.spec.ts new file mode 100644 index 0000000..c8bacaa --- /dev/null +++ b/web/e2e/vote.spec.ts @@ -0,0 +1,206 @@ +import { expect, test } from '@playwright/test'; +import { ALICE, chain, CHARLIE, DOC, person, shot, signed, signIn, TEST_DOC, VOTE_ACCOUNT, wallet } from './helpers'; + +// Lot B — the vote (Yvv, 2026-10-30): unfolded under the article, grouped in a +// basket, and on the Ğ1 chain where the electorate is the web of trust. The chain +// here is simulated (e2e/chain-mock.mjs): never a real transaction. + +test('C21 — le vote déplié sous l’article : ton vote, les totaux, le seuil', async ({ page, browser }) => { + await signIn(page, 'Chiara'); + await page.goto(TEST_DOC); + const e1 = page.locator('section.text article.unit', { hasText: 'Unicité du compte' }); + await e1.hover(); + const toggle = e1.getByRole('button', { name: 'Le vote' }); + await toggle.click(); + const fold = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' }); + await expect(fold).toBeVisible(); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + await expect(toggle).toHaveAttribute('aria-controls', (await fold.getAttribute('id')) ?? ''); + // How far the threshold is: the water, the totals, the votes asked and those missing. + await expect(fold.getByRole('img', { name: /Niveau : \d+ % du seuil/ })).toBeVisible(); + await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible(); + await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible(); + await expect(fold.getByText("Tu n'as pas voté sur cette version.")).toBeVisible(); + await expect(fold.getByText('0 pour · 0 contre')).toBeVisible(); + // Her vote, at once. + await fold.getByRole('button', { name: 'Pour', exact: true }).click(); + await expect(fold.locator('.voted')).toHaveText('Ton vote : pour · enregistré'); + await expect(fold.getByText('1 pour · 0 contre')).toBeVisible(); + await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveAttribute('aria-pressed', 'true'); + await expect(fold.getByText('Ton vote ne se montre à personne : seuls les totaux sont visibles.')).toBeVisible(); + await shot(page, '1-vote-deplie'); + // Someone else sees the totals, never who voted what. + const yvv = await person(browser, 'Yvv'); + await yvv.goto(page.url()); + const theirs = yvv.getByRole('region', { name: 'Le vote de « Unicité du compte »' }); + await expect(theirs.getByText('1 pour · 0 contre')).toBeVisible(); + await expect(theirs.getByText("Tu n'as pas voté sur cette version.")).toBeVisible(); + await expect(theirs.getByText('Chiara')).toHaveCount(0); + // Folded back, then her vote withdrawn: the group is left as found. + await toggle.click(); + await expect(fold).toHaveCount(0); + await toggle.click(); + await fold.getByRole('button', { name: 'Retirer mon vote' }).click(); + await expect(fold.getByText('0 pour · 0 contre')).toBeVisible(); +}); + +test('C22 — le vote groupé : une section en un geste, et l’article à deux versions se choisit', async ({ page }) => { + await signIn(page, 'Chiara'); + await page.goto(TEST_DOC); + // From the list of proposals: « Voter cette section », on the technical commitments. + await page.locator('ul.deep li.section', { hasText: 'Engagements techniques' }).getByRole('button', { name: 'Voter cette section' }).click(); + await expect(page).toHaveURL(/onglet=panier/); + await expect(page).toHaveURL(/section=technique/); + const panel = page.locator('aside.panel'); + const tech = panel.getByRole('region', { name: 'Engagements techniques' }); + await tech.getByRole('group', { name: 'Engagements techniques' }).getByRole('button', { name: 'Tout pour' }).click(); + // E3 has two versions: the grouped gesture leaves it, and says so. + const e3 = tech.locator('li.slot', { hasText: 'Connaissance suffisante' }); + await expect(e3.getByText('Plusieurs versions : choisis la tienne.')).toBeVisible(); + await expect(e3.getByRole('button', { name: 'Pour', pressed: true })).toHaveCount(0); + await expect(panel.getByRole('status')).toContainText('plusieurs versions'); + const filled = await tech.getByRole('button', { name: 'Pour', pressed: true }).count(); + expect(filled).toBeGreaterThan(3); + // Her version of E3, chosen by hand. + await e3.getByRole('group', { name: /variante 1\.0\.0/ }).getByRole('button', { name: 'Pour' }).click(); + const n = filled + 1; + await expect(panel.getByRole('button', { name: `Voter (${n})` })).toBeEnabled(); + await shot(page, '2-panier-bureau'); + // The basket stays for the tab: a reload keeps it. + await page.reload(); + await expect(page.getByRole('button', { name: `Voter en lot · ${n}` })).toBeVisible(); + // One gesture, one write. + const writes: string[] = []; + page.on('request', (r) => { + if (r.method() === 'POST' && r.url().includes('/api/')) writes.push(new URL(r.url()).pathname); + }); + await panel.getByRole('button', { name: `Voter (${n})` }).click(); + await expect(page.getByText(`${n} votes enregistrés.`)).toBeVisible(); + expect(writes.filter((u) => u.endsWith('/votes'))).toHaveLength(1); + expect(writes.filter((u) => u.endsWith('/vote'))).toHaveLength(0); + await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible(); + // Each counted: her version of E3 has its voice, the other none. + await expect(e3.getByRole('group', { name: /variante 1\.0\.0/ }).locator('..')).toContainText('1 pour · 0 contre · ton vote : pour'); + await expect(e3.getByRole('group', { name: /variante 0\.4\.0-draft/ }).locator('..')).toContainText('0 pour · 0 contre'); +}); + +test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur ; le dernier fait foi', async ({ page, browser }) => { + await signIn(page, 'Chiara'); + await wallet(page, [ALICE]); + await page.goto(DOC); + const { control } = await chain(page); + // The basket: two articles in force, for, in one transaction. + await page.getByRole('button', { name: /^Voter en lot/ }).click(); + const panel = page.locator('aside.panel'); + for (const title of ['Unicité du compte', 'Réciprocité']) { + await panel.getByRole('group', { name: `Ton choix pour « ${title} »` }).getByRole('button', { name: 'Pour' }).click(); + } + await expect(panel.getByText('Une seule transaction de 0,1 Ğ1, signée par ton portefeuille, pour tous tes choix.')).toBeVisible(); + await panel.getByRole('button', { name: 'Signer et envoyer (2)' }).click(); + const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); + await expect(dialog.getByText('2 votes, en une transaction : 0,1 Ğ1 vers le compte des votes, avec ton bulletin scellé.')).toBeVisible(); + await expect(dialog.getByText('Ton vote compte : il est inscrit sur la chaîne Ğ1.')).toBeVisible({ timeout: 20_000 }); + await expect(dialog.getByText("Compté pour l'identité Ğ1 « Alice ».")).toBeVisible(); + // One signature: the amount to the vote account and the sealed ballot — nothing of the vote in clear. + const tx = await signed(page); + expect(tx).toHaveLength(1); + expect(tx[0]).toMatchObject({ address: ALICE.address, dest: VOTE_ACCOUNT, amount: '10' }); + expect(tx[0]?.remark).toMatch(/^ld1:[0-9a-f]{64}$/); + await dialog.getByRole('button', { name: 'Fermer' }).click(); + await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible(); + await panel.getByRole('button', { name: 'Fermer le panneau' }).click(); + + // Under the article: her vote, from the chain, and the totals. + const e1 = page.locator('section.text article.unit', { hasText: 'Unicité du compte' }); + await e1.hover(); + await e1.getByRole('button', { name: 'Le vote' }).click(); + const fold1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' }); + await expect(fold1.locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1'); + await expect(fold1.getByText('1 pour · 0 contre')).toBeVisible(); + + // The last one stands, in the order of the chain: a « contre » included first, but seen late + // by the indexer, never overrides the withdrawal included after it. + const e2 = page.locator('section.text article.unit', { hasText: 'Réciprocité' }); + await e2.hover(); + await e2.getByRole('button', { name: 'Le vote' }).click(); + const fold2 = page.getByRole('region', { name: 'Le vote de « Réciprocité »' }); + await control('hide'); + await fold2.getByRole('button', { name: 'Contre', exact: true }).click(); + await expect(dialog.getByText('La chaîne Ğ1 confirme ton vote', { exact: false })).toBeVisible(); + await shot(page, '4-chaine-attente'); + await dialog.getByRole('button', { name: 'Fermer' }).click(); + await expect(fold2.getByText('En attente de la chaîne Ğ1 : contre.')).toBeVisible(); + await control('show'); + await fold2.getByRole('button', { name: 'Retirer mon vote' }).click(); + await expect(dialog.getByText('Ton vote compte : il est inscrit sur la chaîne Ğ1.')).toBeVisible({ timeout: 20_000 }); + await dialog.getByRole('button', { name: 'Fermer' }).click(); + await expect(fold2.getByText('0 pour · 0 contre')).toBeVisible(); + await control('release'); + await expect(fold2.getByText('En attente de la chaîne Ğ1', { exact: false })).toHaveCount(0, { timeout: 30_000 }); + await expect(fold2.getByText("Tu n'as pas voté sur cette version.")).toBeVisible(); + await expect(fold2.getByText('0 pour · 0 contre')).toBeVisible(); + // One vote unfolded at a time (an accordion): back to the first, untouched. + await expect(fold1).toHaveCount(0); + await e1.hover(); + await e1.getByRole('button', { name: 'Le vote' }).click(); + await expect(fold1.getByText('1 pour · 0 contre')).toBeVisible(); + await expect(fold2).toHaveCount(0); + + // Without a wallet in the browser: Ğecko, a link and a QR code; the page waits for the chain. + const other = await person(browser, 'Maëlle'); + await other.goto(DOC); + const raison = other.locator('section.text article.unit').first(); + await raison.hover(); + await raison.getByRole('button', { name: 'Le vote' }).click(); + await raison.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click(); + const sheet = other.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); + await expect(sheet.getByText("Aucun portefeuille n'est branché à ce navigateur", { exact: false })).toBeVisible(); + await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click(); + await expect(sheet.getByRole('img', { name: 'Code à scanner avec Ğecko' })).toBeVisible(); + const link = (await sheet.getByRole('link', { name: 'Ouvrir dans Ğecko' }).getAttribute('href')) ?? ''; + expect(link).toMatch(new RegExp(`^g1://${VOTE_ACCOUNT}\\?amount=0\\.1&comment=ld1:[0-9a-f]{64}$`)); + await shot(other, '5-gecko-qr'); + // Charlie pays it from Ğecko: the simulated node includes it, the page sees it counted. + const { node } = await chain(other); + const remark = link.split('comment=')[1] ?? ''; + const fields = { address: CHARLIE.address, dest: VOTE_ACCOUNT, amount: '10', remark, signature: '0x01' }; + await other.request.post(node, { + data: { jsonrpc: '2.0', id: 1, method: 'author_submitExtrinsic', params: [`0x${Buffer.from(JSON.stringify(fields)).toString('hex')}`] }, + }); + await expect(sheet.getByText("Compté pour l'identité Ğ1 « Charlie ».")).toBeVisible({ timeout: 20_000 }); +}); + +test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ; le groupe d’essai vote', async ({ page }) => { + await signIn(page, 'Chiara'); + // The e2e server has its vote account: the page is shown what a server without one answers + // (the server itself: `without_a_vote_account_the_chain_vote_is_off_and_the_test_group_still_votes`). + await page.route(/\/api\/docs\/[^/?]+$/, async (route) => { + const res = await route.fetch(); + const body = await res.json(); + if (body.vote?.way === 'chain') { + body.vote = { ...body.vote, open: false, reason: 'chain_off', chain: null }; + body.canVote = false; + } + await route.fulfill({ response: res, json: body }); + }); + await page.goto(DOC); + await expect(page.getByRole('button', { name: /^Voter en lot/ })).toHaveCount(0); + await expect(page.getByRole('button', { name: 'Voter cette section' })).toHaveCount(0); + const first = page.locator('section.text article.unit').first(); + await first.hover(); + await first.getByRole('button', { name: 'Le vote' }).click(); + const fold = first.locator('section.fold'); + await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible(); + await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0); + // The totals and the threshold stay in sight. + await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible(); + await fold.getByText('Ton vote ne se montre à personne', { exact: false }).scrollIntoViewIfNeeded(); + await shot(page, '6-vote-ferme'); + // The test group still votes, directly. + await page.goto(TEST_DOC); + await expect(page.getByRole('button', { name: /^Voter en lot/ })).toBeVisible(); + const unit = page.locator('section.text article.unit').first(); + await unit.hover(); + await unit.getByRole('button', { name: 'Le vote' }).click(); + await expect(unit.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible(); +}); diff --git a/web/package-lock.json b/web/package-lock.json index 9f32c28..f95f69b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,13 +15,57 @@ "@sveltejs/kit": "^2.70.0", "@sveltejs/vite-plugin-svelte": "^6.0.0", "@types/node": "^22.20.5", + "polkadot-api": "3.3.0", "svelte": "^5.57.0", "svelte-check": "^4.0.0", "typescript": "^5.5.0", + "uqr": "0.1.3", "vite": "^7.0.0", "vitest": "^3.2.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -551,6 +595,19 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@playwright/test": { "version": "1.63.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", @@ -574,6 +631,367 @@ "dev": true, "license": "MIT" }, + "node_modules/@polkadot-api/cli": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.22.5.tgz", + "integrity": "sha512-kMzV11SNhY+F5c+seMS8ieEnq3r8azbS81AgJZL8a8gKU7Q2W5E/sG/hetTd7NZCeLnYvDWpbat1GCZzxita0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@polkadot-api/codegen": "0.23.2", + "@polkadot-api/ink-contracts": "0.7.2", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.5", + "@polkadot-api/metadata-compatibility": "0.7.2", + "@polkadot-api/observable-client": "0.19.4", + "@polkadot-api/sm-provider": "0.3.10", + "@polkadot-api/smoldot": "0.4.9", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/wasm-executor": "^0.2.3", + "@polkadot-api/ws-middleware": "0.4.2", + "@polkadot-api/ws-provider": "0.9.2", + "@types/node": "^26.6.5", + "commander": "^15.0.0", + "execa": "^10.2.0", + "fs.promises.exists": "^1.1.4", + "ora": "^9.4.1", + "read-pkg": "^10.1.0", + "rollup": "^4.64.12", + "rollup-plugin-esbuild": "^6.2.1", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "typescript": "^6.0.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main/src/main.js", + "polkadot-api": "dist/main/src/main.js" + } + }, + "node_modules/@polkadot-api/cli/node_modules/@types/node": { + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@polkadot-api/cli/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@polkadot-api/cli/node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/codegen": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.23.2.tgz", + "integrity": "sha512-vV7UybrNMCtB79phMBFVWoVaYO8ApVE/5fE5uw7TBUMFTcDZZ3VwVM2qVnNCK/X4jH/FAnMSGoOrdjRVQyeSFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.7.2", + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/metadata-compatibility": "0.7.2", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@polkadot-api/ink-contracts": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.7.2.tgz", + "integrity": "sha512-ipKLbaboWImnjO0TuqLJNDdzoC0tLUTvJ+XNKW0wWm1OM2cXX8X0nXfAfTGy14PdUkyKjLncoPNoqgbEhmAh/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.2.0.tgz", + "integrity": "sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.4.1.tgz", + "integrity": "sha512-F1Hw01C60jn98KQ0vBbgcwAvEcMcKLsJ5kFzq600sgjc6Rnbn1VF/XjTdrjcIqvfpR9sXyGIrpImrov2Usbrsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/known-chains": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.12.5.tgz", + "integrity": "sha512-odoCGRMKTMEq5wS851aw4qwEExvEX4j57sUI3R09+GW4RACnDqgbYxu8GKdqkXtrnHWAUr8lKMaN6RXaHfkTdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/logs-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.2.0.tgz", + "integrity": "sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.3.2.tgz", + "integrity": "sha512-M/ja1A+Hec16oGSiP8qvDuELe0JyxxEBwrfYjg/rE66yM/FiQ+29RrrmHQ8N20AtTlKn4RTaE3gbqQ02K6n1+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.15.2.tgz", + "integrity": "sha512-V8drZbzvu6Aa1EKQs4x/jOLFO05uATuEZagSqQ76ZF4skb0T8qhFmFrXnYpIiwkPndxo0mbY73cfz7rRdpshvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.7.2.tgz", + "integrity": "sha512-sDIbIZHNbf6bm1k56RZf+vUakwni7p8OpO8UUr+7k2KVeQ6K+z1lWmY+Y23IwcgvUI8Lh+2SxH7vPtAsggevSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/substrate-bindings": "0.21.2" + } + }, + "node_modules/@polkadot-api/observable-client": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.19.4.tgz", + "integrity": "sha512-xGzTQwdFfy7SYjS4n6op99rVJwCU0ghAHAEYLTfs3K1bO9dNZcgfduN/6imvdaZCOhZ2SPLBKjJvcEBUIzHW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/pjs-signer": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.9.0.tgz", + "integrity": "sha512-FMVMh5Gqmx0SFrsRJZgYwk44wfNz8N9vqz8zquj6eQYk+XB8KNEds15YCxsCFuyYVzKQ9CRnLSpOUkh24jY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/raw-tx-creator": "0.5.0", + "@polkadot-api/signers-common": "0.4.0", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/tx-creator": "0.2.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@polkadot-api/raw-client": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.3.0.tgz", + "integrity": "sha512-u/wM9W7ugIXxBSOEV8+zvQI43b5vgSI0pvE0Rg8PV0G65BTLK0SMc2o2SQL0HtS5+bi5sw/gg4reBJ/0a4U0cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0" + } + }, + "node_modules/@polkadot-api/raw-tx-creator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-tx-creator/-/raw-tx-creator-0.5.0.tgz", + "integrity": "sha512-NVI7rFc+4x6fjKumR0W6CWA8EbSF8YnPI0zns8wL4qmC/FS1VfwCW4zP3EcGZGcxP8lOc7V0pS7Ky/OpbAIAFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.4.0", + "@polkadot-api/merkleize-metadata": "1.3.2", + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/signers-common": "0.4.0", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/tx-creator": "0.2.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/signers-common": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.4.0.tgz", + "integrity": "sha512-jMSZxMcfzPqgU+wVT75H7a5cHBS59ppoGghG3L7Ef9FguFa4p6LvH4pK2xjEQ2i21HIZ7KKKojWSSklnQzUUrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/tx-creator": "0.2.0", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/sm-provider": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.3.10.tgz", + "integrity": "sha512-TsuUf81ej0zIeSsSo8c0MEJO5Z3iO7ZMBlv5A6JPcmCx9bZhS/Vjj+DWQW7V+M05KYWdlbS//2tpOg69xgb7sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@polkadot-api/smoldot": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.4.9.tgz", + "integrity": "sha512-UbebkD0DOH3UIUAQAhto8UhyVaG3zmiquts4n7BDhZ2x8JSGpskHZv1VJT7GsCJcYBfHy2L5btZpgYrgpKU1CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^26.6.5", + "smoldot": "~3.7.0" + } + }, + "node_modules/@polkadot-api/smoldot/node_modules/@types/node": { + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@polkadot-api/smoldot/node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.21.2.tgz", + "integrity": "sha512-YjQnmS1dfoQky3Qoddm59CCK+5jxl57uvXtGt0qwbgJ7y0zHHznBCDPv8lU4iUzQKS4dn7aSwIi5j7CE3DGAbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.4.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.4.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@polkadot-api/substrate-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.7.0.tgz", + "integrity": "sha512-TWCc4MAMa5SLVQXmomLHknbj+bztQ/Yclgwm8ENBhz8hR7c9rw9FBAkCa02jMBMCAygPhp3ayGRq+UFcF8KIxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/utils": "0.4.0" + } + }, + "node_modules/@polkadot-api/tx-creator": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/tx-creator/-/tx-creator-0.2.0.tgz", + "integrity": "sha512-xac4Lx/8bB7+glcU/HG8uzPYdRFcs0MkSni07xrTtOizWt6j07Q9XEPxtqmT7aL5Ne93uCl0bhvk+VhezHbf0w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "rxjs": "^7.8.2" + } + }, + "node_modules/@polkadot-api/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/wasm-executor": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/wasm-executor/-/wasm-executor-0.2.3.tgz", + "integrity": "sha512-B2h1o+Qlo9idpASaHvMSoViB2I5ko5OAfwfhYF8LQDkTADK0B+SeStzNj1Qn+FG34wqTuv7HzBCdjaUgzYINJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polkadot-api/ws-middleware": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-middleware/-/ws-middleware-0.4.2.tgz", + "integrity": "sha512-UostyJupfCk5jh+MUOjBBZ6qRYwQ2UhPWGnv25sVc0thRzBO9tV93sMg+jsfGLyNOSBmW8F9UlzXYut1/vEfIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/raw-client": "0.3.0", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/ws-provider": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.9.2.tgz", + "integrity": "sha512-FWISrSF4op3WayDUtxjLGDtq/KfhnrbseJRp7UxuMNccwCHaGpwkFvO5meVLN71BaHyFv9gCFA3rV6tPDxcVdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/json-rpc-provider-proxy": "0.4.1", + "@polkadot-api/utils": "0.4.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.64.12", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.12.tgz", @@ -924,6 +1342,46 @@ "win32" ] }, + "node_modules/@rx-state/core": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz", + "integrity": "sha512-Z+3hjU2xh1HisLxt+W5hlYX/eGSDaXXP+ns82gq/PLZpkXLu0uwcNUh9RLY3Clq4zT+hSsA3vcpIGt6+UAb8rQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "rxjs": ">=7" + } + }, + "node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1084,6 +1542,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", @@ -1212,6 +1677,19 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", @@ -1269,6 +1747,19 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -1295,6 +1786,35 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1305,6 +1825,16 @@ "node": ">=6" } }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -1353,6 +1883,39 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.6.tgz", + "integrity": "sha512-gQhL1ksGBLQbHeAo47YU6cs2ahd3Pv+8PFYoWogNZbQnNwQyOh9Ad5kbeHFiWwbKdeWQJ5Z7Y7mwb9ew2hXBLw==", + "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/detect-indent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", + "integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/devalue": { "version": "5.9.4", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", @@ -1444,6 +2007,33 @@ "@types/estree": "^1.0.0" } }, + "node_modules/execa": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-10.2.1.tgz", + "integrity": "sha512-/Bofl3ZBh2Q4/PG0W63qFzP5vDAypi8Zc90UWTBzaDNIYiX7apPOd0N6L8hG8CojUdq4V51adu8BObP+iVsSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "figures": "^6.1.0", + "get-stream": "^9.0.1", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.3.1", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "which-command": "^0.1.0", + "yoctocolors": "^2.2.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1472,6 +2062,32 @@ } } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fs.promises.exists": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fs.promises.exists/-/fs.promises.exists-1.1.4.tgz", + "integrity": "sha512-lJzUGWbZn8vhGWBedA+RYjB/BeJ+3458ljUfmplqhIeb6ewzTFWNPCR1HCiYCkXV9zxcHz9zXkJzMsEgDLzh3Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/fs.promises.exists?sponsor=1" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1487,6 +2103,121 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1497,6 +2228,32 @@ "@types/estree": "^1.0.6" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1521,6 +2278,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1528,6 +2302,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1538,6 +2322,19 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -1584,6 +2381,38 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/obug": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", @@ -1598,6 +2427,102 @@ "node": ">=12.20.0" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1664,6 +2589,42 @@ "node": ">=20" } }, + "node_modules/polkadot-api": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-3.3.0.tgz", + "integrity": "sha512-GZqdSZ/6BqyJro7LFo4JTlyATHJLdoCRWCLagurgLzowZRWYX/rlCWN7y5oFBFrMKRdfeGy5sAJzK3+5nkOdDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.22.5", + "@polkadot-api/ink-contracts": "0.7.2", + "@polkadot-api/json-rpc-provider": "0.2.0", + "@polkadot-api/known-chains": "0.12.5", + "@polkadot-api/logs-provider": "0.2.0", + "@polkadot-api/metadata-builders": "0.15.2", + "@polkadot-api/metadata-compatibility": "0.7.2", + "@polkadot-api/observable-client": "0.19.4", + "@polkadot-api/pjs-signer": "0.9.0", + "@polkadot-api/raw-tx-creator": "0.5.0", + "@polkadot-api/signers-common": "0.4.0", + "@polkadot-api/sm-provider": "0.3.10", + "@polkadot-api/smoldot": "0.4.9", + "@polkadot-api/substrate-bindings": "0.21.2", + "@polkadot-api/substrate-client": "0.7.0", + "@polkadot-api/tx-creator": "0.2.0", + "@polkadot-api/utils": "0.4.0", + "@polkadot-api/ws-middleware": "0.4.2", + "@polkadot-api/ws-provider": "0.9.2", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.js", + "polkadot-api": "bin/cli.js" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, "node_modules/postcss": { "version": "8.5.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", @@ -1693,6 +2654,55 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", + "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.4.4", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.1.tgz", + "integrity": "sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -1707,6 +2717,33 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rollup": { "version": "4.64.12", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.12.tgz", @@ -1753,6 +2790,36 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-esbuild": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-6.2.1.tgz", + "integrity": "sha512-jTNOMGoMRhs0JuueJrJqbW8tOwxumaWYq+V5i+PD+8ecSCVkuX27tGW7BXqDgoULQ55rO7IdNxPcnsWtshz3AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "get-tsconfig": "^4.10.0", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "esbuild": ">=0.18.0", + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -1766,6 +2833,26 @@ "node": ">=6" } }, + "node_modules/scale-ts": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.1.tgz", + "integrity": "sha512-PBMc2AWc6wSEqJYBDPcyCLUj9/tMKnLX70jLOSndMtcUoLQucP/DM0vnQo1wJAYjTrQiq8iG9rD0q6wFzgjH7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.4.tgz", @@ -1780,6 +2867,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -1795,6 +2895,32 @@ "node": ">=18" } }, + "node_modules/smoldot": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-3.7.0.tgz", + "integrity": "sha512-49/jSn4hVysTu6SL8wTk6JRedG1GhxgDkxQ9hEC9gKMadZvWIoxgBVS2N8OQn4UTpWAd3hpifiBh8tRYPh8EkQ==", + "dev": true, + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/sort-keys": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz", + "integrity": "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map-js": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", @@ -1805,6 +2931,42 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -1819,6 +2981,65 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -1884,6 +3105,19 @@ "typescript": "^5.0.0 || ^6.0.0" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1955,6 +3189,42 @@ "node": ">=6" } }, + "node_modules/tsc-prog": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tsc-prog/-/tsc-prog-2.3.0.tgz", + "integrity": "sha512-ycET2d75EgcX7y8EmG4KiZkLAwUzbY4xRhA6NU0uVbHkY4ZjrAAuzTMxXI85kOwATqPnBI5C/7y7rlpY0xdqHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "typescript": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1976,6 +3246,54 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/uqr": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.3.tgz", + "integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vite": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", @@ -2167,6 +3485,22 @@ } } }, + "node_modules/which-command": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz", + "integrity": "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==", + "dev": true, + "license": "MIT", + "bin": { + "which-command": "cli.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/which-command?sponsor=1" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2184,6 +3518,175 @@ "node": ">=8" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-json-file": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-6.0.0.tgz", + "integrity": "sha512-MNHcU3f9WxnNyR6MxsYSj64Jz0+dwIpisWKWq9gqLj/GwmA9INg3BZ3vt70/HB3GEwrnDQWr4RPrywnhNzmUFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-indent": "^7.0.1", + "is-plain-obj": "^4.1.0", + "sort-keys": "^5.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/write-package/-/write-package-7.2.0.tgz", + "integrity": "sha512-uMQTubF/vcu+Wd0b5BGtDmiXePd/+44hUWQz2nZPbs92/BnxRo74tqs+hqDo12RLiEd+CXFKUwxvvIZvtt34Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge-ts": "^7.1.0", + "read-pkg": "^9.0.1", + "sort-keys": "^5.0.0", + "type-fest": "^4.23.0", + "write-json-file": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/write-package/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-package/node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/write-package/node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zimmerframe": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", diff --git a/web/package.json b/web/package.json index 81f494e..749ea07 100644 --- a/web/package.json +++ b/web/package.json @@ -19,9 +19,11 @@ "@sveltejs/kit": "^2.70.0", "@sveltejs/vite-plugin-svelte": "^6.0.0", "@types/node": "^22.20.5", + "polkadot-api": "3.3.0", "svelte": "^5.57.0", "svelte-check": "^4.0.0", "typescript": "^5.5.0", + "uqr": "0.1.3", "vite": "^7.0.0", "vitest": "^3.2.0" } diff --git a/web/src/lib/basket.ts b/web/src/lib/basket.ts new file mode 100644 index 0000000..cb37a23 --- /dev/null +++ b/web/src/lib/basket.ts @@ -0,0 +1,83 @@ +// The basket of a vote (Yvv, 2026-10-30: « Je peux voter tout d'un coup, ou une +// section entière, ou un article avec son titre, ou séparément »). One chooses +// article by article; « tout pour » or « tout contre » fills a section or the +// whole document — but never an article whose versions compete: there, one +// chooses one's version. Then one gesture sends it all: one write on the +// server, or one transaction on the Ğ1 chain. + +import type { Choice, Proposal, Unit } from './types'; + +/** A choice in the basket: for, against, or withdrawing one's vote. Absent: nothing. */ +export type Picks = Record; + +/** An article as the basket shows it. */ +export interface Slot { + unit: Unit; + /** The versions in competition: the text in force, the open texts and removals. */ + versions: Proposal[]; + /** Open changes of its inertia: another question, never filled by a grouped gesture. */ + inertia: Proposal[]; +} + +export interface Section { + /** The section of the seed (introduction, fondamental…), or null. */ + key: string | null; + slots: Slot[]; +} + +/** The articles of the document with something to vote, by section, in document order. */ +export function sections(units: Unit[]): Section[] { + const out: Section[] = []; + for (const unit of units) { + const open = unit.proposals.filter((p) => p.status === 'open'); + const versions = [ + ...(unit.inForce?.status === 'in_force' ? [unit.inForce] : []), + ...open.filter((p) => p.kind !== 'inertia'), + ]; + const inertia = open.filter((p) => p.kind === 'inertia'); + if (!versions.length && !inertia.length) continue; + const last = out.at(-1); + const slot = { unit, versions, inertia }; + if (last && last.key === unit.section) last.slots.push(slot); + else out.push({ key: unit.section, slots: [slot] }); + } + return out; +} + +/** « Tout pour » / « tout contre » over some articles: only those with a single version. */ +export function fill(slots: Slot[], choice: 'for' | 'against', picks: Picks): Picks { + const next = { ...picks }; + for (const s of slots) { + const [only] = s.versions; + if (only && s.versions.length === 1) next[only.id] = choice; + } + return next; +} + +/** Articles of several versions where no version is chosen yet: the interface shows them. */ +export function waiting(slots: Slot[], picks: Picks): Slot[] { + return slots.filter((s) => s.versions.length > 1 && !s.versions.some((p) => p.id in picks)); +} + +/** Empties the basket of these articles only. */ +export function clearScope(slots: Slot[], picks: Picks): Picks { + const ids = new Set(slots.flatMap((s) => [...s.versions, ...s.inertia].map((p) => p.id))); + return Object.fromEntries(Object.entries(picks).filter(([id]) => !ids.has(id))); +} + +/** What is sent: one line per version still in the document; withdrawing is a null choice. */ +export function lines(picks: Picks, known: Set): { proposalId: string; choice: 'for' | 'against' | null }[] { + return Object.entries(picks) + .filter(([id]) => known.has(id)) + .map(([proposalId, c]) => ({ proposalId, choice: c === 'none' ? null : c })); +} + +export function counts(picks: Picks): { for: number; against: number; none: number; total: number } { + const all = Object.values(picks); + return { + for: all.filter((c) => c === 'for').length, + against: all.filter((c) => c === 'against').length, + none: all.filter((c) => c === 'none').length, + total: all.length, + }; +} diff --git a/web/src/lib/components/BallotPanel.svelte b/web/src/lib/components/BallotPanel.svelte new file mode 100644 index 0000000..ed10023 --- /dev/null +++ b/web/src/lib/components/BallotPanel.svelte @@ -0,0 +1,306 @@ + + +{#snippet bulk(key: string | null, label: string)} +
+ + + +
+{/snippet} + +
+

{t('basket.title')}

+

{t('basket.intro')}

+ {#if access && !access.open} +

{t(access.reason === 'chain_off' ? 'vote.chain.off' : 'vote.members')}

+ {/if} + +
+ {t('basket.doc')} + {@render bulk('*', t('basket.doc'))} +
+ {#if left.length} +

{i18n.plural('basket.waiting', left.length)}

+ {/if} + +
+ {#each all as s, i (s.key ?? `none-${i}`)} + {@const label = sectionLabel(s.key)} +
+ {#if label} +
+

{label}

+ {@render bulk(s.key, label)} +
+ {/if} +
    + {#each s.slots as slot (slot.unit.id)} + {@const title = headline(slot)} + {@const several = slot.versions.length > 1} + {@const first = slot.versions[0] ?? slot.inertia[0]} +
  • + + {#if several}

    {t('basket.versions')}

    {/if} + {#each [...slot.versions, ...slot.inertia] as v (v.id)} +
    + + {labelOf(v, slot)} + + {t('tally.short', { for: v.tally.for, against: v.tally.against })} + {#if v.myVote && !v.myVoteStale} · {t('basket.mine', { choice: t(`vote.choice.${v.myVote}`) })}{/if} + + + basket.pick(v.id, c)} + /> +
    + {/each} +
  • + {/each} +
+
+ {/each} +
+ +
+

+ {#if tally.total}{i18n.plural('basket.count', tally.total)}{:else}{t('basket.empty')}{/if} +

+ +

+ {#if access?.way === 'chain' && access.chain}{t('basket.note.chain', { amount: g1Amount(access.chain.amount) })}{:else}{t('basket.note.server')}{/if} +

+ {#if error}{/if} +
+
+ + diff --git a/web/src/lib/components/ChainSign.svelte b/web/src/lib/components/ChainSign.svelte new file mode 100644 index 0000000..4db3321 --- /dev/null +++ b/web/src/lib/components/ChainSign.svelte @@ -0,0 +1,421 @@ + + + !closed && close()}> +

{t('sign.title')}

+

{i18n.plural('sign.summary', lines.length, { amount })}

+ +
+ {#if step.kind === 'start' || step.kind === 'opening'} +

{t('sign.opening')}

+ {:else if step.kind === 'none'} +

{t('sign.none')}

+ {:else if step.kind === 'wallets'} +

{t('sign.wallet')}

+
    + {#each step.list as w (w.id)} +
  • + {/each} +
+ {:else if step.kind === 'accounts'} + {@const wallet = step.wallet} +

{t('sign.account')}

+
    + {#each step.list as a (a.address)} +
  • + +
  • + {/each} +
+ {:else if step.kind === 'signing'} +

{t('sign.signing')}

+ {:else if step.kind === 'waiting'} +

{t('sign.waiting')}

+

{t('sign.later')}

+ {:else if step.kind === 'gecko' && ballot} +

{t('sign.gecko.scan')}

+
+ + + +
+
{t('sign.gecko.amount')}
+
{amount} Ğ1
+
{t('sign.gecko.comment')}
+
{ballot.comment}
+
+
+ {t('sign.gecko.open')} +

{t('sign.waiting')}

+

{t('sign.later')}

+ {:else if step.kind === 'confirmed'} +

{t('sign.confirmed')}

+ {#if step.voter}

{t('sign.confirmed.as', { name: step.voter })}

{/if} + {:else if step.kind === 'refused'} + + {:else if step.kind === 'expired'} + + {:else if step.kind === 'failed'} + + {/if} +
+ +
+ {#if geckoOffered} + + {/if} + {#if step.kind === 'failed' || step.kind === 'expired' || step.kind === 'refused'} + + {/if} + +
+
+ + diff --git a/web/src/lib/components/ChoicePicker.svelte b/web/src/lib/components/ChoicePicker.svelte new file mode 100644 index 0000000..f6d8971 --- /dev/null +++ b/web/src/lib/components/ChoicePicker.svelte @@ -0,0 +1,73 @@ + + + + + + {#if voted || value === 'none'} + + {/if} + + + diff --git a/web/src/lib/components/DeepList.svelte b/web/src/lib/components/DeepList.svelte index ea00f7a..ac68677 100644 --- a/web/src/lib/components/DeepList.svelte +++ b/web/src/lib/components/DeepList.svelte @@ -1,8 +1,9 @@ -