M2. Deposits and sweeps¶
Goal. The user funds their Privy member wallet with mainnet USDC. The backend sweeps that USDC into the group treasury, credits the position on confirmed treasury arrival, and never credits on member-wallet balance alone. Sweeps are idempotent on transaction signature.
Depends on. M1 complete.
Owns. Deposit handlers, internal/worker poller, app.ObserveSweep, position updates on confirmed sweep, relayer use on sweep txs, related Postgres migrations, thin mobile deposit status UI. Pyth prices, votes, and redeem payout stay out.
Structure¶
apps/backend/
internal/app/
deposit.go CreateDeposit, ObserveSweep
internal/worker/
sweep_poller.go member USDC poll → Privy SubmitSweep → ObserveSweep
internal/postgres/
deposits.go signature-keyed upsert on deposits.tx_signature
positions.go share_units + amount_deposited
internal/httpapi/
deposits.go POST deposit, GET deposit status
Claim-unit minting at marked pot NAV is M4. M2 only increments position columns on sweep confirm.
Flow¶
- Mobile
POST /v1/groups/{id}/deposits { amount }. Insertdepositsrow (pending,from_address= member wallet). - Worker polls member wallet USDC via Privy + mainnet RPC. No Privy webhooks.
- When balance covers intent, Privy client
SubmitSweep. Relayer pays SOL. - RPC confirms sweep. Worker builds
ObservedSweep(signature, from, to group treasury, amount). app.ObserveSweepin one Postgres transaction:- If
deposits.tx_signaturealready equals this signature, return prior status (no double credit). - Set
deposits.tx_signature, status confirmed. - Increment
positions.amount_depositedandpositions.share_unitsby swept USDC (same number in M2). - USDC sitting only in the member wallet never calls step 5. Zero position change.
GET /v1/deposits/{id}returns status and credited share units when done.
Data models¶
Product tables for M2. One family with M3 and M4. No sweep_tx_log or share_ledger.
- deposits.
user_id,group_id,amount,from_address(member wallet),status,tx_signature(unique idempotency key when sweep confirms),created_at. A deposit row is user funding a group: sweep member wallet to treasury. Credit share units only after confirmed treasury arrival. - positions.
user_id,group_id,share_units,amount_deposited(net USDC in for P and L),amount_withdrawn(default 0 until M4 redeem). Primary key(user_id, group_id). Share units are internal claim tickets, not dollar IOUs. In M2 the pot is USDC only, soshare_unitsandamount_depositedincrease by the same swept USDC amount. - withdrawals.
user_id,group_id,amount,to_address(proven payout pubkey),status,tx_signature(unique when paid),created_at. Schema in the M2 migration. Write path ships in M4 redeem. M2 may persist deposits and positions only. - users. M1 table. Add columns in M2 only if needed. Nothing secret.
M4 adjusts share_units on deposit when the treasury holds marked xStock so a later member does not capture unrealized gain. That math is not part of M2.
percent returnuses net USDC in: deposits credited minus withdrawals paid (withdrawals stay zero in M2)
USDC mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. Poll. Do not use Privy production webhooks.
Parallelization¶
Milestone gate: complete Wave 5 before M3.
| Wave | Tracks (run in parallel within the wave) | Gate for next wave |
|---|---|---|
| 1 | Schema M2-T1 · Types M2-T2 · Logging M2-T12 | M1 |
| 2 | Deposit API M2-T3 · Poller M2-T4 · Relayer on sweep M2-T6 | Wave 1 |
| 3 | Sweep submit M2-T5 · Parent credit M2-T8 | M2-T4; M2-T5 for T8 |
| 4 | Read APIs M2-T11 · Mobile deposit UI M2-T13 | M2-T8 |
| 5 | Unit tests M2-T14, M2-T15 · Integration tests M2-T16, M2-T17 · just wiring M2-T18 | Wave 4 |
Sweep pipeline is sequential inside Wave 3: poller (T4) → sweep submit (T5) → credit (T8). Backend read APIs and mobile UI are parallel in Wave 4.
Tickets¶
- M2-T1 Add the migration for deposits, positions, and withdrawals schema.
- M2-T2 Define domain types for deposit, position, and sweep credit at the API boundary.
- M2-T3 Implement
POSTdeposit keyed by user, group, and amount.
Depends on: M2-T1, M2-T2 - M2-T4 Implement the member-wallet USDC balance poller using Privy and mainnet RPC.
Depends on: M2-T1 - M2-T5 Implement the server-signed USDC sweep from member wallet to group treasury via Privy.
Depends on: M2-T4 - M2-T6 Wire the app relayer as SOL fee payer on sweep transactions.
Depends on: M1-T8 - Parent M2-T8 Credit position share units on confirmed treasury credit only, idempotent on deposit signature.
Depends on: M2-T5, M2-T6 - └ Subissue of M2-T8 M2-T7 Confirm the sweep on-chain and set
tx_signatureon the deposit row with unique upsert - └ Subissue of M2-T8 M2-T9 On confirmed sweep, increment
amount_depositedandshare_unitsby swept USDC (1:1 in M2) - └ Subissue of M2-T8 M2-T10 Reject share credit when USDC sits only in the member wallet
- M2-T11 Add GET endpoints for deposit status, member share units, and treasury USDC balance.
Depends on: M2-T8 - M2-T12 Add structured logs for sweep attempt, confirmation, and share credit.
- M2-T13 Add a thin mobile deposit screen to create a deposit and poll status.
Depends on: M2-T3, M2-T11 - M2-T14 Add unit tests that credited
share_unitsandamount_depositedequal swept USDC.
Depends on: M2-T8 - M2-T15 Add unit tests that multiple deposits sum
share_unitsandamount_depositedcorrectly.
Depends on: M2-T8 - M2-T16 Add an integration test that a duplicate sweep signature does not double-credit shares.
Depends on: M2-T8 - M2-T17 Add an integration test that member-wallet-only USDC creates zero position share units.
Depends on: M2-T10 - M2-T18 Wire
just test backendto cover deposit and sweep tests against local Docker Postgres.
Depends on: M2-T14, M2-T15, M2-T16, M2-T17
Ticket details¶
M2-T1: Add migration for deposits, positions, and withdrawals schema¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T1 is wave 1 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Problem
SQL tables/columns for M2-T1 are missing from supabase/migrations/; migration runner cannot apply them.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
supabase/migrations/. No drive-by refactors. - Add next sequential SQL file under
supabase/migrations/with tables/columns fromdocs/milestones/m2-deposits.md. - Run migration via existing runner (
apps/backend/internal/postgres/). Do not hand-apply in prod. - Satisfy milestone gate:
just test backendand/orjust test mobileexit 0 with no new failing tests.
API / data contract
No HTTP. SQL migration only. Apply via migration runner invoked by API boot and just test backend.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
supabase/migrations/
Out of scope (do not touch) - Jupiter swaps - buy proposals - votes - redeem payout - leaderboards - Pyth marks
Acceptance Criteria
- [ ] Files and symbols named in Proposal exist under supabase/migrations/.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Files and symbols named in Proposal exist under supabase/migrations/.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - None - Wave: 1
M2-T2: Define domain types for deposit, position, and sweep credit at API boundary¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T2 is wave 1 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Problem
Behavior for Define domain types for deposit, position, and sweep credit at API boundary is not implemented under apps/backend/internal/app/deposit.go.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit.go. No drive-by refactors. - Satisfy milestone gate:
just test backendand/orjust test mobileexit 0 with no new failing tests.
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit.go
Out of scope (do not touch) - Worker loop - HTTP handlers
Acceptance Criteria
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - None - Wave: 1
M2-T3: Implement POST deposit keyed by user, group, and amount¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T3 is wave 2 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T1, M2-T2.
Problem
Behavior for Implement POST deposit keyed by user, group, and amount is not implemented under apps/backend/internal/httpapi/deposits.go, apps/backend/internal/app/deposit.go.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/httpapi/deposits.go,apps/backend/internal/app/deposit.go. No drive-by refactors. - Add
TestPOST_deposit_createsPendingDepositRowin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestPOST_deposit_missingAuth_returns401in the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestPOST_deposit_nonMember_returns403in the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestPOST_deposit_zeroOrNegativeAmount_returns400in the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/httpapi/deposits.goapps/backend/internal/app/deposit.go
Out of scope (do not touch) - Sweep poller - Share credit
Acceptance Criteria
- [ ] Test TestPOST_deposit_createsPendingDepositRow exists, uses AAA comments, and passes.
- [ ] Test TestPOST_deposit_missingAuth_returns401 exists, uses AAA comments, and passes.
- [ ] Test TestPOST_deposit_nonMember_returns403 exists, uses AAA comments, and passes.
- [ ] Test TestPOST_deposit_zeroOrNegativeAmount_returns400 exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/httpapi/deposits.go, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestPOST_deposit_createsPendingDepositRow exists, uses AAA comments, and passes.
- [ ] Test TestPOST_deposit_missingAuth_returns401 exists, uses AAA comments, and passes.
- [ ] Test TestPOST_deposit_nonMember_returns403 exists, uses AAA comments, and passes.
- [ ] Test TestPOST_deposit_zeroOrNegativeAmount_returns400 exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/httpapi/deposits.go, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T1 — do not start while open - M2-T2 — do not start while open - Wave: 2
M2-T4: Implement member-wallet USDC balance poller using Privy and mainnet RPC¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T4 is wave 2 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T1.
Problem
Behavior for Implement member-wallet USDC balance poller using Privy and mainnet RPC is not implemented under apps/backend/internal/worker/sweep_poller.go.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/worker/sweep_poller.go. No drive-by refactors. - Add
TestSweepPoller_memberBalanceCoversIntent_triggersSubmitSweepin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestSweepPoller_memberBalanceBelowIntent_doesNotSweepin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/worker/sweep_poller.go
Out of scope (do not touch) - Share credit - Jupiter
Acceptance Criteria
- [ ] Test TestSweepPoller_memberBalanceCoversIntent_triggersSubmitSweep exists, uses AAA comments, and passes.
- [ ] Test TestSweepPoller_memberBalanceBelowIntent_doesNotSweep exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/worker/sweep_poller.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestSweepPoller_memberBalanceCoversIntent_triggersSubmitSweep exists, uses AAA comments, and passes.
- [ ] Test TestSweepPoller_memberBalanceBelowIntent_doesNotSweep exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/worker/sweep_poller.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T1 — do not start while open - Wave: 2
M2-T5: Implement server-signed USDC sweep from member wallet to group treasury via Privy¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T5 is wave 3 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T4.
Problem
Behavior for Implement server-signed USDC sweep from member wallet to group treasury via Privy is not implemented under apps/backend/internal/privy/sweep.go, apps/backend/internal/worker/.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/privy/sweep.go,apps/backend/internal/worker/. No drive-by refactors. - Add
TestSubmitSweep_callsPrivyClientWithMemberAndTreasuryAddressesin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/privy/sweep.goapps/backend/internal/worker/
Out of scope (do not touch) - Position credit
Acceptance Criteria
- [ ] Test TestSubmitSweep_callsPrivyClientWithMemberAndTreasuryAddresses exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/privy/sweep.go, apps/backend/internal/worker/.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestSubmitSweep_callsPrivyClientWithMemberAndTreasuryAddresses exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/privy/sweep.go, apps/backend/internal/worker/.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T4 — do not start while open - Wave: 3
M2-T6: Wire app relayer as SOL fee payer on sweep transactions¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T6 is wave 2 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M1-T8.
Problem
Behavior for Wire app relayer as SOL fee payer on sweep transactions is not implemented under apps/backend/internal/privy/sweep.go.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/privy/sweep.go. No drive-by refactors. - Add
TestSubmitSweep_includesRelayerAsFeePayerin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/privy/sweep.go
Out of scope (do not touch) - Jupiter fee payer
Acceptance Criteria
- [ ] Test TestSubmitSweep_includesRelayerAsFeePayer exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/privy/sweep.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestSubmitSweep_includesRelayerAsFeePayer exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/privy/sweep.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M1-T8 — do not start while open - Wave: 2
M2-T8: Credit position share units on confirmed treasury credit only, idempotent on deposit signature¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T8 is wave 3 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Parent ticket; subissues: M2-T7, M2-T9, M2-T10.
Blocked until dependencies land: M2-T5, M2-T6.
Problem
Parent M2-T8 orchestration in apps/backend/internal/app/deposit.go, apps/backend/internal/postgres/deposits.go, apps/backend/internal/postgres/positions.go missing; subissues M2-T7, M2-T9, M2-T10 not wired together.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit.go,apps/backend/internal/postgres/deposits.go,apps/backend/internal/postgres/positions.go. No drive-by refactors. - Parent glue: wire subissues M2-T7, M2-T9, M2-T10 into
apps/backend/internal/app/deposit.goafter each subissue lands. - Add
TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEquallyin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
Parent orchestration. Public contract is union of subissue routes/symbols: M2-T7, M2-T9, M2-T10.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit.goapps/backend/internal/postgres/deposits.goapps/backend/internal/postgres/positions.go
Out of scope (do not touch) - Marked NAV minting (M4)
Acceptance Criteria
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit.go, apps/backend/internal/postgres/deposits.go, apps/backend/internal/postgres/positions.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] All subissues (M2-T7, M2-T9, M2-T10) closed before parent closes.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit.go, apps/backend/internal/postgres/deposits.go, apps/backend/internal/postgres/positions.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] All subissues (M2-T7, M2-T9, M2-T10) closed before parent closes.
- [ ] Subissues closed: M2-T7, M2-T9, M2-T10.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T5 — do not start while open - M2-T6 — do not start while open - Wave: 3 - Subissue: M2-T7 - Subissue: M2-T9 - Subissue: M2-T10
M2-T7: Confirm sweep on-chain and set tx_signature on deposit row with unique upsert¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T7 is wave 3 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Subissue of M2-T8.
Blocked until dependencies land: M2-T5.
Problem
Subissue M2-T7 of M2-T8: symbols under apps/backend/internal/postgres/deposits.go, apps/backend/internal/app/deposit.go not implemented; parent cannot close.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/postgres/deposits.go,apps/backend/internal/app/deposit.go. No drive-by refactors. - Subissue of
M2-T8. Do not add routes or flows owned by parent/sibling subissues. - Add
TestObserveSweep_confirmedTreasuryArrival_setsDepositStatusConfirmedin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new public route. Subissue of M2-T8. Internal symbols only in Owns paths.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/postgres/deposits.goapps/backend/internal/app/deposit.go
Out of scope (do not touch) - Share unit increment logic (M2-T9)
Acceptance Criteria
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_setsDepositStatusConfirmed exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/postgres/deposits.go, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_setsDepositStatusConfirmed exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/postgres/deposits.go, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T5 — do not start while open - Wave: 3 - Parent: M2-T8
M2-T9: On confirmed sweep increment amount_deposited and share_units by swept USDC (1:1 in M2)¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T9 is wave 3 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Subissue of M2-T8.
Blocked until dependencies land: M2-T7.
Problem
Subissue M2-T9 of M2-T8: symbols under apps/backend/internal/postgres/positions.go, apps/backend/internal/app/deposit.go not implemented; parent cannot close.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/postgres/positions.go,apps/backend/internal/app/deposit.go. No drive-by refactors. - Subissue of
M2-T8. Do not add routes or flows owned by parent/sibling subissues. - Add
TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEquallyin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDepositedin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new public route. Subissue of M2-T8. Internal symbols only in Owns paths.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/postgres/positions.goapps/backend/internal/app/deposit.go
Out of scope (do not touch) - Marked pot ratio (M4)
Acceptance Criteria
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually exists, uses AAA comments, and passes.
- [ ] Test TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDeposited exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/postgres/positions.go, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually exists, uses AAA comments, and passes.
- [ ] Test TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDeposited exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/postgres/positions.go, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T7 — do not start while open - Wave: 3 - Parent: M2-T8
M2-T10: Reject share credit when USDC sits only in the member wallet¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T10 is wave 3 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Subissue of M2-T8.
Blocked until dependencies land: M2-T8.
Problem
Subissue M2-T10 of M2-T8: symbols under apps/backend/internal/app/deposit.go not implemented; parent cannot close.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit.go. No drive-by refactors. - Subissue of
M2-T8. Do not add routes or flows owned by parent/sibling subissues. - Add
TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPositionin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new public route. Subissue of M2-T8. Internal symbols only in Owns paths.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit.go
Out of scope (do not touch) - Jupiter - Votes
Acceptance Criteria
- [ ] Test TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPosition exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPosition exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T8 — do not start while open - Wave: 3 - Parent: M2-T8
M2-T11: Add GET endpoints for deposit status, member share units, and treasury USDC balance¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T11 is wave 4 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T8.
Problem
Behavior for Add GET endpoints for deposit status, member share units, and treasury USDC balance is not implemented under apps/backend/internal/httpapi/deposits.go.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/httpapi/deposits.go. No drive-by refactors. - Add
TestGET_depositStatus_returnsCreditedShareUnitsWhenConfirmedin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestGET_memberShareUnits_reflectsPositionAfterSweepin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestGET_treasuryUsdcBalance_returnsPostSweepAmountin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/httpapi/deposits.go
Out of scope (do not touch) - xStock balances
Acceptance Criteria
- [ ] Test TestGET_depositStatus_returnsCreditedShareUnitsWhenConfirmed exists, uses AAA comments, and passes.
- [ ] Test TestGET_memberShareUnits_reflectsPositionAfterSweep exists, uses AAA comments, and passes.
- [ ] Test TestGET_treasuryUsdcBalance_returnsPostSweepAmount exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/httpapi/deposits.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestGET_depositStatus_returnsCreditedShareUnitsWhenConfirmed exists, uses AAA comments, and passes.
- [ ] Test TestGET_memberShareUnits_reflectsPositionAfterSweep exists, uses AAA comments, and passes.
- [ ] Test TestGET_treasuryUsdcBalance_returnsPostSweepAmount exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/httpapi/deposits.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T8 — do not start while open - Wave: 4
M2-T12: Add structured logs for sweep attempt, confirmation, and share credit¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T12 is wave 1 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Problem
Behavior for Add structured logs for sweep attempt, confirmation, and share credit is not implemented under apps/backend/internal/worker/, apps/backend/internal/app/deposit.go.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/worker/,apps/backend/internal/app/deposit.go. No drive-by refactors. - Add structured log lines at attempt/confirm/credit (or quote/execute/poll) transitions using existing logger; fields:
group_id,user_id,deposit_id/tx_signature/symbolas applicable. - Satisfy milestone gate:
just test backendand/orjust test mobileexit 0 with no new failing tests.
API / data contract
No new HTTP surface. Internal Go/Swift symbols in Owns paths only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/worker/apps/backend/internal/app/deposit.go
Out of scope (do not touch) - Metrics stack - Privy webhooks
Acceptance Criteria
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/worker/, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/worker/, apps/backend/internal/app/deposit.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - None - Wave: 1
M2-T13: Add thin mobile deposit screen to create deposit and poll status¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T13 is wave 4 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T3, M2-T11.
Problem
SwiftUI flow in apps/mobile/Features/Deposit/ is missing or still scaffold; product/API wiring for Add thin mobile deposit screen to create deposit and poll status not shipped.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/mobile/Features/Deposit/. No drive-by refactors. - Build SwiftUI screen in Owns; call
MonacoAPIClientonly (no xStocks/Jupiter/Pyth/Solana RPC). - Compile gate:
just build mobileandjust test mobileexit 0. - Manual (after automated green): Fund member wallet with mainnet USDC; Confirm sweep in explorer
API / data contract
Mobile consumes existing backend JSON only. No direct calls to xStocks, Jupiter, Pyth, or Solana RPC from Swift. Use MonacoAPIClient methods matching backend routes in milestone doc.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
- MockURLProtocol for API client unit tests
Scope¶
apps/mobile/Features/Deposit/
Out of scope (do not touch) - Product deposit copy (M5)
Acceptance Criteria
- [ ] Files and symbols named in Proposal exist under apps/mobile/Features/Deposit/.
- [ ] just test mobile exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test mobile
Manual (only for this ticket): - Fund member wallet with mainnet USDC - Confirm sweep in explorer
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Files and symbols named in Proposal exist under apps/mobile/Features/Deposit/.
- [ ] just test mobile exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T3 — do not start while open - M2-T11 — do not start while open - Wave: 4
M2-T14: Add unit tests that credited share_units and amount_deposited equal swept USDC¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T14 is wave 5 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T8.
Problem
Locked test names for M2-T14 are absent or not wired into just test recipes; apps/backend/internal/app/deposit_test.go incomplete.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit_test.go. No drive-by refactors. - Add
TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEquallyin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestProperty_sweptUsdcEqualsShareUnitsAndAmountDepositedin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new API. Tests and/or Justfile wiring only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit_test.go
Out of scope (do not touch) - Integration duplicate-signature test (M2-T16)
Acceptance Criteria
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually exists, uses AAA comments, and passes.
- [ ] Test TestProperty_sweptUsdcEqualsShareUnitsAndAmountDeposited exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually exists, uses AAA comments, and passes.
- [ ] Test TestProperty_sweptUsdcEqualsShareUnitsAndAmountDeposited exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T8 — do not start while open - Wave: 5
M2-T15: Add unit tests that multiple deposits sum share_units and amount_deposited correctly¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T15 is wave 5 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T8.
Problem
Locked test names for M2-T15 are absent or not wired into just test recipes; apps/backend/internal/app/deposit_test.go incomplete.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit_test.go. No drive-by refactors. - Add
TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDepositedin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestProperty_multipleDepositsPreserveOneToOneInvariantin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new API. Tests and/or Justfile wiring only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit_test.go
Out of scope (do not touch) - Mobile tests
Acceptance Criteria
- [ ] Test TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDeposited exists, uses AAA comments, and passes.
- [ ] Test TestProperty_multipleDepositsPreserveOneToOneInvariant exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDeposited exists, uses AAA comments, and passes.
- [ ] Test TestProperty_multipleDepositsPreserveOneToOneInvariant exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T8 — do not start while open - Wave: 5
M2-T16: Add integration test that duplicate sweep signature does not double-credit shares¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T16 is wave 5 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T8.
Problem
Locked test names for M2-T16 are absent or not wired into just test recipes; apps/backend/internal/app/deposit_integration_test.go incomplete.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit_integration_test.go. No drive-by refactors. - Add
TestObserveSweep_duplicateSignature_doesNotDoubleCreditin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestObserveSweep_concurrentDuplicateSignature_creditsOnceOnlyin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert). - Add
TestProperty_observeSweepIdempotentin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new API. Tests and/or Justfile wiring only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit_integration_test.go
Out of scope (do not touch) - Jupiter idempotency
Acceptance Criteria
- [ ] Test TestObserveSweep_duplicateSignature_doesNotDoubleCredit exists, uses AAA comments, and passes.
- [ ] Test TestObserveSweep_concurrentDuplicateSignature_creditsOnceOnly exists, uses AAA comments, and passes.
- [ ] Test TestProperty_observeSweepIdempotent exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_integration_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_duplicateSignature_doesNotDoubleCredit exists, uses AAA comments, and passes.
- [ ] Test TestObserveSweep_concurrentDuplicateSignature_creditsOnceOnly exists, uses AAA comments, and passes.
- [ ] Test TestProperty_observeSweepIdempotent exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_integration_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T8 — do not start while open - Wave: 5
M2-T17: Add integration test that member-wallet-only USDC creates zero position share units¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T17 is wave 5 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T10.
Problem
Locked test names for M2-T17 are absent or not wired into just test recipes; apps/backend/internal/app/deposit_integration_test.go incomplete.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
apps/backend/internal/app/deposit_integration_test.go. No drive-by refactors. - Add
TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPositionin the*_test.goor Swift test target under Owns; use AAA comments (// Arrange,// Act,// Assert).
API / data contract
No new API. Tests and/or Justfile wiring only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
apps/backend/internal/app/deposit_integration_test.go
Out of scope (do not touch) - Treasury-only USDC without deposit row
Acceptance Criteria
- [ ] Test TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPosition exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_integration_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Test TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPosition exists, uses AAA comments, and passes.
- [ ] Files and symbols named in Proposal exist under apps/backend/internal/app/deposit_integration_test.go.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T10 — do not start while open - Wave: 5
M2-T18: Wire just test backend to cover deposit and sweep tests against local Docker Postgres¶
Context M2 follows M1 wallets. Deposit → sweep → 1:1 share credit must work before Jupiter buys (M3).
Ticket M2-T18 is wave 5 in docs/milestones/m2-deposits.md under milestone M2 — Deposits.
Blocked until dependencies land: M2-T14, M2-T15, M2-T16, M2-T17.
Problem
Locked test names for M2-T18 are absent or not wired into just test recipes; Justfile incomplete.
Proposal Implement in order. No drive-by refactors. Lock choices below; do not invent product policy.
- Edit only Owns paths:
Justfile. No drive-by refactors. - Update root
Justfilerecipes so locked tests run underjust test backendand/orjust test mobile. Exit 0 required. - Satisfy milestone gate:
just test backendand/orjust test mobileexit 0 with no new failing tests.
API / data contract
No new API. Tests and/or Justfile wiring only.
Test fakes (locked names):
- fakePrivyClient (MemberUSDCBalance, SubmitSweep)
- fakeSolanaRPC
- stubClock
- buildObservedSweep(overrides)
- integrationApp(t)
Scope¶
Justfile
Out of scope (do not touch) - just test mobile
Acceptance Criteria
- [ ] Files and symbols named in Proposal exist under Justfile.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
Verification
Baseline (run before marking done):
- just test backend
Done when (zero wiggle room):
- [ ] Every Acceptance Criteria checkbox is satisfied.
- [ ] Files and symbols named in Proposal exist under Justfile.
- [ ] just test backend exits 0 with no new failures.
- [ ] Out of scope paths untouched; no unrelated refactors.
- [ ] No drive-by edits outside Scope; Out of scope untouched.
Dependencies - M2-T14 — do not start while open - M2-T15 — do not start while open - M2-T16 — do not start while open - M2-T17 — do not start while open - Wave: 5
Automated verification¶
Commands. just test backend.
Test layers¶
| Layer | Runs under | What it proves |
|---|---|---|
| Go unit | just test backend |
app.ObserveSweep, CreateDeposit validation, poller decision logic with fake Privy, fake RPC, stub clock — no Postgres, no network. |
| Go integration | just test backend |
Full deposit → sweep confirm → position credit against local Docker Postgres; idempotency and member-wallet-only negative path. |
| Concurrency | just test backend |
Two goroutines call ObserveSweep with the same signature; expect one credit (M2-T16). |
Tests assert literal share_units, amount_deposited, and deposits.tx_signature row counts. AAA comments on every test.
Test map¶
M2-T3 — POST deposit (integration)
TestPOST_deposit_createsPendingDepositRowTestPOST_deposit_missingAuth_returns401TestPOST_deposit_nonMember_returns403TestPOST_deposit_zeroOrNegativeAmount_returns400
M2-T4 / M2-T5 — poller and sweep (unit)
TestSweepPoller_memberBalanceCoversIntent_triggersSubmitSweepTestSweepPoller_memberBalanceBelowIntent_doesNotSweepTestSubmitSweep_callsPrivyClientWithMemberAndTreasuryAddresses
M2-T8 — ObserveSweep credit (unit + integration) — parent M2-T7, M2-T9, M2-T10
TestObserveSweep_confirmedTreasuryArrival_incrementsShareUnitsAndAmountDepositedEqually(M2-T9, 1:1 in M2)TestObserveSweep_confirmedTreasuryArrival_setsDepositStatusConfirmed(M2-T7)TestObserveSweep_duplicateSignature_doesNotDoubleCredit(M2-T16, idempotency)TestObserveSweep_memberWalletOnlyBalance_doesNotCreditPosition(M2-T10)TestObserveSweep_multipleDeposits_sumsShareUnitsAndAmountDeposited(M2-T15)TestObserveSweep_concurrentDuplicateSignature_creditsOnceOnly(race)
M2-T11 — read APIs (integration)
TestGET_depositStatus_returnsCreditedShareUnitsWhenConfirmedTestGET_memberShareUnits_reflectsPositionAfterSweepTestGET_treasuryUsdcBalance_returnsPostSweepAmount
M2-T6 — relayer on sweep (unit)
TestSubmitSweep_includesRelayerAsFeePayer
Product invariants (must pass)
share_unitsincrease equalsamount_depositedincrease equals swept USDC micros (M2 has no marked NAV).- Credit only after confirmed treasury arrival, never on member-wallet balance alone.
- Idempotent on
deposits.tx_signature. - No Jupiter, votes, or redeem payout in M2.
Fixtures / fakes¶
fakePrivyClient—MemberUSDCBalance,SubmitSweepreturns deterministic signatures.fakeSolanaRPC— confirm/not-found for sweep signatures.stubClock— poller tick control.buildDeposit(overrides),buildPosition(overrides),buildObservedSweep(overrides).integrationApp(t)with fake Privy/RPC; real Postgres.
Testability note. ObserveSweep must accept injected store and run inside InTx so unit tests can use an in-memory or test DB without the worker loop.
Property / invariant tests¶
Use testing/quick or pgregory.net/rapid (Go) in packages/domain or internal/app tests:
TestProperty_sweptUsdcEqualsShareUnitsAndAmountDeposited— for any positive swept USDC in M2, creditedshare_units==amount_deposited== swept amount.TestProperty_observeSweepIdempotent— applying the sameObservedSweepN times leaves position unchanged after the first credit.TestProperty_multipleDepositsPreserveOneToOneInvariant— sum of credits equals sum of swept amounts.
Manual verification¶
Mainnet USDC, Privy funding, explorer, and log watching only. Row counts and 1:1 credit are automated above.
- Sign in on mobile. Fund the member Privy wallet with a small mainnet USDC amount, onramp or external transfer.
- In the Privy dashboard, note the member wallet address and USDC balance before deposit.
- Create a deposit for a test group and submit.
- Watch API logs for poller detection, sweep build, and relayer-signed submit.
- Open the Solana explorer for the sweep signature. Confirm USDC moved to the group treasury.
- Confirm treasury USDC rose and member wallet USDC fell by the swept amount in the Privy dashboard or explorer.
Out of scope¶
Jupiter swaps, buy proposals, votes, redeem payout, leaderboards, P and L boards, token price marks, Privy production webhooks, user-facing gas or seed phrase flows. Withdrawal rows are not written until M4.
Open decisions¶
None for M2.