Skip to content

Pre-flight balance check across all chunked bundle txs#79

Open
kriskowal wants to merge 2 commits into
mainfrom
preflight-balance-check-chunked-bundles
Open

Pre-flight balance check across all chunked bundle txs#79
kriskowal wants to merge 2 commits into
mainfrom
preflight-balance-check-chunked-bundles

Conversation

@kriskowal

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an optional validateCost callback to installBundle() that runs once after compression and before the first signAndBroadcast (in both chunked and non-chunked paths), so a chunked submission can no longer begin with a balance that can't cover the full series.
  • agoric.tsx provides the callback using already-in-scope swingSetParams and accountBalances queries plus existing balance.ts helpers; throws a toast error naming the shortfall and (when chunked) the "manifest of N total" framing.
  • Closes a silent-pass hole in BundleForm.tsx where the form-level check was skipped when costPerByte wasn't loaded yet.

Test plan

  • yarn tsc --noEmit clean
  • yarn lint clean (3 pre-existing fast-refresh warnings unrelated to this change)
  • yarn test:unit 14/14 passing
  • Manual: chunked path (?enable-chunking) with insufficient uist — toast names shortfall, manifest-of-N description; no wallet signing prompt; nothing left on chain
  • Manual: chunked path with sufficient funds — flow runs unchanged
  • Manual: non-chunked path — same gating applies; toast names "the bundle install transaction"
  • Manual: submit before swingset params resolve — form-level toast appears and submission is aborted

🤖 Generated with Claude Code

@netlify

netlify Bot commented Apr 30, 2026

Copy link
Copy Markdown

Deploy Preview for cosmos-proposal-builder ready!

Name Link
🔨 Latest commit 4c08d9c
🔍 Latest deploy log https://app.netlify.com/projects/cosmos-proposal-builder/deploys/69f3f1fc3b6ff60008bca024
😎 Deploy Preview https://deploy-preview-79--cosmos-proposal-builder.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

kriskowal and others added 2 commits May 1, 2026 00:20
A wallet that could afford the manifest tx but not the full chunk
series was able to begin a chunked submission and run out of funds
mid-stream, leaving partial state on chain. The existing form-level
check was also silently skipped whenever the swingset params query
was still loading, so submission could begin with no real balance
check at all.

Add an optional validateCost callback to installBundle that fires
once after compression and before the first signAndBroadcast in
either the chunked or non-chunked path. agoric.tsx provides it,
using the already-in-scope swingset params and account balances
queries plus existing balance.ts helpers, and throws a toast error
naming the shortfall (and the manifest-of-N total when chunked) if
the wallet can't cover costPerByte * compressedSize. The form-level
guard now also refuses when cost data isn't available rather than
falling through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Make installBundle the single source of truth: drop the redundant
  form-level balance check from BundleForm, since validateCost now
  covers it authoritatively and uses the compressed-byte total
  (tighter and consistent with what the chain stores). Two layers
  of checking against two different snapshots (uncompressed file
  size vs. compressed bytes) was a source of divergence.
- Use hasSufficientBalance helper instead of recomputing remaining
  cost manually in agoric.tsx; only compute display details in the
  insufficient branch.
- Display amounts via scaleToDenomBase so the toast says "1.5 IST"
  instead of "1500000 uist", matching the form-level cost display
  in CodeInput.
- Match the existing insufficient-funds wording from
  transactionParser ("X IST required, only Y IST available")
  rather than inventing a parallel format.
- Use 3000ms autoClose to match the rest of the app.
- Replace the chunkCount=0 overload in validateCost's info object
  with chunked: boolean; chunkCount?: number, matching the existing
  InstallBundleProgress preflight event shape.
- Add unit tests for installBundle covering: validateCost throw
  aborts before any signAndBroadcast, chunked path passes
  chunked=true with a chunk count, and the happy path proceeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kriskowal kriskowal force-pushed the preflight-balance-check-chunked-bundles branch from eb0463e to 4c08d9c Compare May 1, 2026 00:21
@kriscendobot

Copy link
Copy Markdown

We applied the following findings against PR #79 in a workspace on kriscendobot/cosgov (branch preflight-balance-check-chunked-bundles); the diff is at 4c08d9c...kriscendobot:cosgov:preflight-balance-check-chunked-bundles. Reviewers can pull the branch into a local checkout to evaluate the proposed edits.

