feat: batch deposit to multiple pools from the dashboard - #232
Conversation
Members of several rotational pools had to visit each pool page and sign a separate deposit. This adds a dashboard-level batch deposit: one place to see everything owed this round, select what to pay, and run it all in one go. Design note — Soroban permits exactly one InvokeHostFunction operation per transaction, so N pool deposits cannot be fused into one atomic multi-operation transaction; they are necessarily N independent transactions. That resolves the tension in the issue between "atomic, all-or-nothing" and "retry only the failed pools": each deposit now succeeds or fails on its own, confirmed deposits stay confirmed, and only the failures are re-run. Signing is serialized through the existing tx-queue so the wallet shows one prompt at a time. - lib/batch-deposit.ts: selection totals, urgency banding, batching, progress - hooks/useBatchDeposit.ts: getPoolsRequiringDeposit / buildBatchDepositTx / submitBatchDeposit / retryFailed, with per-pool status reporting - components/dashboard/batch-deposit-panel.tsx: panel + selection dialog, select-all/deselect-all, live total, hidden when nothing is owed - components/dashboard/batch-deposit-progress.tsx: progress bar and per-pool pending/signing/submitted/confirmed/failed indicators - api/pools: `member=` returns every pool a wallet belongs to - useJointSaveContracts: submitTx exported as submitContractTx with an onPhase callback so the batch UI reuses the submit pipeline instead of duplicating it Transactions are queued in batches of 15 and the split is surfaced in the UI. Tests: 22 unit tests for the pure logic, 10 component tests driving the real panel through the real hook (asserting the built transactions invoke deposit(member) on the right contract), and an E2E spec covering the flow, the hidden state and a mobile viewport. Closes JointSave-org#214
|
@Fury03 kindly fix failing checks so i can review |
Sendi0011
left a comment
There was a problem hiding this comment.
Review — Request Changes
The batch deposit feature is excellently designed. The separation of pure logic (lib/batch-deposit.ts) from the network/wallet layer (hooks/useBatchDeposit.ts) is exactly right — the pure module has 262 lines of thorough unit tests covering selection totals, chunking, urgency banding, and progress calculation. The component is well-structured with proper dialog flow, per-pool progress, and retry semantics.
Blocking issues
1. Build is failing — the CI shows Build ❌. This is the primary reason for requesting changes. The pnpm build step must pass before this can be merged. Please check the Next.js build output for TypeScript or import errors. Common causes:
- Missing
loading-skeletonsexports if PR #233 conflicts - Circular imports between
batch-deposit.tsandhooks/useBatchDeposit.ts - Missing
Spinnercomponent import inbatch-deposit-progress.tsx
2. E2E is also failing — same root cause likely.
Non-blocking observations
-
submitContractTxrefactoring (useJointSaveContracts.ts) — renamingsubmitTxtosubmitContractTxand addingonPhasecallback is a clean API improvement. However, this change touches every hook that callssubmitTx(deposit, withdraw, trigger payout, etc.) — if there are merge conflicts with other PRs that modify these same call sites, they will be painful. Consider whether this refactor could be a separate PR to reduce blast radius. -
useBatchDeposit.ts:137—getPoolsRequiringDepositscans all member pools concurrently withSCAN_CONCURRENCY = 4. If a user is in 50+ pools, this fires ~50 RPC calls in waves of 4. Consider adding a timeout per pool or a total timeout for the scan. -
batch-deposit-panel.tsx:563— early return afteruseEffectcalls means the hook still runsrefresh()on mount even when the panel will render nothing. Minor perf concern. -
logDepositActivityuses a PATCH to/api/pools?id=...— verify the PATCH route handles theactivitybody shape, since errors are silently swallowed by try/catch.
Fix the build failures and this is ready to merge.
…operations # Conflicts: # frontend/app/api/pools/route.ts # frontend/hooks/useJointSaveContracts.ts # frontend/package.json
The E2E run caught a real integration bug that the component tests missed. `onDepositsComplete` fired as soon as the run finished. The dashboard reloads its pool list from that callback, and `MyGroups` returns a loading skeleton while that fetch is in flight — which unmounts BatchDepositPanel, destroying the dialog and the per-pool results the moment they appeared. The component tests rendered the panel standalone with no callback, so they never saw it. The parent is now notified when the dialog is dismissed, and only if something was actually deposited. Two regression tests cover both halves: the callback must not fire while the progress list is still on screen, and it must not fire at all when the dialog is closed without depositing. Also in this commit: - prettier on e2e/batch-deposit.spec.ts, which failed `pnpm format:check` - a 10s per-pool timeout on the state reads in getPoolsRequiringDeposit, so a wallet in dozens of pools can't be held behind one unresponsive RPC read (review feedback)
|
Thanks for the review. Conflicts are resolved (merged 1. The failing check wasn't the buildThe Next.js build never ran.
On the three causes you suggested: no circular import exists ( 2. E2E was a different, real bug — and a good catchNot the same root cause. Two of the three batch specs passed; only the full-run spec failed, at the point where the progress list should appear.
Fixed: the parent is notified when the dialog is dismissed, and only if something was actually deposited. Two regression tests now cover both halves — the callback must not fire while the progress list is still on screen, and must not fire at all when the dialog is closed without depositing. 3.
|
Sendi0011
left a comment
There was a problem hiding this comment.
Review: Batch Deposit System — APPROVED + MERGING
Excellent work. Clean hook architecture, proper chunking with retry logic, and comprehensive stats UI.
Architecture
useBatchDeposithook — Manages chunked deposits with configurable chunk size, retry logic, and abort supportuseBatchDepositStatshook — Fetches and aggregates deposit statisticsBatchDepositPanelcomponent — Full UI with progress tracking, chunk status, and retry controlsbatch-deposits/page.tsx— Dedicated page with stats cards + deposit panel
Code Quality
useLocalStoragefor persisting deposit preferences- AbortController integration for cancellation support
useCallback/useMemoproperly used to prevent unnecessary re-renders- Error handling with proper
ErrorInfotype and chunk-level error tracking
Previous Feedback Addressed
Author updated and addressed previously requested changes. All 5 CI checks now pass.
Non-blocking Notes
- Hardcoded
COUNTER_CONTRACT_IDandPOOL_ADMIN_SECRETinbatch-actions.ts— should be env vars (follows existing patterns) - Batch size cap of 200 members is reasonable but undocumented in the UI
Verdict
Production-ready batch deposit system. All 5 CI checks pass. Merging.
Closes #214
Members of several rotational pools had to open each pool page and sign a separate deposit. This adds a dashboard-level batch deposit: one place to see everything owed this round, pick what to pay, and run it all in one go.
The design decision behind this PR
The issue asked for "a single Stellar transaction with multiple
invoke_contractoperations". That isn't achievable on Soroban: a transaction may carry exactly oneInvokeHostFunctionoperation — two contract calls in one transaction is rejected as malformed. Multi-operation batching is a classic-Stellar (payment-style) capability that does not extend to contract invocations. Without an on-chain batching contract (explicitly out of scope), N pool deposits are necessarily N transactions.That constraint resolves a contradiction in the acceptance criteria rather than creating one. The issue asked for both "if one operation fails the entire transaction fails (atomicity)" and "retry only the failed pools" — these cannot both hold under one atomic transaction. Under N independent transactions they both make sense, and that is what's implemented: each deposit succeeds or fails on its own, confirmed deposits stay confirmed, and retry re-runs only the failures.
Signing is serialized through the app's existing
lib/tx-queue.ts, so the wallet shows one prompt at a time instead of N racing popups.What's in it
lib/batch-deposit.tshooks/useBatchDeposit.tsgetPoolsRequiringDeposit/buildBatchDepositTx/submitBatchDeposit/retryFailed, with per-pool statuscomponents/dashboard/batch-deposit-panel.tsxcomponents/dashboard/batch-deposit-progress.tsxapp/api/pools/route.tsmember=branch — every pool a wallet belongs to (created or joined)hooks/useJointSaveContracts.tssubmitTxexported assubmitContractTxwith anonPhasecallbackThe
submitContractTxchange is a rename + export + optionalonPhaseparam, so the batch UI reuses the existing simulate → assemble → sign → send → poll pipeline instead of duplicating it. It also drops akitparameter that was accepted but never used. All 18 existing call sites were updated; behaviour is unchanged.Acceptance criteria
batch-deposit-panel.tsx, rendered frommy-groups.tsxsummarizeSelection/formatBatchSummary, test "totals the selection…"deposit(member)on the right contract idbatch-deposit-progress.tsx— pending / signing / submitted / confirmed / failedretryFailed(), test "retries only the failed pools" asserts exactly one extra transactionchunk(…, MAX_TX_PER_BATCH)+ split notice, test "splits a selection larger than 15 pools…"sm:breakpoints; E2E asserts no horizontal overflow at 390pxPools that are paused, inactive, or that the wallet isn't actually an on-chain member of are excluded from the scan — a deposit to any of those is a guaranteed on-chain failure.
Verification
The component tests drive the real
BatchDepositPanelthrough the realuseBatchDeposithook — only/api/poolsand the contract layer (the repo's existing global mock) are stubbed. The transaction-building test decodes the actual built envelopes and asserts each one carries a singleinvokeHostFunctionoperation callingdeposit(member)on that pool's contract.e2e/batch-deposit.spec.tscovers the full browser flow, the hidden state, and a mobile viewport. It could not be executed locally — this environment is missing chromium's shared libraries (libnspr4.so) and has no network access to install them, so no Playwright spec runs here, including the repo's existingnavigation.spec.ts. It will run in CI.