Production-ready multi-chain batch sender: Base Account + EIP-5792, testable core modules, security & UI polish - #1
Open
arena-ai-coding-agent[bot] wants to merge 6 commits into
Open
arena-ai-coding-agent[bot] wants to merge 6 commits into
arena-ai-coding-agent[bot] wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…and docs - Stateless BatchSender.sol (disperseEther/disperseToken) — reviewed version of the contract uploaded to main: identical code, plus NatSpec documenting that it has no owner, no upgrade path, and no withdraw function - Multi-chain registry in public/chains.json (Base, Ethereum, Optimism, Arbitrum, BSC, Avalanche, Polygon PoS, LitVM LiteForge) with per-chain fallback RPC URLs; ABI sanity-checked against the contract - Extract pure logic from main.js into src/core/ (parse, validate, distribute, debounce, errors, rpc, sendCalls) with 62 Vitest unit tests - npm run check:chains drift guard keeps chains.json in sync with the AppKit network list - AUDIT.md (security review) and ARCHITECTURE.md (design notes) - Rename gitignore -> .gitignore; drop dead public/indexold.html - Vite code-splitting + host allowlist for the preview proxy; package-lock.json re-synced so npm ci works (it failed against the previously committed lock file) Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…zy Reown, UI polish Base Account (Base App passkey smart wallet) support: - Dedicated 'Sign in with Base Account' connect button; Base Account featured first in the Reown wallet modal (official wallet ID from docs.base.org) - EIP-5792 dispatch: when the connected wallet advertises wallet_sendCalls (detected via wallet_getCapabilities), the batch is submitted atomically to the smart wallet and tracked via wallet_getCallsStatus to a terminal state; standard gas-estimated signer path remains as fallback - ERC20 dispersements send value=0x0 in the smart-wallet batch (disperseToken is non-payable); only ETH batches attach the ETH value Architecture & robustness: - Reown AppKit lazy-loaded after first paint (requestIdleCallback) — main bundle ~36 kB raw / ~11 kB gzip; the ~400 kB-gzip Reown chunk is deferred - Centralized error handling with ABI revert-reason decoding (src/core/errors.js); read-only RPC fallback provider for balance/allowance reads — signing always uses the connected wallet - Debounce + generation-token on the async summary/allowance path so switching chains/tokens can no longer apply stale results - Gas-estimation failure is fatal (no more manual-gas broadcast that would guarantee a revert); MAX_RECIPIENTS=500 safety cap - Burn/dead-address (zero + ...dead) confirmation gate: dispatch stays disabled until the user explicitly acknowledges UI/UX: - Consistent SVG icon system for notifications, section headers, and stepper checks (emojis removed); ARIA live region for notifications - vercel.json: single redirects array — the Farcaster /.well-known/farcaster.json manifest redirect was silently dropped by a duplicate JSON key Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
arena-ai-coding-agent
Bot
force-pushed
the
arena/01a019cb-farsend
branch
from
September 20, 2026 18:41
c33d2a3 to
ddf67dd
Compare
…dispatch Base Account is a registered Reown wallet, so it connects through the standard AppKit modal — a second connect surface was redundant. It stays featured first in the wallet (featuredWalletIds) and the EIP-5792 wallet_sendCalls dispatch is unaffected, so smart-wallet users still get atomic batch submission with wallet-side gas handling. - Drop baseAccountBtn markup, handleBaseAccountConnect(), and its listener - README/ARCHITECTURE now document the single-modal connect flow Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
… hex fixes, RPC failover, hardening Blockers (verified against the final EIP-5792 spec): 1. Double-send eliminated. The old dispatcher fell through to a second signer.sendTransaction on any non-rejection error — including after the batch was already with the wallet (poll timeout was misread as failure). Now: once wallet_sendCalls RESOLVES the batch is never resubmitted; poll timeout => 'pending in your wallet, do not resubmit'; FAILED/UNKNOWN => surface, no auto-retry. Fallback to the signer path only when classifySendCallsError() proves nothing was submitted (method-not-found, invalid-params, bundle-too-large, etc. — the case the spec's Backwards-Compatibility section sanctions). Decision table unit-tested. 2. Spec shapes corrected. wallet_getCapabilities now takes [account, [chainIdHex]] and checks atomic.status 'supported'|'ready' (per-chain or 0x0 global; legacy draft booleans tolerated; the hardcoded 0x2105 cross-chain fallback is removed). wallet_sendCalls sends an app-provided id (wallet must echo it) and reads result.id (legacy batchId tolerated). wallet_getCallsStatus uses the numeric status codes (1xx/2xx/4xx/5xx/6xx; legacy strings tolerated); 5730 => UNKNOWN. 3. chains.json chainIdHex fixes (Base 0x2141 -> 0x2105, Avalanche 0xa882 -> 0xa86a). check-chains.mjs now asserts parseInt(chainIdHex,16) === chainId so this drift class fails CI (negative-tested). Serious items: 4. Burn gate re-checked inside handleDispatch (defense in depth alongside MAX_RECIPIENTS), not only via the disabled button. 5. Token symbol XSS: symbol() output is contract-controlled and was interpolated raw into innerHTML in two places; both now use escapeHtml (unit-tested). 6. package-lock.json restored — reproducible npm ci for a fund-moving app. 7. 'Fallback RPC' is now a real fallback: createFallbackProvider returns the full ordered provider list and readWithFallback fails over across endpoints (ethers v6 takes a single URL per provider, so failover is explicit and unit-tested, including the no-fallback-after-rejection rule). 8. burnTotal sums decimal strings exactly (scaled BigInt) — the burn warning can no longer show float artifacts. 9. Docs made honest: README/ARCHITECTURE no longer claim CI runs on GitHub (workflow written but held locally — the automation account lacks the workflows permission); AUDIT.md marks debounce done and records this audit round with its verification. Tests: 62 -> 91 (spec-shaped capability detection, batch-id generation, request/response shaping, numeric status mapping, 5730/timeout handling, full classifySendCallsError decision table, RPC failover order, escapeHtml, exact burn totals). node --check / check:chains / build all clean. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Follow-ups from the re-audit (not merge blockers, cheap to close): - EIP-5792 status 200 with an empty receipts array is still success: clear the form and do not re-enable Dispatch on a live batch. Message tells the user to check their wallet. (Previously the success UI was skipped, so a second click could re-send.) - showNotification no longer interpolates the message into innerHTML. The toast body is textContent; the only HTML is our trusted SVG icons plus an optional explorer <a> built from txExplorerHref (https explorer URL + a 32-byte hex hash). Token symbols in toasts are therefore escaped by construction. - ARCHITECTURE.md 'Bottom line' updated — it still claimed 'no tests, and no CI' after both were added. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Turns FarSend into a production-grade, multi-chain batch sender that works with regular EOAs, Farcaster Mini App context, and Base Account (the passkey ERC-4337 smart wallet powering the Base App) — all through the same deployed, immutable
BatchSendercontracts, so no existing contracts need redeploying.Response to external audit
The audit's verdict ("do not merge until the 5792 layer is spec-correct and cannot double-send") was correct. Every blocker was reproduced locally, verified against the final EIP-5792 spec, and fixed in
986e915:wallet_sendCalls(non-rejection errors — incl. poll timeout — fell through to a secondsendTransaction)wallet_sendCallsresolves the batch is never resubmitted: poll timeout → "pending in your wallet, do not resubmit"; FAILED/UNKNOWN → surfaced, no auto-retry. Fallback to the signer path only whenclassifySendCallsError()proves nothing was submitted (method-not-found, invalid-params, bundle-too-large… — exactly the case the spec's Backwards-Compatibility section sanctions). Full decision table unit-tested, incl. "never fallback once submitted".sendCalls/atomicBatchflags; hardcoded0x2105fallback)wallet_getCapabilities([account, [chainIdHex]]), checksatomic.status === 'supported' | 'ready'(per-chain or0x0global; explicit per-chain wins; legacy draft booleans tolerated; the0x2105cross-chain fallback is removed — unit-tested).chainIdHexinchains.json(Base0x2141, Avalanche0xa882)0x2105,0xa86a) andcheck-chains.mjsnow assertsparseInt(chainIdHex,16) === chainId— negative-tested to fail on the old values.package-lock.jsondeleted986e915) — reproduciblenpm cifor a fund-moving app. Note: the deletion (20f1f5f) was a manual commit on this branch; restore is per the audit's recommendation — easy to revert if intentional.urls[0]only)createFallbackProviderreturns the full ordered provider list;readWithFallbackfails over across endpoints (unit-tested: order, stop-on-success, last-error-when-all-down, rejection never swallowed).workflowspermission); AUDIT.md marks debounce done and records this audit round.handleDispatchdidn't re-check the burn gatefindBurnRecipients+burnConfirmedinside the handler, alongsideMAX_RECIPIENTS.Plus two pre-existing items fixed while in there: token
symbol()(contract-controlled) was interpolated raw intoinnerHTMLin two places — now escaped (escapeHtml, unit-tested); and the burn-warning total is now an exact scaled-BigInt decimal sum (no float artifacts in the security panel).Still open / out of scope (unchanged from prior notes): CI not running on GitHub (permission), guaranteed gas sponsorship (needs app registration/paymaster), dead
webhookUrl(skipped by request), runtime-fetchedchains.jsoncanary,SafeERC20(would require contract redeploy — intentionally avoided),parseFloatin non-security display paths.Commits
Key changes
Base Account / Base App smart wallet support
wallet_getCapabilities([account, [chainId]])→ atomicsupported|ready; batch submitted with an app-providedidviawallet_sendCalls; status tracked viawallet_getCallsStatusnumeric codes to a terminal state; the no-double-send invariant is enforced and unit-testedvalue=0x0in the batch (non-payabledisperseToken); only ETH batches attach the ETH valueArchitecture (pure core modules, all unit-tested)
src/core/:parse(+escapeHtml),validate(burn detection, exact totals),distribute,debounce,errors,rpc(multi-endpoint failover),sendCalls(EIP-5792 + error-classification decision table)chains.jsonin sync with AppKit networks and validateschainIdHex(npm run check:chains)Security & safety
BatchSender.sol(no owner/upgrade/withdraw); ABI mirrored inchains.jsonMAX_RECIPIENTS = 500; contract-derived strings escaped before any HTML interpolationAUDIT.md(active, both audit rounds recorded) +ARCHITECTURE.mddesign notesMulti-chain config
chainIdHexvalues verified against numeric IDs by the drift guardBatchSender.sol+ onechains.jsonentry; old deployments untouchedUI/UX & infra polish
vercel.jsonduplicateredirectskey fixed (Farcaster manifest redirect was silently dropped)Verification
node --checkclean on all modulesnpm test→ 91/91 passing (incl. the no-double-send decision table, spec-shaped 5792 tests, RPC failover order, hex-drift guard negative test)npm run check:chains→ OK; negative-tested to reject the old wrong hex valuesnpm run build→ cleanNotes / follow-ups (intentionally out of scope)
.github/workflows/ci.ymlready locally, not on GitHub (GitHub App lacksworkflowspermission). It should usenpm ci(lock file restored),check:chains(now with hex assertion), and addnode --checkfor allsrc/core/*modules.capabilities.paymasterServicecan be passed later).webhookUrlin the Farcaster manifest left as-is per request.