Findings (suggested next steps):

  1. Drop the bundleCost! non-null assertion and the opaque !== false admit pattern. In src/config/agoric/agoric.tsx (the validateCost callback around lines 158-160 of the current PR head), replace

    const bundleCost = calculateBundleCost(costPerByte, compressedSize);
    if (hasSufficientBalance(bundleCost, balances) !== false) return;
    const [required, denom] = bundleCost!;

    with explicit guards:

    const bundleCost = calculateBundleCost(costPerByte, compressedSize);
    if (!bundleCost) return;
    if (hasSufficientBalance(bundleCost, balances)) return;
    const [required, denom] = bundleCost;

    Pure clarity/safety; behavior is unchanged because the null branch is unreachable for any non-empty bundle (and validateBundleJson rejects malformed input upstream).

  2. Distinct toast when the wallet holds no balance for the storage denom. In the same validateCost callback in src/config/agoric/agoric.tsx, branch on selectCoinBalance(accountBalances, denom) === undefined before computing scaledAvailable. Emit "No ${scaledDenom} balance found in wallet. Required: ${scaledRequired} ${scaledDenom}.${txContext}" for that case; keep the existing "Insufficient funds. ..." wording when the denom is present but insufficient. Both still throw with autoCloseToast: 3000.

  3. Remove the now-dead getBundleCost from CodeInputMethods. In src/components/CodeInput.tsx, drop the getBundleCost?: () => [amount: number, denom: string] | null; field from the CodeInputMethods interface (~line 31) and the corresponding getBundleCost: () => bundleCost, line from the useImperativeHandle block (~line 72). The only consumer was the form-level check this PR removed; grep -rn "getBundleCost" src/ confirms no remaining references.

  4. Tighten the happy-path test. In src/installBundle/installBundle.spec.ts, the third test currently only asserts call counts. Add expect(validateCost).toHaveBeenCalledWith({ compressedSize: expect.any(Number), chunked: false, chunkCount: undefined }) to lock the contract shape on the happy path.

  5. Pin chunkCount exactly in the chunked test. In the same spec, replace expect(seen[0].chunkCount).toBeGreaterThan(0) with the explicit invariant Math.ceil(compressedBundleBytes.byteLength / chunkSizeLimit). Since the gzip mock is identity (async (bytes) => bytes) and chunkSizeLimit: 10, the expected value is Math.ceil(new TextEncoder().encode(validBundleJson).byteLength / 10); assert exact equality.

  6. Add three agoric.spec.tsx coverage tests (deferred). The validateCost callback in agoric.tsx has three exit paths: missing costPerByte/balances throws "Cannot verify funds…"; sufficient balance returns silently; insufficient balance throws "Insufficient funds…" (or the new "No balance found…") with the chunked txContext suffix when chunked=true. The existing src/config/agoric/agoric.spec.tsx only renders tabs/forms; wiring wallet + queries + file upload to exercise these branches is heavy integration work and the harness pattern does not extend naturally. Branches 1 and 2 are already covered transitively through the installBundle spec.

We did not pursue these intentionally (each is sound on its own merits but out of scope for this round):

  • BigInt migration for cosmos uint64 amounts (pre-existing pattern across the repo).
  • Submit-button-disable race / in-flight guard (separate UX work).
  • await refetch() inside an async validateCost (speculative refactor).
  • Stale-closure-during-gzip mitigation (negligible window in practice).
  • JSDoc on validateCost (codebase prefers no comments; symmetric onProgress is undocumented).
  • Extracting the inline validateCost into a named helper (the file's onProgress callback at the same call site is comparably long; matches local convention).
  • Negative-cost / malicious-RPC hardening (UX-only; chain consensus is the security boundary).

Local validations on the branch pass: yarn tsc --noEmit clean, yarn lint clean modulo the three pre-existing react-refresh/only-export-components warnings in src/contexts/*.tsx, and yarn test:unit --run reports 23/23 passing across 6 files. Reviewers can pull the branch into a local checkout to evaluate the proposed edits.

kriscendobot pushed a commit to kriskowal/garden that referenced this pull request May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants