diff --git a/DESIGN.md b/DESIGN.md index b824eba..99a0ac3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -70,3 +70,7 @@ Native Windows Chrome/Edge at 100%, 125% and 150% display scaling and physical p The Vault title is “Always in stock”, without a full stop, eyebrow or subline; PageTitle omits an absent paragraph. Its heading occupies the former heading-and-sentence space, in one or two lines, with unchanged character speech and frame. Deposit and Redeem title geometry is unchanged. The Deposit aside shows both approved paragraphs directly; its separate stock list stays collapsed until pressed. The Docs first panel adds the four approved paragraphs without changing Numbers. The link preview is a 1200×630 JPEG of the blue framed panel, red Basket Protocol sign and Vault character, with only “ALWAYS IN STOCK” and the project name. Both copies and the exact share-tag shell are pinned by check-bundle; manifest art remains unchanged. + +## v13 presentation details + +On the Deposit page the list under the search, "All stocks you can deposit (N)", is shown open while the form is empty: with no wallet connected, or with a connected wallet whose read balances hold no listed open stock while no row is in the form. It is then a div in the fold's place with the same words as a plain bold 14px heading line (no underline, no marker), the same two sentences, and the same stock buttons in the existing auto-fill grid, which fills the whole panel width (as many even columns as fit, left-aligned, nothing centred; a long ticker still takes two columns and wraps inside its button). Once shown for an account it stays open for that account until the account changes or the page is reloaded, also after a press has added a row. With a wallet that holds listed stock the list is the closed fold as before. Nothing moves above the list; what follows it (the Preview deposit button and, below the panel, the "Before you deposit" box) moves down by the list's height. On phones the open list is the same grid with fewer columns. diff --git a/README.md b/README.md index 38cd09b..e177bfd 100644 --- a/README.md +++ b/README.md @@ -179,3 +179,16 @@ What the checks allow in this update: - check-bundle additionally allows styles, the exact share-tag shell and only share-card.jpg as a new public file. Existing public assets, configuration, packages, lockfile, pinned source and contracts remain protected. The delivery limit remains 8,388,608 bytes. - check-art retains manifest, pixel collision, title, strip and shelf assertions, adds the exact approved words and a portable widened 24/7 case, and checks the Vault heading and panel through 320–2560px. check-visibility compares v11 at 375×812, retaining header, strip and character geometry and documenting the original v12 removals and Docs displacement together with the later approved issue-only Vault/Deposit status, Closed sign and removal of both wallet instruction lines. Final measurements are recorded in the build evidence. - check-fixes retains the pending-transaction tests and exercises Claims blank/quiet/full states, failed reads, retry, retained results, refresh, hidden navigation and account resets. The fork keeps all 74 earlier check positions and the same 70 page sends. Two status-result names change. Of 149 inherited assertion calls, 140 remain byte-identical and ordered; nine status-presentation assertions are replaced explicitly to check no normal notice, the Closed sign and exact short sentences, and absence of the removed label/hours detail. All transaction and on-chain Hours/Dst checks remain. The v12 cases also check the compact Vault notice and both removed wallet instruction lines, along with text/layout, served share image, same-nonce deposit repricing, widened label and late Connect. It expects 84 checks, 70 page sends and 24 screenshots; generated artifacts record actual results. The 24 screenshots use JPEG quality 26 (32 in v11); final evidence records the complete delivery budget and the next-higher-quality comparison. + +## Site update v13 + +The current patch is applied to v12, commit 586998cf5eb2a6ccce05615ddf7f44874813042c. Two owner decisions of 2026-10-13: +- Deposit: while the form is empty (no wallet connected, or a connected wallet whose read balances hold no listed open stock while no row is in the form), the list "All stocks you can deposit (N)" is shown open across the panel: a div with the fold's words as a plain heading line, the same two sentences and the same stock buttons in the existing grid, which now has the whole panel width to fill. Once shown for an account it stays open for that account until the account changes or the page is reloaded, also after a press has added a row. A wallet that holds listed stock sees the closed fold as before; the rows, the search box, the buttons and every word are unchanged. +- Redeem: the result line after a confirmed Claim all (and after a redeem that left stock owed) reads "Still owed to : " and no longer counts the stocks that are not owed; the unused count is removed from the tail of send(). +- No transaction input, send preparation, chain read, wallet request, storage key or application network call changes. send() through its receipt wait is byte-identical to v12; its tail has only the one pinned detail replacement. Every other web/src file equals v12. + +What the checks allow in this update: + +- check-calldata permits exactly Flows and wallet (styles through check-bundle); Vault, components, Docs and main are frozen again. The two files restore v12 through documented pieces, and independently: the reviewed source digests, the ordered added-call pins (four display-only calls in DepositPage: the new state, the emptiness test, the effect and its setter), the single removed count call, the byte-identical send head, the one tail piece, the ordered mined-hash state writes, the connect/disconnect texts, the counter uses, the hook return, the raw protected blocks (approveAll, every onSubmit, getSpec and transaction button, the Claim all handler, the owed read, the Deposit effects) and the differential encoding (496 identical, 1,773 refused) stay. +- check-bundle's source scope is Flows, wallet and styles; the shell, both share JPEG copies, public files, configuration, packages, lockfile, pinned source and contracts remain protected. The delivery limit remains 8,388,608 bytes. +- check-art keeps the fixed words ("All stocks you can deposit" and "Press a stock to add it to your deposit." among them) and every title, strip and shelf assertion. check-visibility compares v12 at 375×812: every v12 state must equal the baseline, and the only named exception is the Deposit list (heading line in the fold line's place, exact displacement by the open list's height); in both Deposit variants it asserts the open list, its words, its start and that it spans the panel. check-fixes is unchanged (34 regressions). The fork keeps all 84 v12 check positions and the same 70 page sends and adds two checks: the open list in both empty states with a press that keeps it open and the owner's wallet keeping the closed fold, and the Claim all result line. diff --git a/web/scripts/check-bundle.mjs b/web/scripts/check-bundle.mjs index 7ab49f7..1774646 100644 --- a/web/scripts/check-bundle.mjs +++ b/web/scripts/check-bundle.mjs @@ -6,9 +6,9 @@ import {createHash} from 'node:crypto'; const root=process.env.BASKET_SOURCE_ROOT||path.resolve('..'); const git=args=>execFileSync('git',args,{cwd:root,encoding:'utf8'}); const changed=git(['diff','--name-only','HEAD']).trim().split('\n').filter(Boolean); -// v12 SPEC sections 1-6 and the approved persistent Claims mount: only these sources may change. +// v13 (owner decisions of 2026-10-13): only these sources may change. // check-calldata independently pins each reviewed source and every protected transaction block. -const v12Source=['Vault.tsx','components.tsx','Docs.tsx','Flows.tsx','wallet.tsx','main.tsx','styles.css'].map(f=>'web/src/'+f); +const v13Source=['Flows.tsx','wallet.tsx','styles.css'].map(f=>'web/src/'+f); const SHELL_SHA256='82f66899a0c19f61299254c0005ee5117af0a912b97e8e57d249288e9dbc962d'; const SHARE_SHA256='dc14dd6f4068ec9325426583104b1c6ae8748fbb17d8326863c9612280eacd85'; // The new favicon: red panel, yellow frame, the header's basket in yellow. @@ -22,7 +22,7 @@ for(const p of changed){ // v12 section 5: the exact page shell is pinned below; build configuration and validation fixtures stay unchanged. assert.ok(!['web/vite.config.ts','web/tsconfig.json'].includes(p)&&(!p.startsWith('web/validation/')||p==='web/validation/README.md'),'Page shell, build configuration or validation fixture changed: '+p); assert.ok(!/(^|\/)(lib|node_modules|\.github|\.git)(\/|$)|(^|\/)\.env(?:\.|$)|(^|\/)(foundry\.toml|foundry\.lock|remappings\.txt|\.gitmodules|package\.json|[^/]*lock[^/]*)$/.test(p),'Protected path changed: '+p); - if(p.startsWith('web/src/'))assert.ok(v12Source.includes(p),'Source outside the v12 scope changed: '+p); + if(p.startsWith('web/src/'))assert.ok(v13Source.includes(p),'Source outside the v13 scope changed: '+p); assert.ok(!p.startsWith('src/'),'Root contract source changed'); } // v12 section 5: only the pinned share image may be new, whether untracked or tracked. @@ -48,7 +48,7 @@ for(const p of files) assert.ok(!/(^|\/)(node_modules|\.npm|\.cache|\.vite|\.pla const shots=files.filter(p=>/^artifacts\/(empty|stocks25)-.*\.jpg$/.test(p));assert.equal(shots.length,24); const extraImages=files.filter(p=>/^artifacts\/.*\.(png|jpg|webp|svg)$/.test(p)&&!shots.includes(p)&&!/^artifacts\/redeem-title-dpr-(1|1\.25|1\.5)\.png$/.test(p));assert.deepEqual(extraImages,[]); const productionFiles={};for(const p of files.filter(p=>p.startsWith('dist/'))){const b=fs.readFileSync(path.join(root,p));productionFiles[p]={bytes:b.length,sha256:createHash('sha256').update(b).digest('hex')};} -const result={limitBytes:8388608,uncompressedDeliveryBytes:0,fileCount:files.length,productionExportBytes:Object.values(productionFiles).reduce((a,b)=>a+b.bytes,0),productionFiles,accounting:'Complete candidate file contents including tracked lib sources and this report. Excludes Git metadata, removed task inputs and disposable test/scratch. No Git metadata was modified.',checks:['Protected configuration, dependencies, root contracts, web/pinned, build configuration and validation fixtures unchanged; v12 source scope independently pinned; exact share shell and both share JPEG copies pinned; both favicon copies unchanged','No git submodules or nested dependency/cache/archive payloads','24 required empty/25-stock JPEGs retained (quality 26 in v12, 32 in v11, 36 in v9/v10, 40 before); no additional artifact images','Both copies of all 37 manifest assets included']}; +const result={limitBytes:8388608,uncompressedDeliveryBytes:0,fileCount:files.length,productionExportBytes:Object.values(productionFiles).reduce((a,b)=>a+b.bytes,0),productionFiles,accounting:'Complete candidate file contents including tracked lib sources and this report. Excludes Git metadata, removed task inputs and disposable test/scratch. No Git metadata was modified.',checks:['Protected configuration, dependencies, root contracts, web/pinned, build configuration and validation fixtures unchanged; v13 source scope independently pinned; exact share shell and both share JPEG copies pinned; both favicon copies unchanged','No git submodules or nested dependency/cache/archive payloads','24 required empty/25-stock JPEGs retained (quality 26 in v12, 32 in v11, 36 in v9/v10, 40 before); no additional artifact images','Both copies of all 37 manifest assets included']}; const report=path.join(root,'artifacts/bundle-check.json'); for(let i=0;i<4;i++){fs.writeFileSync(report,JSON.stringify(result,null,2)+'\n');result.uncompressedDeliveryBytes=files.reduce((sum,p)=>sum+fs.statSync(path.join(root,p)).size,0);} fs.writeFileSync(report,JSON.stringify(result,null,2)+'\n');assert.ok(result.uncompressedDeliveryBytes<=result.limitBytes,"Over the 8 MiB budget (if the 24 screenshots are still an older set, run scripts/run-browser.mjs first: it rewrites them at quality 26): "+JSON.stringify(result));console.log('PASS complete bundle',result.uncompressedDeliveryBytes,'/',result.limitBytes,'bytes; lib included'); diff --git a/web/scripts/check-calldata.mjs b/web/scripts/check-calldata.mjs index 1799219..83f4f34 100644 --- a/web/scripts/check-calldata.mjs +++ b/web/scripts/check-calldata.mjs @@ -11,12 +11,18 @@ const git=args=>execFileSync('git',args,{cwd:root,encoding:'utf8'}); const baseline=p=>git(['show','HEAD:'+p]); const read=p=>fs.readFileSync(path.join(root,p),'utf8'); const sha256=s=>createHash('sha256').update(s).digest('hex'); -// v12 on HEAD=v11: the approved presentation changes, mined-receipt hash and connection intent guard. -// Transactions and all chain reads remain HEAD's. main keeps Claims mounted by owner-approved Activity. +// v13 on HEAD=v12: two owner decisions of 2026-10-13. Flows.tsx: the Deposit stock list is shown open across the +// panel while the form is empty (no wallet, or a wallet without a listed stock) and then for that account. +// wallet.tsx: the Claim all result detail loses its "Nothing owed for N other stocks." sentence. Transactions +// and all chain reads remain HEAD's; every other web/src file is byte-identical to HEAD. const protectedFiles=[ + "Docs.tsx", "Losses.tsx", "Owner.tsx", "Scenery.tsx", + "Vault.tsx", + "components.tsx", + "main.tsx", "chain.ts", "deployment.ts", "governance.ts", @@ -26,146 +32,34 @@ const protectedFiles=[ "vault.abi.json" ]; for(const f of protectedFiles) assert.equal(read('web/src/'+f),baseline('web/src/'+f),f); -// The v12 files: byte-identical to HEAD once their documented pieces [v12, HEAD] are put back (each exactly once), so +// The v13 files: byte-identical to HEAD once their documented pieces [v13, HEAD] are put back (each exactly once), so // every word, call and attribute in them is pinned. The list between the two marker lines was generated from the // difference to HEAD; the pins further down do not depend on it: they compare with HEAD itself or with texts written // out in this file, so a piece cannot carry a change to a transaction, a wallet request or a chain read past them. -// >>> v12 pieces (generated from the difference to HEAD, not edited by hand) +// >>> v13 pieces (generated from the difference to HEAD, not edited by hand) const editedFiles={ - "Vault.tsx": [ - [ - " return now;\n}\n// Owner-approved display: no reads, requests or transaction state.\nconst DEPOSIT_LETTERS: Record = {\n O: [\"01110\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"01110\"],\n E: [\"11111\", \"10000\", \"10000\", \"11110\", \"10000\", \"10000\", \"11111\"],\n C: [\"01111\", \"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"01111\"],\n L: [\"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"11111\"],\n S: [\"01111\", \"10000\", \"10000\", \"01110\", \"00001\", \"00001\", \"11110\"],\n D: [\"11110\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"11110\"],\n};\n\nfunction DepositLedSign() {\n const label = \"Closed\";\n const word = label.toUpperCase();\n const step = 4.8;\n const start = (220 - ((word.length * 6 - 2) * step)) / 2;\n return (\n \n \n \n \n \n {[...word].flatMap((letter, index) => DEPOSIT_LETTERS[letter].flatMap((row, y) => [...row].map((on, x) => (\n \n ))))}\n \n \n );\n}\n\nexport function DepositState({ snapshot: s, showSign = false }: { snapshot: Snapshot; showSign?: boolean }) {\n const r = s.status;\n const reason = r?.ok && !s.loading ? Number(r.value[0]) : undefined;\n", - " return now;\n}\nexport function DepositState({ snapshot: s }: { snapshot: Snapshot }) {\n const r = s.status;\n const reason = r?.ok && !s.loading ? Number(r.value[0]) : undefined;\n" - ], - [ - " ? closedWords(reason, s.globals.settings, s.blockTime, now)\n : undefined;\n // The owner wants a notice only for closed deposits or an unreadable status.\n const statusUnreadable =\n !s.complete || !r?.ok || s.globals.genesisFinalized === undefined;\n const operational =\n !statusUnreadable && reason === 0 && s.globals.genesisFinalized === true &&\n s.assets.some((a) => a.open && !a.retired);\n if (s.loading || operational || closed === mayBeOpen) return null;\n const issueReason = s.globals.genesisFinalized === false ? 1\n : !s.assets.length ? 4\n : s.assets.every((a) => !a.open || a.retired) ? 5\n : reason === 13 ? 9 : reason;\n const issue = reasonWords(issueReason, s.globals.settings).split(\" (\")[0];\n const weekendHours = reason === 3 &&\n s.globals.settings?.hoursFrom === 72000n && s.globals.settings?.hoursTo === 504000n;\n return (\n
\n {statusUnreadable ? (\n <>\n {!showSign && Vault status:}\n

unreadable — Retry vault

\n \n \n ) : showSign ? (\n <>\n \n

{weekendHours\n ? \"Deposits reopen when the stock feed prices update around 8pm on Sundays.\"\n : `Deposits will be back open when “${issue}” is resolved.`}

\n \n ) : (\n <>Vault status:

Closed

\n )}\n
\n", - " ? closedWords(reason, s.globals.settings, s.blockTime, now)\n : undefined;\n return (\n
\n Vault deposit status\n

\n {s.loading\n ? \"Reading...\"\n : !s.assets.length &&\n s.complete &&\n s.globals.genesisFinalized === false\n ? \"No Stock Tokens are listed yet. Deposits start after the owner finishes setup.\"\n : s.assets.length > 0 &&\n s.assets.every((a) => !a.open || a.retired) &&\n s.globals.genesisFinalized\n ? \"Every stock is closed. The owner must reopen a stock before deposits.\"\n : !r || !r.ok\n ? \"unreadable — Retry vault\"\n : reasonWords(Number(r.value[0]), s.globals.settings)}\n

\n {r?.ok && r.value[1] !== zeroAddress && (\n

\n Stock at fault:{\" \"}\n {s.assets.find(\n (a) => a.token.toLowerCase() === r.value[1].toLowerCase(),\n )?.symbol ?? r.value[1]}\n

\n )}\n {closed &&

{closed}

}\n

Hours: {s.loading ? \"Reading...\" : hoursLine(s.globals.settings)}

\n {!s.loading && (!r || !r.ok) && (\n \n )}\n
\n" - ], - [ - " <>\n \n \n
\n
\n", - " line: \"I was hoping you would come through my aisle\",\n }}\n >\n Meet Basket (BASK): your share of the Stock Tokens in this vault.\n \n
\n
\n" - ], - [ - "
\n
\n Maximum Basket Capacity(eligible to be raised)\n {s.loading ? \"Reading...\" : usd(s.globals.NAV_CAP)}\n
\n", - "
\n
\n Size limit\n {s.loading ? \"Reading...\" : usd(s.globals.NAV_CAP)}\n
\n" - ], - [ - " )}\n
\n
\n \n
\n
\n Fees\n", - " )}\n
\n \n
\n Fees\n" - ] - ], - "components.tsx": [ - [ - " eyebrow?: string;\n title: string;\n children?: ReactNode;\n character?: {\n kind: \"vault\" | \"deposit\" | \"redeem\";\n", - " eyebrow?: string;\n title: string;\n children: ReactNode;\n character?: {\n kind: \"vault\" | \"deposit\" | \"redeem\";\n" - ], - [ - " {eyebrow &&

{eyebrow}

}\n

{title}

\n {children != null &&

{children}

}\n {character && (\n
\n", - " {eyebrow &&

{eyebrow}

}\n

{title}

\n

{children}

\n {character && (\n
\n" - ] - ], - "Docs.tsx": [ - [ - " vault.\n

\n

\n The basket has no set weights. Any listed stock can be deposited, in any amount, as long as the vault’s checks pass and the vault stays under its size limit. So the mix is whatever holders have put in, and it changes with every deposit and redemption.\n

\n

\n BASK’s backing moves with every stock in the vault, so its market price can drift from the value behind it. Traders close that gap: they deposit stocks and sell BASK when it trades high, or buy BASK and redeem when it trades low. That keeps BASK near its backing.\n

\n

\n The 0.5% fee on each deposit and redemption is set to go to ITEM staking once ITEM is live, so more trading through the vault means more fees for ITEM.\n

\n

\n ITEM is not live yet. Its address will be listed here when it is.\n

\n
\n
\n", - " vault.\n

\n
\n
\n" - ] - ], - "Flows.tsx": [ - [ - " Receiver,\n} from \"./components\";\nimport { DepositState } from \"./Vault\";\nimport type { Wallet } from \"./wallet\";\ntype Props = { snapshot: Snapshot; wallet: Wallet };\n", - " Receiver,\n} from \"./components\";\nimport { DepositState, hoursLine } from \"./Vault\";\nimport type { Wallet } from \"./wallet\";\ntype Props = { snapshot: Snapshot; wallet: Wallet };\n" - ], - [ - " // From the last finished vault read, kept while a new read runs so the form\n // does not jump; undefined before the first read or when unreadable.\n // Dollar value of n units at the feed price of the last vault read, 18\n // decimals; undefined when that price is unreadable.\n", - " // From the last finished vault read, kept while a new read runs so the form\n // does not jump; undefined before the first read or when unreadable.\n const r = s.status;\n const reason = r?.ok ? Number(r.value[0]) : undefined;\n // Dollar value of n units at the feed price of the last vault read, 18\n // decimals; undefined when that price is unreadable.\n" - ], - [ - "
\n \n \n

\n Choose amounts, preview, approve each stock, press Deposit.\n", - "

\n \n {/* One line while the vault takes deposits; otherwise, and while the\n status is unread or unreadable, the full box with its reason. */}\n {reason === 0 && open.length > 0 ? (\n

\n Deposits are open. Hours:{\" \"}\n {hoursLine(s.globals.settings)}\n

\n ) : (\n \n )}\n

\n Choose amounts, preview, approve each stock, press Deposit.\n" - ], - [ - " <>\n {!ready &&

Reading your wallet...

}\n {unread ? (\n

\n", - " <>\n {!ready &&

Reading your wallet...

}\n

\n Showing {w.account.slice(0, 6)}…{w.account.slice(-4)}. To\n use another wallet, switch the address in your wallet app:\n this page follows it.\n

\n {unread ? (\n

\n" - ], - [ - " subtracts this lock and any fee.\n

\n

\n You approve each stock for exactly the amount you deposit. Each approval is its own wallet prompt; the deposit prompt comes last.\n

\n

\n A prepared deposit must be sent within ten minutes. The BASK goes to your connected wallet.\n

\n \n Feeds lag the market. Issuer restrictions and pool checks can stop\n", - " subtracts this lock and any fee.\n

\n
\n Transaction details\n

\n Each short allowance is set to exactly that stock’s amount.\n Approvals are separate wallet prompts; the deposit follows after\n simulation.\n

\n

\n The deadline is ten minutes from preparation. Your wallet receives\n the BASK.\n

\n
\n \n Feeds lag the market. Issuer restrictions and pool checks can stop\n" - ], - [ - " Receive your share of every stock.\n \n
\n
\n \n", - " Receive your share of every stock.\n \n
\n
\n \n" - ], - [ - "

\n
\n
\n \n", - "

\n
\n \n
\n \n" - ], - [ - " (a) => owed[a.token] !== undefined && owed[a.token]! > 0n,\n );\n const visible = [\n ...claimable,\n ...s.assets.filter((a) => owed[a.token] === undefined),\n ];\n // Presentation only: keep the last finished visibility through refreshes.\n // Capture old account values so neither its owed result nor its status can\n // reveal the new account's panel before that account's first read finishes.\n const visit = useRef({ account: w.account, initialOwed: owed, progress: w.progress, error });\n const readStart = useRef(owed);\n useLayoutEffect(() => {\n readStart.current = owed;\n }, [w.account, s.loadedAt, retry]);\n const [display, setDisplay] = useState({ account: w.account, finished: false, shown: false, items: [] as Asset[], count: 0 });\n useLayoutEffect(() => {\n if (visit.current.account !== w.account) {\n visit.current = { account: w.account, initialOwed: owed, progress: w.progress, error };\n setDisplay({ account: w.account, finished: false, shown: false, items: [], count: 0 });\n return;\n }\n if (!w.account) return;\n const finished = !reading && !s.loading && owed !== readStart.current;\n const status = Object.keys(w.progress).some(\n (key) => (key === \"claim-all\" || key.startsWith(\"Claim \")) &&\n w.progress[key] !== visit.current.progress[key],\n );\n const shown = working || (error !== \"\" && error !== visit.current.error) || status ||\n (finished && (!s.complete || visible.length > 0));\n if (finished || shown)\n setDisplay((old) => ({\n account: w.account, finished: old.finished || finished, shown: old.shown || shown,\n items: finished ? visible : old.items, count: finished ? claimable.length : old.count,\n }));\n }, [w.account, owed, reading, s.loading, s.complete, working, error, w.progress]);\n if (!w.account)\n return

Connect the receiver wallet to read and claim its owed stocks.

;\n if (display.account !== w.account || !display.shown)\n return display.account === w.account && display.finished\n ?

Nothing is owed to this wallet.

\n : null;\n return (\n
\n", - " (a) => owed[a.token] !== undefined && owed[a.token]! > 0n,\n );\n const zeros = s.assets.filter((a) => owed[a.token] === 0n);\n const visible = [\n ...claimable,\n ...s.assets.filter((a) => owed[a.token] === undefined),\n ];\n return (\n
\n" - ], - [ - " \n
\n {display.items.map((a) => (\n
\n {a.symbol}\n", - " \n
\n {visible.map((a) => (\n
\n {a.symbol}\n" - ], - [ - " ))}\n
\n {!s.assets.length && (\n

\n", - " ))}\n

\n {zeros.length > 0 && (\n
\n Nothing owed for {zeros.length} other stocks\n

{zeros.map((a) => a.symbol).join(\", \")}

\n
\n )}\n {!s.assets.length && (\n

\n" - ], - [ - "

\n )}\n {display.count > 1 && (\n

\n Claim all collects up to 10 stocks per wallet prompt. If one stock can’t be paid, that batch stays owed: collect the others with their own Claim buttons.\n

\n )}\n \n )}\n", - "

\n )}\n

\n Batches of up to 10 stocks, with one wallet prompt per batch. If one\n stock’s payment fails, the vault reverts that whole batch: nothing is\n lost and every stock in it stays owed. Use each stock’s own Claim\n button to collect the others.\n

\n \n )}\n" - ] + "Flows.tsx":[ + [" const [showSmall, setShowSmall] = useState(false);\n // v13: the account (\"\" for none) for which the stock list under the search\n // was shown open because the form was empty; it stays open for it.\n const [listOpenFor, setListOpenFor] = useState();\n // The stock an Add button just added, until its row is on screen.\n", + " const [showSmall, setShowSmall] = useState(false);\n // The stock an Add button just added, until its row is on screen.\n"], + [" });\n // v13: the form is empty with no wallet, or with a wallet whose balances\n // are read and hold no listed stock while no row is in the form. Then the\n // stock list is shown open across the panel, and it stays open for that\n // account until the account changes or the page is reloaded.\n const emptyForm =\n !w.account ||\n (ready && !unread && !rows.length && !stocks.some((x) => x.inWallet));\n const accountKey = w.account ?? \"\";\n useEffect(() => {\n if (emptyForm) setListOpenFor(accountKey);\n }, [emptyForm, accountKey]);\n const listOpen = emptyForm || listOpenFor === accountKey;\n const StockList = listOpen ? \"div\" : \"details\";\n // `focus` is false for Enter in the search box, which keeps the focus.\n", + " });\n // `focus` is false for Enter in the search box, which keeps the focus.\n"], + ["
\n {/* Every stock the vault takes now. Closed until pressed while\n the form has rows; shown open across the panel while the\n form is empty (no wallet, or a wallet without a listed\n stock), and then for the rest of the visit for that account\n (v13). A press is the same Add as in the search; a stock\n that has a row stays in the list, switched off. */}\n \n {listOpen ? (\n

\n All stocks you can deposit ({open.length})\n

\n ) : (\n \n All stocks you can deposit ({open.length})\n \n )}\n

Press a stock to add it to your deposit.

\n", + "
\n {/* Every stock the vault takes now, closed until pressed. A\n press is the same Add as in the search; a stock that has a\n row stays in the list, switched off. */}\n
\n All stocks you can deposit ({open.length})\n

Press a stock to add it to your deposit.

\n"], + [" \n \n {counted + badAmount.length + noPrice.length > 0 && (\n", + " \n
\n {counted + badAmount.length + noPrice.length > 0 && (\n"] ], - "wallet.tsx": [ - [ - " const disconnected = useRef(savedDisconnected());\n const accountVersion = useRef(0);\n // Advanced by Connect wallet and Disconnect only: a Connect answer that\n // arrives after a later Disconnect is dropped.\n const connectVersion = useRef(0);\n const [connecting, setConnecting] = useState(false);\n const [progress, setProgress] = useState>({});\n", - " const disconnected = useRef(savedDisconnected());\n const accountVersion = useRef(0);\n const [connecting, setConnecting] = useState(false);\n const [progress, setProgress] = useState>({});\n" - ], - [ - " connectingLock.current = true;\n accountVersion.current++;\n const version = ++connectVersion.current;\n setConnecting(true);\n try {\n", - " connectingLock.current = true;\n accountVersion.current++;\n setConnecting(true);\n try {\n" - ], - [ - " method: \"eth_requestAccounts\",\n });\n // Disconnect was pressed while the wallet was asked: it stands.\n if (version !== connectVersion.current) return;\n if (a[0]) {\n disconnected.current = false;\n", - " method: \"eth_requestAccounts\",\n });\n if (a[0]) {\n disconnected.current = false;\n" - ], - [ - " if (sending.current) return;\n accountVersion.current++;\n connectVersion.current++;\n disconnected.current = true;\n try {\n", - " if (sending.current) return;\n accountVersion.current++;\n disconnected.current = true;\n try {\n" - ], - [ - " : \"The transaction was replaced in the wallet by a different one; it did not run. Refresh the vault before retrying.\",\n );\n // After a Speed up the transaction is mined under another hash than the\n // one the wallet returned: from here on the mined one is shown and read.\n const mined = receipt.transactionHash;\n sent = mined;\n setHash(mined);\n if (receipt.status !== \"success\")\n throw new Error(\n \"The transaction reverted. Refresh the data before retrying.\",\n );\n setConfirmed({ hash: mined, functionName: s.functionName });\n report(key, { message: `${label}: Confirmed. Refreshing…`, hash: mined });\n let detail = \"\";\n try {\n", - " : \"The transaction was replaced in the wallet by a different one; it did not run. Refresh the vault before retrying.\",\n );\n if (receipt.status !== \"success\")\n throw new Error(\n \"The transaction reverted. Refresh the data before retrying.\",\n );\n setConfirmed({ hash: tx, functionName: s.functionName });\n report(key, { message: `${label}: Confirmed. Refreshing…`, hash: tx });\n let detail = \"\";\n try {\n" - ], - [ - " \"Refresh unreadable. Retry vault / claims to read the confirmed result.\";\n }\n report(key, { message: `${label}: Confirmed.`, hash: mined, detail });\n return receipt;\n } catch (e) {\n", - " \"Refresh unreadable. Retry vault / claims to read the confirmed result.\";\n }\n report(key, { message: `${label}: Confirmed.`, hash: tx, detail });\n return receipt;\n } catch (e) {\n" - ] - ], - "main.tsx": [ - [ - "import { Activity, useCallback, useEffect, useRef, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { chain, verifyNetwork, VAULT } from \"./chain\";\n", - "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { chain, verifyNetwork, VAULT } from \"./chain\";\n" - ], - [ - " \n )}\n \n \n \n \n
\n", - " \n )}\n {page === \"Redeem\" && }\n \n
\n" - ] + "wallet.tsx":[ + [" });\n detail =\n", + " });\n const zeros = values.filter((v) => v.ok && v.value === 0n).length;\n detail =\n"], + [" (fresh.complete\n ? remaining.join(\"; \") || \"Nothing owed.\"\n : \"Stock list unreadable — Retry vault.\");\n", + " (fresh.complete\n ? (remaining.join(\"; \") || \"Nothing owed.\") +\n (zeros ? ` Nothing owed for ${zeros} other stocks.` : \"\")\n : \"Stock list unreadable — Retry vault.\");\n"] ] }; -// <<< v12 pieces +// <<< v13 pieces assert.deepEqual(Object.keys(editedFiles),[ - "Vault.tsx", - "components.tsx", - "Docs.tsx", "Flows.tsx", - "wallet.tsx", - "main.tsx" -],'the v12 files'); + "wallet.tsx" +],'the v13 files'); const putBack=(text,pieces,what)=>{for(const [n,o] of pieces){assert.equal(text.split(n).length,2,what+': expected once: '+n);text=text.split(n).join(o);}return text;}; for(const [f,edits] of Object.entries(editedFiles)) assert.equal(putBack(read('web/src/'+f),edits,f),baseline('web/src/'+f),f+' changed beyond the documented pieces'); // Calls that build, encode, simulate or send a transaction (or parse its arguments) or read the chain. @@ -219,21 +113,8 @@ function difference(a,b){ } // Guarded calls: the exact, documented differences from HEAD, each added call with the place it must sit in // (everything equals HEAD, in order and place). The existing receipt read and display-only amount parser are -// already part of HEAD. The v12 connection guard adds no guarded call or wallet request. -// SPEC 1-6 add no guarded call: receipt reading and display amount parsing already belong to HEAD. +// already part of HEAD. v13 adds no guarded call, wallet request or chain read. const expected={ - "Vault.tsx": { - "removed": [], - "added": [] - }, - "components.tsx": { - "removed": [], - "added": [] - }, - "Docs.tsx": { - "removed": [], - "added": [] - }, "Flows.tsx": { "removed": [], "added": [] @@ -241,30 +122,17 @@ const expected={ "wallet.tsx": { "removed": [], "added": [] - }, - "main.tsx": { - "removed": [], - "added": [] } }; // Action attributes: the exact differences from HEAD, in page order, each [element, attribute, why]. Everything else // equals HEAD in order, text for text. None of them builds or starts a transaction (no onSubmit, getSpec, build or // claim) and none contains a guarded call (both asserted below). The list between the marker lines was generated // from the difference to HEAD, as the pieces were; each why was written by hand, and an entry without one fails. -// >>> v12 attributes (generated from the difference to HEAD; each why written by hand) +// >>> v13 attributes (generated from the difference to HEAD; each why written by hand) const attributeChanges={ - "Vault.tsx": { - "removed": [], - "added": [ - [ - "svg", - "role=\"img\"", - "The owner-approved Deposit LED sign is an inline SVG image with an accessible Closed label, shown only for a known closure using the existing snapshot status. This role is presentation only and performs no action or read." - ] - ] - } + }; -// <<< v12 attributes +// <<< v13 attributes // Every call expression: HEAD's calls, in order and place, are all still there, except the removed ones listed here; // a call that only gained the documented piece inside it (put back before comparing) counts as kept. Every added // call, including display helpers and effects, is independently pinned below, not only the guarded calls. @@ -276,71 +144,20 @@ const attributeChanges={ // read is added; the existing receipt read and answer handling are independently byte-pinned. // The guarded startup eth_accounts answer is part of HEAD and must remain exact. const restoreAccountReply=(x)=>x.text; -// Reviewed v12 differences in calls and watched-name uses. Independent of the generated pieces. +// Reviewed v13 differences in calls and watched-name uses. Independent of the generated pieces. const callChanges={ - "Vault.tsx": { - "why": "SPEC 1 and owner-approved short closure notice: the heading and removed sentence, compact Closed status on Vault, and Closed LED plus one sentence on Deposit. The exact local display tail uses the existing snapshot and settings; original r/reason/clock/closed calculations remain byte-identical. Normal, loading and reopening-boundary states show no notice; unreadable status retains Retry.", - "pieces": [], - "removed": [ - "reasonWords(Number(r.value[0]), s.globals.settings)", - "Number(r.value[0])", - "s.assets.find((a) => a.token.toLowerCase() === r.value[1].toLowerCase())", - "a.token.toLowerCase()", - "r.value[1].toLowerCase()", - "hoursLine(s.globals.settings)" - ], - "uses": [] - }, - "components.tsx": { - "why": "SPEC 1: PageTitle renders no empty paragraph when there are no children.", - "pieces": [], - "removed": [], - "uses": [] - }, - "Docs.tsx": { - "why": "SPEC 2: four approved paragraphs in the existing panel.", - "pieces": [], - "removed": [], - "uses": [] - }, "Flows.tsx": { - "why": "SPEC 3/4 and owner-approved presentation: direct Deposit paragraphs, the short DepositState sign notice, one-column Redeem, removed redundant wallet instruction lines and Claims presentation. The unused hoursLine import, display calls and r/reason declarations are removed; all transaction builders and reads stay byte-identical.", + "why": "v13 (owner decision 2026-10-13): while the Deposit form is empty (no wallet, or a wallet whose read balances hold no listed stock and no row) the stock list under the search is shown open across the panel, in a div with a plain heading line instead of the details fold, and it stays open for that account. One new local state (the account the list opened for), one display-only effect that sets it, and the display-only emptiness test; no call of HEAD is removed, no read, send, storage or network use is added.", "pieces": [], - "removed": [ - "Number(r.value[0])", - "hoursLine(s.globals.settings)", - "w.account.slice(0, 6)", - "w.account.slice(-4)", - "s.assets.filter((a) => owed[a.token] === 0n)", - "visible.map((a) => (
{a.symbol}

Still owed:{\" \"} {reading ? \"Reading...\" : fmt(owed[a.token], a.tokenDecimals)}

vault(\"claim\", [[a.token], recipient(to)])}/>
))", - "zeros.map((a) => a.symbol).join(\", \")", - "zeros.map((a) => a.symbol)" - ], + "removed": [], "uses": [] }, "wallet.tsx": { - "why": "SPEC 6: a separate connection intent counter and mined-receipt hash propagation after the protected send head.", + "why": "v13 (owner decision 2026-10-13): the Claim all result detail no longer counts the stocks that are not owed; the unused zeros count (a filter over the already-read owed values) is removed. The send head, every state write and every request stay HEAD's.", "pieces": [], "removed": [ - "setConfirmed({ hash: tx, functionName: s.functionName })", - "report(key, { message: `${label}: Confirmed. Refreshing…`, hash: tx })", - "report(key, { message: `${label}: Confirmed.`, hash: tx, detail })" + "values.filter((v) => v.ok && v.value === 0n)" ], - "uses": [ - [ - "setHash(mined)", - "useWallet > send" - ], - [ - "setConfirmed({ hash: mined, functionName: s.functionName })", - "useWallet > send" - ] - ] - }, - "main.tsx": { - "why": "Owner-approved persistence: React Activity keeps Claims mounted across navigation and pauses its effects while hidden.", - "pieces": [], - "removed": [], "uses": [] } }; @@ -393,8 +210,8 @@ function localDeclarations(file,text,fn,list){ } const sources=git(['ls-tree','-r','--name-only','HEAD','web/src']).trim().split('\n').filter(p=>/\.tsx?$/.test(p)).map(p=>p.slice(8)); assert.deepEqual(git(['ls-files','--cached','--others','--exclude-standard','web/src']).trim().split('\n').filter(p=>/\.tsx?$/.test(p)).map(p=>p.slice(8)).sort(),[...sources].sort(),'No source file added or removed'); -// Every source is either frozen (byte-identical to HEAD) or a v12 file with its documented pieces (above). -assert.deepEqual([...sources].sort(),[...protectedFiles,...Object.keys(editedFiles)].filter(f=>/\.tsx?$/.test(f)).sort(),'every web/src source is frozen or a documented v12 file'); +// Every source is either frozen (byte-identical to HEAD) or a v13 file with its documented pieces (above). +assert.deepEqual([...sources].sort(),[...protectedFiles,...Object.keys(editedFiles)].filter(f=>/\.tsx?$/.test(f)).sort(),'every web/src source is frozen or a documented v13 file'); const scans={},facts={attributes:{},guarded:{},calls:{}}; for(const f of sources){ const old=calls(baseline('web/src/'+f),f),now=calls(read('web/src/'+f),f),c=callChanges[f]??{pieces:[],removed:[],uses:[]},used=c.pieces.map(()=>0); @@ -418,40 +235,20 @@ if(process.env.BASKET_GATE_FACTS)fs.writeFileSync(process.env.BASKET_GATE_FACTS, // includes these pins. Changing a callback, receiver, argument, location, order or count changes the digest. // >>> reviewed calls (manual; never refreshed by put-pieces) const addedCallPins={ - "Vault.tsx": { - "sha256": "4f11db85cbd213b7a60ff309a42fc122b75f2408685534c1596c381656a0347d", - "callees": "toUpperCase flatMap flatMap map some split reasonWords" - }, - "components.tsx": { - "sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "callees": "" - }, - "Docs.tsx": { - "sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "callees": "" - }, "Flows.tsx": { - "sha256": "a300e32681affcedb9a06c8a18f5f3278b503113fe7752c4b7f6705fcefc19f1", - "callees": "useRef useRef useLayoutEffect useState useLayoutEffect setDisplay some keys startsWith setDisplay map" + "sha256": "684589218caafbf91bde61f7f37ff05fb113369917cae3028fae03a1cdc5a4bd", + "callees": "useState some useEffect setListOpenFor" }, "wallet.tsx": { - "sha256": "4aa092bccb8c851943d0ea97214d6e4be2efb19245eea9383a4a4380efdf23c8", - "callees": "useRef setHash setConfirmed report report" - }, - "main.tsx": { "sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "callees": "" } }; // <<< reviewed calls -// SPEC 1-6 and the owner's mounting decision: exact reviewed source, beyond either generated block. +// v13: exact reviewed source of the two files, beyond either generated block. const reviewedSourcePins={ - "Vault.tsx": "d8936edd240c7571e4e65bdf878f65bea73b889c5541873f8fe2b128633a7582", - "components.tsx": "d9ae83c4ad85ca3762e24914efd4669e22916bb5a2e29bbfd778bcdec5041730", - "Docs.tsx": "5f24a982beba7124f0af8e06496b5e64f79b052978ff37785ff6700279f984c5", - "Flows.tsx": "3b380cc8bcdf5d2cc81ed46fef7274e00a0ac83025200cdc046bf8fb18c49e3b", - "wallet.tsx": "488b62c80121b3302ea8cbdac7d23e0a32d8fc874082dfab6499388949b083de", - "main.tsx": "2745de3906186f90b599d7b3675d602be6cc5ed0ed055813de8b4dd7b0f0e04e" + "Flows.tsx": "8ec2d9af627b26797c2971eb12154262001c0e1e5aab7b8b723e9640e41fed6a", + "wallet.tsx": "564ce2be2ec1c9e79f55bfd13b26c87e0b0525915e131d32d4d983413e8b8bcf" }; for(const f of Object.keys(editedFiles)){ const {now,all}=scans[f],list=all.added.map(i=>[now.all[i].text,now.all[i].place]); @@ -603,7 +400,7 @@ assert.deepEqual(sendUses['components.tsx'].filter(x=>/\(/.test(x)),['wallet.sen // names in LOADS, written as a name, a property or a plain string (folded across +): a picture, sound or worker made // in script; the address bar, the history and the navigator; a new window; an element or HTML built in script; a // src, href, action or style set in script. Then every JSX attribute in LOAD_ATTRIBUTES (any letter case) with its -// whole value, and every spread of attributes onto an element. Every file equals HEAD, with no exception: v12 adds no +// whole value, and every spread of attributes onto an element. Every file equals HEAD, with no exception: v13 adds no // link, picture, frame, style attribute or navigation. `open` alone is the list of open stocks in Flows.tsx, so it // counts only as a property (window.open) or when it is called. This is a list of known ways, not a proof that no // other way exists. @@ -622,9 +419,8 @@ function loadsOf(text,file){ })(tree); return out; } -// The retained scanner treats the asset flag a.open as a load name. This one new display-only -// entry belongs to the independently raw-pinned operational declaration; no network sink is exempted. -const loadDisplayAdded={'Vault.tsx':[{text:'a.open',place:'DepositState'}]}; +// v13 adds nothing a page loads or opens: every file equals HEAD here. +const loadDisplayAdded={}; const loadUses={}; for(const f of sources){ const h=loadsOf(baseline('web/src/'+f),f),n=loadsOf(read('web/src/'+f),f); @@ -693,15 +489,14 @@ function sendParts(text){ } const headSend=sendParts(baseline('web/src/wallet.tsx')),nowSend=sendParts(read('web/src/wallet.tsx')); assert.equal(nowSend.head,headSend.head,'wallet.tsx send(): byte-identical head through receipt wait'); -// SPEC 6: mined hash, sent error-link hash, top link, confirmation record and both Confirmed reports. -// These exact replacements are independent of both regeneration blocks and each must occur exactly once. +// v13: the Claim all result detail loses the "Nothing owed for N other stocks." sentence and its unused count. +// This exact replacement is independent of both regeneration blocks and must occur exactly once; the mined-hash +// statements of v12 are part of HEAD now and are pinned by sendTailWrites below. const SEND_TAIL_PIECES=[ - [' // After a Speed up the transaction is mined under another hash than the\n // one the wallet returned: from here on the mined one is shown and read.\n const mined = receipt.transactionHash;\n sent = mined;\n setHash(mined);\n',''], - ['setConfirmed({ hash: mined, functionName: s.functionName });','setConfirmed({ hash: tx, functionName: s.functionName });'], - ['report(key, { message: `${label}: Confirmed. Refreshing…`, hash: mined });','report(key, { message: `${label}: Confirmed. Refreshing…`, hash: tx });'], - ['report(key, { message: `${label}: Confirmed.`, hash: mined, detail });','report(key, { message: `${label}: Confirmed.`, hash: tx, detail });'], + [' detail =\n `Still owed to ${creditor}: ` +\n (fresh.complete\n ? remaining.join("; ") || "Nothing owed."\n : "Stock list unreadable — Retry vault.");\n', + ' const zeros = values.filter((v) => v.ok && v.value === 0n).length;\n detail =\n `Still owed to ${creditor}: ` +\n (fresh.complete\n ? (remaining.join("; ") || "Nothing owed.") +\n (zeros ? ` Nothing owed for ${zeros} other stocks.` : "")\n : "Stock list unreadable — Retry vault.");\n'], ]; -assert.equal(putBack(nowSend.tail,SEND_TAIL_PIECES,'wallet.tsx send() tail'),headSend.tail,'wallet.tsx send(): only exact receipt-hash tail replacements'); +assert.equal(putBack(nowSend.tail,SEND_TAIL_PIECES,'wallet.tsx send() tail'),headSend.tail,'wallet.tsx send(): only the exact v13 detail replacement in the tail'); pinnedFunctions['wallet.tsx send()']={headEqualToHead:true,headBytes:Buffer.byteLength(nowSend.head,'utf8'),headSha256:sha256(nowSend.head),tailPieces:SEND_TAIL_PIECES.map(([now,head])=>({now,head}))}; // An explicit ordered pin of the changed transaction-status statements, independent of the piece list. const sendTailWrites=[]; @@ -760,17 +555,8 @@ function rawProtected(label,file,select,count=1){ protectedCode[label]=h.map(x=>({bytes:Buffer.byteLength(x,'utf8'),sha256:sha256(x)})); } for(const name of ['switchChain','listingPending','waitPending'])rawProtected('wallet '+name,'wallet.tsx',n=>ts.isFunctionDeclaration(n)&&n.name?.text===name); -// Owner-approved issue-only notice and Deposit-only Closed LED: only these exact pieces may differ. -// Every original calculation, status branch, clock use and Retry handler restores byte-for-byte to HEAD. +// Vault.tsx is frozen in v13 (byte-identical to HEAD above); DepositState keeps its raw pin with no piece. const DEPOSIT_STATE_PIECES=[ - [ - "export function DepositState({ snapshot: s, showSign = false }: { snapshot: Snapshot; showSign?: boolean }) {", - "export function DepositState({ snapshot: s }: { snapshot: Snapshot }) {" - ], - [ - "\n // The owner wants a notice only for closed deposits or an unreadable status.\n const statusUnreadable =\n !s.complete || !r?.ok || s.globals.genesisFinalized === undefined;\n const operational =\n !statusUnreadable && reason === 0 && s.globals.genesisFinalized === true &&\n s.assets.some((a) => a.open && !a.retired);\n if (s.loading || operational || closed === mayBeOpen) return null;\n const issueReason = s.globals.genesisFinalized === false ? 1\n : !s.assets.length ? 4\n : s.assets.every((a) => !a.open || a.retired) ? 5\n : reason === 13 ? 9 : reason;\n const issue = reasonWords(issueReason, s.globals.settings).split(\" (\")[0];\n const weekendHours = reason === 3 &&\n s.globals.settings?.hoursFrom === 72000n && s.globals.settings?.hoursTo === 504000n;\n return (\n
\n {statusUnreadable ? (\n <>\n {!showSign && Vault status:}\n

unreadable — Retry vault

\n \n \n ) : showSign ? (\n <>\n \n

{weekendHours\n ? \"Deposits reopen when the stock feed prices update around 8pm on Sundays.\"\n : `Deposits will be back open when “${issue}” is resolved.`}

\n \n ) : (\n <>Vault status:

Closed

\n )}\n
\n );\n}", - "\n return (\n
\n Vault deposit status\n

\n {s.loading\n ? \"Reading...\"\n : !s.assets.length &&\n s.complete &&\n s.globals.genesisFinalized === false\n ? \"No Stock Tokens are listed yet. Deposits start after the owner finishes setup.\"\n : s.assets.length > 0 &&\n s.assets.every((a) => !a.open || a.retired) &&\n s.globals.genesisFinalized\n ? \"Every stock is closed. The owner must reopen a stock before deposits.\"\n : !r || !r.ok\n ? \"unreadable — Retry vault\"\n : reasonWords(Number(r.value[0]), s.globals.settings)}\n

\n {r?.ok && r.value[1] !== zeroAddress && (\n

\n Stock at fault:{\" \"}\n {s.assets.find(\n (a) => a.token.toLowerCase() === r.value[1].toLowerCase(),\n )?.symbol ?? r.value[1]}\n

\n )}\n {closed &&

{closed}

}\n

Hours: {s.loading ? \"Reading...\" : hoursLine(s.globals.settings)}

\n {!s.loading && (!r || !r.ok) && (\n \n )}\n
\n );\n}" - ] ]; const headDepositState=rawFunction(baseline('web/src/Vault.tsx'),'Vault.tsx','DepositState').text; const nowDepositState=rawFunction(read('web/src/Vault.tsx'),'Vault.tsx','DepositState').text; @@ -805,7 +591,7 @@ for(const name of ['short','approvals'])rawProtected('Deposit '+name,'Flows.tsx' // component or hook (parameters included) is still declared there once, by the same text: the same import, the same // useState or useRef, the same expression. So `vault`, `token`, `amount`, `depositArgs`, `VAULT`, `quote`, `inputs`, // `open`, `approvals`, `account` and every other name the pinned transaction code uses still mean what they meant at -// HEAD. The names v12 adds at the top of a file are listed here; a name v12 adds at the top of a component or hook +// HEAD. The names v13 adds at the top of a file are listed here (none); a name v13 adds at the top of a component or hook // must be new there and must not hide a name declared at the top of the file. function scopes(text,file){ const tree=parse(text,file),print=printerOf(tree),module=Object.create(null),functions=Object.create(null); @@ -867,120 +653,18 @@ for(const f of Object.keys(editedFiles)){ transactionCode[f]={pieces:n.count,namesDeclaredAroundThem:n.names}; } const namesAdded={ - "Vault.tsx": { - "DEPOSIT_LETTERS": "DEPOSIT_LETTERS: Record = { O: [\"01110\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"01110\"], E: [\"11111\", \"10000\", \"10000\", \"11110\", \"10000\", \"10000\", \"11111\"], C: [\"01111\", \"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"01111\"], L: [\"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"11111\"], S: [\"01111\", \"10000\", \"10000\", \"01110\", \"00001\", \"00001\", \"11110\"], D: [\"11110\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"11110\"], }", - "DepositLedSign": "function" - }, - "components.tsx": {}, - "Docs.tsx": {}, "Flows.tsx": {}, - "wallet.tsx": {}, - "main.tsx": { - "Activity": "import Activity from \"react\"" - } + "wallet.tsx": {} }; -// Exact removal of the unused display-only hoursLine import for the owner-approved status move. +// v13 changes no declaration at the top of either file. const moduleChanges={ - "Vault.tsx": [], - "components.tsx": [], - "Docs.tsx": [], - "Flows.tsx": [ - { - "name": "hoursLine", - "head": [ - "import hoursLine from \"./Vault\"" - ], - "now": null - } - ], - "wallet.tsx": [], - "main.tsx": [] + "Flows.tsx": [], + "wallet.tsx": [] }; -// Narrow exact declarations changed by SPEC 1/4 and the owner-approved status move; never generated by put-pieces. +// v13 changes no declaration at the top of a component or hook; never generated by put-pieces. const scopeChanges={ - "Vault.tsx": [ - { - "function": "DepositState", - "name": "s", - "head": [ - "parameter { snapshot: s }: { snapshot: Snapshot; }" - ], - "now": [ - "parameter { snapshot: s, showSign = false }: { snapshot: Snapshot; showSign?: boolean; }" - ] - } - ], - "components.tsx": [ - { - "function": "PageTitle", - "name": "eyebrow", - "head": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ], - "now": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children?: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ] - }, - { - "function": "PageTitle", - "name": "title", - "head": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ], - "now": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children?: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ] - }, - { - "function": "PageTitle", - "name": "children", - "head": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ], - "now": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children?: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ] - }, - { - "function": "PageTitle", - "name": "character", - "head": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ], - "now": [ - "parameter { eyebrow, title, children, character, }: { eyebrow?: string; title: string; children?: ReactNode; character?: { kind: \"vault\" | \"deposit\" | \"redeem\"; src: string; width: number; height: number; alt: string; line: string; }; }" - ] - } - ], - "Docs.tsx": [], - "Flows.tsx": [ - { - "function": "DepositPage", - "name": "r", - "head": [ - "r = s.status" - ], - "now": null - }, - { - "function": "DepositPage", - "name": "reason", - "head": [ - "reason = r?.ok ? Number(r.value[0]) : undefined" - ], - "now": null - }, - { - "function": "Claims", - "name": "zeros", - "head": [ - "zeros = s.assets.filter((a) => owed[a.token] === 0n)" - ], - "now": null - } - ], - "wallet.tsx": [], - "main.tsx": [] + "Flows.tsx": [], + "wallet.tsx": [] }; const newNames={}; for(const f of Object.keys(editedFiles)){ @@ -1005,7 +689,7 @@ for(const f of Object.keys(editedFiles)){ } } // The whole-source digest is additional to the specific structural assertions above. -for(const [f,digest] of Object.entries(reviewedSourcePins))assert.equal(sha256(read('web/src/'+f)),digest,f+': reviewed v12 source differs; regeneration does not authorize source edits'); +for(const [f,digest] of Object.entries(reviewedSourcePins))assert.equal(sha256(read('web/src/'+f)),digest,f+': reviewed v13 source differs; regeneration does not authorize source edits'); // Differential encoding: HEAD and current governance/model/chain bundled side by side. For the same inputs the // calldata must be byte-identical whenever the current page accepts; v12 adds no refusal, never accepts what HEAD // refused, and refuses with HEAD's words (model.ts, chain.ts and governance.ts are byte-identical to HEAD in v12). diff --git a/web/scripts/check-visibility.mjs b/web/scripts/check-visibility.mjs index 88bbed1..e1f329b 100644 --- a/web/scripts/check-visibility.mjs +++ b/web/scripts/check-visibility.mjs @@ -46,12 +46,16 @@ const base=`http://127.0.0.1:${server.address().port}`; const b=await chromium.launch(),page=await b.newPage({viewport:{width:375,height:812},reducedMotion:'reduce'}); const results=[]; -// v12 SPEC 1-4 permits only the named text/layout changes below. All prior +// v13 on the v12 baseline: the only permitted first-screen change is on the +// Deposit page, where the stock list under the search is shown open across the +// panel while the form is empty (both variants here: no wallet, and the +// synthetic account that holds nothing): the fold line becomes a plain heading +// line with the same words in the same place, and what follows moves down by +// exactly the open list's height. Every v12 state (Vault heading, Docs +// paragraphs, Redeem layout, status notices) must equal the baseline. All prior // first-screen elements otherwise retain their words and remain on screen. // The header, slogan strip, and start of each title character stay pinned. const characterPages=['vault','deposit','redeem']; -const removedVault='Meet Basket (BASK): your share of the Stock Tokens in this vault.'; -const removedWalletLine='Showing 0x0000…11ce. To use another wallet, switch the address in your wallet app: this page follows it.'; const docsAdded=[ 'The basket has no set weights. Any listed stock can be deposited, in any amount, as long as the vault’s checks pass and the vault stays under its size limit. So the mix is whatever holders have put in, and it changes with every deposit and redemption.', 'BASK’s backing moves with every stock in the vault, so its market price can drift from the value behind it. Traders close that gap: they deposit stocks and sell BASK when it trades high, or buy BASK and redeem when it trades low. That keeps BASK near its backing.', @@ -187,6 +191,12 @@ function layoutSnapshot() { })), depositOpen:document.querySelector('.deposit-open')?{text:document.querySelector('.deposit-open').textContent.replace(/\s+/g,' ').trim(),...box('.deposit-open')}:null, flowSteps:document.querySelector('.flow-steps')?{text:document.querySelector('.flow-steps').textContent.replace(/\s+/g,' ').trim(),...box('.flow-steps')}:null, + // v13: the stock list under the Deposit search (the closed fold at the baseline; the open list in v13's empty states). + stockList:{open:!!document.querySelector('.all-stocks.open-list'),summary:!!document.querySelector('.all-stocks summary'), + title:document.querySelector('.all-stocks .list-title, .all-stocks summary')?.textContent.replace(/\s+/g,' ').trim()??null, + ...box('.all-stocks'), + width:(()=>{const ul=document.querySelector('.all-stocks ul'),panel=ul?.closest('section.panel');if(!ul||!panel)return null;const cs=getComputedStyle(panel),inner=panel.clientWidth-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);return ul.getBoundingClientRect().width/inner;})(), + buttons:document.querySelectorAll('.all-stocks ul button').length}, depositSummary:box('.deposit-summary'), led:ledGeometry(), sideways:document.documentElement.scrollWidth>innerWidth, @@ -246,15 +256,16 @@ try { const docsGrowth=newLayout.docsAddedHeight; if(name==='vault') { assert.equal(newLayout.heading,'Always in stock'); - assert.deepEqual(oldLayout.titleParagraphs,[removedVault]); + assert.equal(oldLayout.heading,newLayout.heading,'Vault heading equals the v12 baseline'); + assert.deepEqual(oldLayout.titleParagraphs,[]); assert.deepEqual(newLayout.titleParagraphs,[]); assert.ok(newLayout.title.height<=oldLayout.title.height+0.5,'Vault panel must not get taller'); - assert.equal(oldLayout.capacity.label,'Size limit','Baseline retains the old capacity label'); - assert.equal(newLayout.capacity.value,oldLayout.capacity.value,'Capacity figure value remains unchanged'); - checks.push('v12 Vault: exact replacement heading, sentence and paragraph removed, panel does not grow; exact capacity label with a smaller note underneath, unchanged figure value'); + assert.equal(oldLayout.capacity.label,'Maximum Basket Capacity','Baseline carries the v12 capacity label'); + assert.deepEqual(newLayout.capacity,oldLayout.capacity,'Capacity label and figure value remain unchanged'); + checks.push('v13 Vault: unchanged; heading, empty title paragraphs, panel height, capacity label and figure value equal the v12 baseline'); } assert.ok(statusFixtureReads>beforeFixtureReads,'State comparison consumes the existing depositStatus fixture'); - assert.equal(oldLayout.led.signs.length,0,'Baseline has no LED sign'); + assert.equal(oldLayout.led.signs.length,newLayout.led.signs.length,'Baseline and v13 carry the same sign count'); if(statusCase==='normal') { assert.equal(newLayout.depositStatus.length,0,'Normal operation has no extra status area'); assert.equal(newLayout.led.signs.length,0,'Normal operation has no sign'); @@ -271,8 +282,10 @@ try { assert.equal(status.compact,name==='vault','Compact display is scoped to Vault'); assert.equal(newLayout.led.reopening,null,'Old reopening/countdown paragraphs are absent'); assert.equal(newLayout.depositOpen,null,'Deposit no longer uses the open-only line'); - if(statusCase==='closed')assert.equal(oldLayout.depositStatus[0].paragraphs[0].text,'too few fresh prices','Baseline proves the same reason fixture'); + if(statusCase==='closed')assert.equal(oldLayout.depositStatus[0].paragraphs[0].text,name==='vault'?'Closed':ISSUE_NOTICE,'Baseline proves the same reason fixture'); else assert.ok(weekendBlockReads>beforeBlocks,'Weekend comparison uses the public-block timestamp fixture'); + assert.deepEqual(newLayout.depositStatus,oldLayout.depositStatus,'v13: the status area equals the v12 baseline'); + assert.deepEqual(newLayout.led,oldLayout.led,'v13: the sign geometry equals the v12 baseline'); assert.equal(newLayout.led.signs.length,name==='deposit'?1:0,'Only Deposit has the known-closed sign'); if(name==='deposit') { const sign=newLayout.led.signs[0]; @@ -286,45 +299,49 @@ try { assert.equal(newLayout.walletLine,null,'Owner removed the wallet-switch instruction from Deposit'); assert.equal(newLayout.flowSteps.text,oldLayout.flowSteps.text,'Task instructions keep their words'); if(oldLayout.flowSteps.bottom<=812)assert.ok(newLayout.flowSteps.bottom<=812,'Previously visible Deposit task instructions remain on the first screen'); + // v13: both variants are empty forms (no wallet; the synthetic account holds nothing), so the list is open + // across the panel with the fold's words as a plain heading line in the fold line's place; the baseline shows the closed fold. + assert.deepEqual([oldLayout.stockList.open,oldLayout.stockList.summary,newLayout.stockList.open,newLayout.stockList.summary],[false,true,true,false],'v13: the open list replaces the closed fold in the empty states'); + assert.equal(newLayout.stockList.title,oldLayout.stockList.title,'The list heading keeps the fold line\'s words'); + assert.match(newLayout.stockList.title,/^All stocks you can deposit \(\d+\)$/); + assert.ok(near(newLayout.stockList.top,oldLayout.stockList.top),'The list starts where the fold line started'); + assert.ok(newLayout.stockList.width>0.98,'The open list spans the panel: '+newLayout.stockList.width); + assert.ok(newLayout.stockList.buttons>0&&newLayout.stockList.title.endsWith('('+newLayout.stockList.buttons+')'),'Every open stock has its button'); + checks.push('v13 Deposit: open stock list across the panel in both empty states, heading words and start unchanged, no fold, one button per open stock'); } checks.push(statusCase==='normal'?'Owner follow-up: no normal status area/sign':'Owner follow-up: exact single '+statusCase+' sentence, compact Vault Closed notice, Deposit-only CLOSED sign'); if(name==='docs') { - assert.equal(oldLayout.docsParagraphs.length,1); + assert.equal(oldLayout.docsParagraphs.length,5); assert.equal(newLayout.docsParagraphs.length,5); - assert.deepEqual(newLayout.docsParagraphs.slice(1).map(p=>p.text),docsAdded); - assert.equal(newLayout.docsParagraphs[0].text,oldLayout.docsParagraphs[0].text); - assert.ok(near(newLayout.docsParagraphs[0].top,oldLayout.docsParagraphs[0].top)&&near(newLayout.docsParagraphs[0].height,oldLayout.docsParagraphs[0].height),'Docs existing paragraph keeps its geometry'); - assert.ok(docsGrowth>0&&near(newLayout.docs[0].height-oldLayout.docs[0].height,docsGrowth),'Docs grows only by the four paragraphs including their spacing'); + assert.deepEqual(oldLayout.docsParagraphs.slice(1).map(p=>p.text),docsAdded,'Baseline carries the four v12 paragraphs'); + assert.deepEqual(newLayout.docsParagraphs.map(p=>p.text),oldLayout.docsParagraphs.map(p=>p.text),'Docs paragraphs equal the baseline word for word'); assert.equal(newLayout.docs.length,oldLayout.docs.length); - for(let i=1;i0,'The open list is taller than the fold line'); for(const x of prior) { const y=counterpart(x); - if(['deposit','redeem'].includes(name)&&x.tag==='P'&&normalize(x.text)===removedWalletLine) { - assert.equal(y,null,'The exact owner-removed wallet-switch line is absent');exceptions.push({reason:'Owner removed wallet-switch instruction',element:x});continue; - } - if(name==='vault'&&x.inTitle&&x.tag==='P'&&normalize(x.text)===removedVault) { - assert.equal(y,null,'The exact removed Vault sentence is absent');exceptions.push({reason:'SPEC 1 removed Vault sentence',element:x});continue; - } - if(name==='redeem'&&(x.inRedeemAside||x.inClaims)) { - exceptions.push({reason:x.inRedeemAside?'SPEC 4 removed Redeem aside':'SPEC 4 claims panel absent when nothing owed',element:x});continue; + if(name==='deposit'&&x.tag==='SUMMARY'&&/^All stocks you can deposit \(\d+\)$/.test(normalize(x.text))) { + const line=now.find(z=>z.tag==='P'&&words(z.text)===words(x.text)); + assert.ok(line&&near(line.y,x.y),'v13: the fold line is a plain heading line with the same words in the same place'); + exceptions.push({reason:'v13 open stock list: heading line in the place of the fold line',element:x,now:line});continue; } - if(name==='docs'&&x.docsIndex>0&&y&&near(y.y-x.y,docsGrowth)&&near(y.bottom-x.bottom,docsGrowth)) { - exceptions.push({reason:'SPEC 2 exact added-paragraph displacement',element:x,now:y});continue; + if(name==='deposit'&&x.y>=oldLayout.stockList.top-0.5&&y&&near(y.y-x.y,listGrowth)&&near(y.bottom-x.bottom,listGrowth)) { + exceptions.push({reason:'v13 open stock list: exact displacement by the open list height',element:x,now:y});continue; } if(y&&y.bottom<=812) { if(name==='vault'&&(y.y>x.y+0.5||y.bottom>x.bottom+0.5))regressions.push({...x,now:y,reason:'Vault content moved downward'}); @@ -333,7 +350,7 @@ try { regressions.push({...x,now:y}); } // Original slogan-strip and title-character assertions, with explicit header - // height and overflow pins added. No v12 exception applies to these parts. + // height and overflow pins added. No v13 exception applies to these parts. checks.push('header retains its exact position and height'); if(!near(newLayout.header.top,oldLayout.header.top)||!near(newLayout.header.height,oldLayout.header.height))regressions.push({tag:'HEADER',text:'header',old:oldLayout.header,now:newLayout.header}); checks.push('slogan strip in the same place and size, still and full under reduced motion'); @@ -352,6 +369,6 @@ try { } assert.deepEqual(routingErrors,[],'Only local files and allowed public RPC reads; fixture account reads stay synthetic'); for(const r of results)if(r.regressions.length)throw Error(JSON.stringify({variant:r.variant,name:r.name,regressions:r.regressions})); - console.log('PASS 375x812 baseline visibility: six pages, no-wallet and connected;',results.reduce((n,r)=>n+r.prior.length,0),'previous first-screen elements preserved except exact named v12 removals and Docs displacement; header height, slogan strip and character assertions retained'); + console.log('PASS 375x812 baseline visibility: six pages, no-wallet and connected;',results.reduce((n,r)=>n+r.prior.length,0),'previous first-screen elements preserved except the exact named v13 Deposit list heading and displacement; header height, slogan strip and character assertions retained'); fs.writeFileSync('../artifacts/phone-visibility.json',JSON.stringify(results,null,2)+'\n'); }finally{await b.close();await new Promise(r=>server.close(r));} diff --git a/web/scripts/run-browser.mjs b/web/scripts/run-browser.mjs index 3516071..78c2bf3 100644 --- a/web/scripts/run-browser.mjs +++ b/web/scripts/run-browser.mjs @@ -467,6 +467,7 @@ try { let lastSent, lastMined, speedNext = false, repricing; let holdConnect = false, releaseConnect, emptyAccounts = false; const v12Checks = []; + const v13Checks = []; page.on("console", (m) => { if (m.type() === "error") consoleErrors.push(m.text()); }); @@ -1599,6 +1600,90 @@ try { checks.push( "Disconnect: address gone, Connect wallet shown, the exact top message, still disconnected after a reload, Connect wallet brings the address back; Disconnect makes no wallet request or send", ); + // v13: the stock list under the search. With a wallet that holds listed + // stock it is the closed fold (checked above); while the form is empty it is + // shown open across the panel, with its heading as a plain line and no + // details element, and it stays open for that account after a press. The + // list reads from the vault only: no wallet request and no send. + const openList = page.locator("div.all-stocks.open-list"), + listFacts = () => + page.evaluate(() => { + const ul = document.querySelector(".all-stocks ul"), + title = document.querySelector(".all-stocks .list-title"), + panel = ul.closest("section.panel"), + cs = getComputedStyle(panel), + // The panel's content box: client width without its frame and padding. + inner = panel.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight), + u = ul.getBoundingClientRect(); + return { + title: title?.textContent, + titleTag: title?.tagName, + summaries: document.querySelectorAll(".all-stocks summary").length, + buttons: ul.querySelectorAll("button").length, + ratio: u.width / inner, + columns: getComputedStyle(ul).gridTemplateColumns.split(" ").length, + overflow: document.documentElement.scrollWidth > innerWidth, + }; + }); + const listRequests = asked.length, listSends = sends.length; + // No wallet: the Deposit page after a Disconnect (and after the reload above + // the page stayed disconnected). The owner's connected wallet above showed the + // fold closed again: the list stays open only for the account it opened for. + await page.getByRole("button", { name: "Disconnect", exact: true }).click(); + await page + .getByText("Connect your wallet to see the stocks you hold.", { exact: true }) + .waitFor(); + await openList.waitFor(); + assert.equal(await page.locator("details.all-stocks").count(), 0); + let facts = await listFacts(); + assert.deepEqual( + [facts.title, facts.titleTag, facts.summaries, facts.buttons, facts.overflow], + ["All stocks you can deposit (25)", "P", 0, 25, false], + JSON.stringify(facts), + ); + assert.ok(facts.ratio > 0.98 && facts.columns >= 8, JSON.stringify(facts)); + await openList + .getByText("Press a stock to add it to your deposit.", { exact: true }) + .waitFor(); + // A wallet that holds no listed stock (the next owner's account, which never + // held one): the same open list; a press adds the row and the list stays open. + await walletAnswers(2, () => + header.getByRole("button", { name: "Connect wallet", exact: true }).click(), + ); + await address.waitFor(); + await page.getByLabel("FIG amount", { exact: true }).waitFor(); + assert.equal(await page.locator("details.all-stocks").count(), 1); + await page.evaluate((a) => window.__accounts([a]), nextOwner); + await page + .getByText("No open listed stock in this wallet.", { exact: true }) + .waitFor(); + await openList.waitFor(); + assert.deepEqual( + [await page.locator(".deposit-row").count(), await page.locator("details.all-stocks").count()], + [0, 0], + ); + facts = await listFacts(); + assert.deepEqual([facts.title, facts.summaries, facts.buttons], ["All stocks you can deposit (25)", 0, 25]); + assert.ok(facts.ratio > 0.98 && facts.columns >= 8, JSON.stringify(facts)); + await openList.getByRole("button", { name: "FIG", exact: true }).click(); + await page.getByLabel("FIG amount", { exact: true }).waitFor(); + assert.equal(await page.locator(":focus").getAttribute("aria-label"), "FIG amount"); + assert.equal(await page.locator(".deposit-row").count(), 1); + assert.equal(await openList.count(), 1); + assert.equal(await openList.getByRole("button", { name: "FIG", exact: true }).isDisabled(), true); + assert.equal(await page.locator("details.all-stocks").count(), 0); + // Back to the owner's wallet: its rows, and the fold closed as before. + await page.evaluate((a) => window.__accounts([a]), owner); + await page.waitForFunction(() => document.querySelectorAll(".deposit-row").length === 25); + assert.equal(await openList.count(), 0); + assert.equal(await page.locator("details.all-stocks").count(), 1); + assert.equal(await page.locator("details.all-stocks").evaluate((d) => d.open), false); + assert.equal(sends.length, listSends); + assert.deepEqual([...new Set(asked.slice(listRequests))].sort(), ["eth_chainId", "eth_requestAccounts"]); + assert.deepEqual([issues, consoleErrors.slice(before[2])], [[], []]); + v13Checks.push( + "Deposit stock list: open across the panel with a plain heading and no fold while the form is empty (no wallet; a wallet without a listed stock), a press adds the row and the list stays open for that account, the owner's wallet keeps the closed fold; no wallet request or send", + ); // A Saturday: deposits refused (Hours) with the reopen time; redeem and claim still work. await warpTo(await saturday()); assert.equal(await rv("insideHours"), false); @@ -1698,6 +1783,12 @@ try { assert.equal(await page.locator("section.claims .claim-grid > .receipt").count(), 0); await page.waitForFunction(()=>document.querySelector("section.claims")?.textContent.includes("Confirmed.")); v12Checks.push("Claims: one/two owed show panel with exact conditional note and no zero fold; last claim confirmation survives navigation"); + // v13: the Claim all result line names what is still owed and no longer + // counts the stocks that are not. + const claimsText = await page.locator("section.claims").textContent(); + assert.match(claimsText, /Still owed to 0x[0-9a-fA-F]{40}: Nothing owed\./, claimsText); + assert.ok(!/Nothing owed for \d+ other stocks/.test(claimsText), claimsText); + v13Checks.push("Claim all result: the Still owed line ends with Nothing owed. and carries no other-stocks count"); // Immediate controls and each proposal form. await nav("Owner"); const closeForm = action("Close a stock"); @@ -2068,7 +2159,7 @@ try { assert.ok(facts.natural>=240&&facts.lines>1&&facts.inside&&!facts.overflow,JSON.stringify(facts));assert.deepEqual(facts.bad,[]); } v12Checks.push("24/7 label: widened text wraps at whole words inside pill and panel at 661px on Deposit and Redeem"); - checks.push(...v12Checks); + checks.push(...v12Checks, ...v13Checks); assert.deepEqual([...new Set(asked)].sort(),["eth_accounts","eth_chainId","eth_requestAccounts","eth_sendTransaction"]); assert.equal(revoked.length,0); assert.deepEqual(issues,[]); diff --git a/web/src/Flows.tsx b/web/src/Flows.tsx index 0762c7d..2fc1038 100644 --- a/web/src/Flows.tsx +++ b/web/src/Flows.tsx @@ -79,6 +79,9 @@ export function DepositPage({ snapshot: s, wallet: w }: Props) { const [held, setHeld] = useState>(); const [removed, setRemoved] = useState>({}); const [showSmall, setShowSmall] = useState(false); + // v13: the account ("" for none) for which the stock list under the search + // was shown open because the form was empty; it stays open for it. + const [listOpenFor, setListOpenFor] = useState(); // The stock an Add button just added, until its row is on screen. const focusRow = useRef
(undefined); const sequence = useRef(0), @@ -337,6 +340,19 @@ export function DepositPage({ snapshot: s, wallet: w }: Props) { b = y.symbol.toLowerCase(); return a < b ? -1 : a > b ? 1 : 0; }); + // v13: the form is empty with no wallet, or with a wallet whose balances + // are read and hold no listed stock while no row is in the form. Then the + // stock list is shown open across the panel, and it stays open for that + // account until the account changes or the page is reloaded. + const emptyForm = + !w.account || + (ready && !unread && !rows.length && !stocks.some((x) => x.inWallet)); + const accountKey = w.account ?? ""; + useEffect(() => { + if (emptyForm) setListOpenFor(accountKey); + }, [emptyForm, accountKey]); + const listOpen = emptyForm || listOpenFor === accountKey; + const StockList = listOpen ? "div" : "details"; // `focus` is false for Enter in the search box, which keeps the focus. function add(t: Address, focus = true) { if (focus) focusRow.current = t; @@ -642,11 +658,24 @@ export function DepositPage({ snapshot: s, wallet: w }: Props) {

)}
- {/* Every stock the vault takes now, closed until pressed. A - press is the same Add as in the search; a stock that has a - row stays in the list, switched off. */} -
- All stocks you can deposit ({open.length}) + {/* Every stock the vault takes now. Closed until pressed while + the form has rows; shown open across the panel while the + form is empty (no wallet, or a wallet without a listed + stock), and then for the rest of the visit for that account + (v13). A press is the same Add as in the search; a stock + that has a row stays in the list, switched off. */} + + {listOpen ? ( +

+ All stocks you can deposit ({open.length}) +

+ ) : ( + + All stocks you can deposit ({open.length}) + + )}

Press a stock to add it to your deposit.

Grey stocks are already in the form above.

    @@ -670,7 +699,7 @@ export function DepositPage({ snapshot: s, wallet: w }: Props) { ))}
-
+ {counted + badAmount.length + noPrice.length > 0 && (
{counted > 0 && ( diff --git a/web/src/styles.css b/web/src/styles.css index 0d3538f..1004cac 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1354,7 +1354,8 @@ button.quiet.small-toggle { .all-stocks summary:hover { text-decoration-thickness: 2px; } -.all-stocks[open] { +.all-stocks[open], +.all-stocks.open-list { margin-bottom: 20px; } /* Clear of the focus ring of the line above it. */ @@ -1362,6 +1363,13 @@ button.quiet.small-toggle { margin: 6px 0 10px; font-size: 14px; } +/* v13: while the form is empty (no wallet, or a wallet without a listed stock) + the list is shown open across the panel, and its heading line is plain text + in the place of the fold's line, not a link. */ +.all-stocks .list-title { + margin: 0; + font-weight: 700; +} /* Even columns that fill the line, so many tickers stay easy to scan. */ .all-stocks ul { display: grid; diff --git a/web/src/wallet.tsx b/web/src/wallet.tsx index 4f78a82..5b1107e 100644 --- a/web/src/wallet.tsx +++ b/web/src/wallet.tsx @@ -423,12 +423,10 @@ export function useWallet(refresh: () => Promise) { ? [`${a.symbol}: ${fmt(value.value, a.tokenDecimals)}`] : []; }); - const zeros = values.filter((v) => v.ok && v.value === 0n).length; detail = `Still owed to ${creditor}: ` + (fresh.complete - ? (remaining.join("; ") || "Nothing owed.") + - (zeros ? ` Nothing owed for ${zeros} other stocks.` : "") + ? remaining.join("; ") || "Nothing owed." : "Stock list unreadable — Retry vault."); } } catch { diff --git a/web/validation/README.md b/web/validation/README.md index 5d9e04a..947b151 100644 --- a/web/validation/README.md +++ b/web/validation/README.md @@ -34,3 +34,5 @@ BASKET_BASELINE_DIST=/absolute/previous/dist node scripts/check-visibility.mjs c The v12 fork keeps all 74 earlier check positions and 70 page sends. Two owner-approved result names change: the normal/closure status case and the changed-Hours closure wording case. Of 149 inherited assertion calls, 140 are byte-identical and ordered. Nine status-presentation assertions are replaced explicitly: one either-line-or-box assertion becomes conditional absence/presence checks, two open-line assertions become absence checks, the holiday label/reason array becomes one exact sentence, the holiday detail/hour/length assertions become no-heading/no-hours/one-paragraph checks, and two Saturday arrays become the exact default-weekend and changed-hours generic sentences. All on-chain Hours/Dst and transaction assertions remain unchanged. Three obsolete status-text waits use the new exact sentences. Added cases cover the Closed-only sign, plain Vault notice and both removed wallet instruction lines alongside the original v12 checks. The mock repricing submission is internal to the fixture and adds no page send. The walk expects 84 checks, 70 page sends and 24 screenshots; generated browser-fork.json and the exact send comparison record the actual results. The existing v12 Vault case also checks the exact capacity label and smaller separate subline; the Deposit case verifies its Size limit wording remains. The expanded fix suite drives actual Claims through first-read blank state, quiet empty/no-wallet lines, positive and unreadable balances, incomplete lists, retry, stable refreshes, running claims, errors and retained confirmations. React Activity preserves the account visit while hidden and pauses effects; account changes reset it. The art suite adds a portable widened-text case of at least 240px, with whole-word wrapping and pill/panel/character clearance, including a rejection of the former nowrap rule. Every existing assertion remains except its narrowly documented owner-approved replacement. The latest status decision replaces the obsolete art-bundle pin for “Deposits are open.” with the exact short closure sentence on Deposit, the plain Closed notice on Vault, and absence of normal status. The Closed-only Deposit LED sign’s named border-chase and letter-glow animations are an explicit owner-approved addition; marquee and shelf motion checks remain, and reduced motion stops both sign animations while retaining its readable state. + +The v13 baseline is the v12 commit 586998cf5eb2a6ccce05615ddf7f44874813042c. check-calldata permits only Flows and wallet, restored exactly through documented v13 pieces; Vault, components, Docs, main and every other source equal HEAD. check-bundle's source scope is Flows, wallet and styles. check-visibility compares the v12 six-route phone view at 375×812 and names one exception, the Deposit stock list shown open while the form is empty. The v13 fork keeps all 84 v12 check positions and 70 page sends, adds two checks (the open stock list in both empty states, with a press that keeps it open and the owner's wallet keeping the closed fold; the Claim all result line without the other-stocks count) and takes the same 24 screenshots.