Skip to content

feat(payments): batch send — recipient selection, bulk import, custom mints#549

Open
multipletwigs wants to merge 9 commits into
mainfrom
bashtwigs/pro-1335-phase-8-batch-payments-fe-batch-payments-for-all-providers
Open

feat(payments): batch send — recipient selection, bulk import, custom mints#549
multipletwigs wants to merge 9 commits into
mainfrom
bashtwigs/pro-1335-phase-8-batch-payments-fe-batch-payments-for-all-providers

Conversation

@multipletwigs

Copy link
Copy Markdown
Collaborator

Summary

Phase 8 batch payments for all providers (PRO-1335). Adds an onchain batch-send flow to Payments v2: pick a source wallet + asset, choose recipients from a counterparty-grouped wallet table or bulk-import them by pasting rows, review, and send to many counterparties in one batch. Supports any SPL token by mint, not just well-known symbols.

API (sdp-api)

  • Crypto-recipients listingGET /v1/counterparties/crypto-recipients returns active Solana crypto_wallet accounts (counterparty name + address + label), with search and pagination. Adds an ids filter so a specific set of wallet ids can be resolved in one query (powers bulk import).
  • Counterparty account repogetCounterpartyAccountByIdInProject and listCounterpartyAccountsByIdsInProject, scoped by org + project.
  • Batch transfers — optional counterpartyIdrecipients[].counterpartyId is now optional. When omitted, the account is resolved within org/project scope and the counterparty is derived from the account row (authoritative, unspoofable). When supplied, the verified counterparty-scoped lookup is kept (defense in depth). This lets the paste flow send with just { counterpartyAccountId, amount }.

Web (sdp-web)

  • Batch send rail + recipients step — counterparty-grouped, expandable wallet table; source-wallet/asset selection with the available balance shown inline in the wallet trigger (shakes once + turns red when the total exceeds balance); single/batch send-mode toggle wired through the shared ramp wizard shell.
  • Bulk import dialog — Vercel-style paste/rows UI: paste counterparty_wallet_id, currency-or-mint, amount lines into any field to explode them into editable rows, or add rows manually. Resolves wallet ids to recipients, enforces one currency per batch, and surfaces per-row + unresolved errors.
  • Custom mint support — currency column accepts a well-known symbol or a raw base58 mint; resolves against the source wallet's balances.

Testing

  • tsc --noEmit clean (both apps), biome check clean on touched files.
  • sdp-api batch transfer suite green (7/7), including a new test for the derive-counterpartyId path.

Notes

  • Legacy send/receive is untouched; this is additive to the v2 ramp flow.
  • Batch idempotency remains out of scope (devnet demo).

Add the crypto_wallet recipient listing endpoint plus repo methods to
fetch counterparty accounts by id within an org/project scope, including
a batch by-ids variant used to resolve pasted wallet ids.
Make recipients.counterpartyId optional. When absent, resolve the
account within org/project scope and derive the counterparty from the
account row, keeping the verified counterparty-scoped lookup when an
id is supplied.
Add the batch-send rail and recipients step: a counterparty-grouped,
expandable wallet table, source-wallet/asset selection with an in-trigger
balance readout, a single/batch send-mode toggle, and the batch wizard
wiring through the shared ramp wizard shell.
Add a Vercel-style paste/rows dialog that parses
counterparty_wallet_id, currency-or-mint, amount lines into editable
rows, resolves wallet ids to recipients, and merges them into the batch.
@linear

linear Bot commented Jun 30, 2026

Copy link
Copy Markdown

PRO-1335

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sdp-docs Ready Ready Preview, Comment Jun 30, 2026 12:15pm
sdp-web Ready Ready Preview, Comment Jun 30, 2026 12:15pm

Request Review

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a full onchain batch-send flow to Payments v2: recipients step (counterparty-grouped table + bulk paste import), source wallet/asset selection with live balance validation, and a review/submit flow. It also introduces a new GET /v1/counterparties/accounts endpoint for Solana wallet listing with search/pagination, and collapses the previous N+1 recipient DB queries at batch-submission time into a single bulk lookup.

  • API (sdp-api): New listBatchRecipients repo method (with 4 test cases), a listCounterpartyAccountsByIdsInProject bulk lookup replacing per-recipient queries in resolveRecipients, and counterpartyId made optional on the batch recipient schema so paste-imported rows work without the caller supplying it.
  • Web (sdp-web): BatchSendRail + useBatchSendWizard hook handle multi-step wizard state; BulkImportDialog supports paste-to-rows with one-currency enforcement and server-side ID resolution; ramp-action-page gains a send-mode toggle wiring single vs batch flow.
  • Types (sdp-types): CounterpartyAccountSummary and ListProjectCounterpartyAccountsResponse shared across both apps.

Confidence Score: 4/5

Safe to merge after fixing MAX_BATCH_RECIPIENTS; the rest of the batch flow is correct.

MAX_BATCH_RECIPIENTS is set to 6 — identical to the page size — which prevents any batch from exceeding a single page of recipients. This directly breaks the multi-page selection UX and causes bulk import to reject 7+ rows with a misleading error. Everything else (bulk DB lookup replacing N+1 queries, ID-scoped counterparty resolution, wizard state management) is well-implemented and tested.

apps/sdp-web/src/app/dashboard/payments/ramps/schema.ts — the MAX_BATCH_RECIPIENTS constant needs to be corrected before the batch feature can handle more recipients than fit on one page.

Important Files Changed

Filename Overview
apps/sdp-web/src/app/dashboard/payments/ramps/schema.ts Adds batchSendSchema and MAX_BATCH_RECIPIENTS; the constant is incorrectly set to 6 (same as page size), limiting batches to a single page of recipients.
apps/sdp-api/src/db/repositories/counterparty-account.repository.postgres.ts Adds getCounterpartyAccountByIdInProject, listCounterpartyAccountsByIdsInProject, and listBatchRecipients; the bulk-lookup for batch submission omits a counterparty-active check that the picker enforces.
apps/sdp-api/src/routes/payments/handlers/transfer-batches.ts Replaces N+1 concurrent DB queries in resolveRecipients with a single bulk lookup using listCounterpartyAccountsByIdsInProject; logic is correct.
apps/sdp-web/src/app/dashboard/payments/ramps/hooks/use-batch-send-wizard.ts Core wizard hook for batch send; selections persist across pages, bulk import resolves IDs server-side, and all SWR keys are well-scoped.
apps/sdp-api/src/routes/counterparties/handlers.ts Adds listProjectCounterpartyAccounts handler; correctly overrides limit/offset when resolving by IDs and scopes queries to org/project.
apps/sdp-web/src/app/dashboard/payments/ramps/components/batch-send-step-content.tsx New Recipients/Review/Result UI; well-structured with correct exhaustive-switch for batch status, skeleton loading states, and proper animation keys.
apps/sdp-web/src/app/dashboard/payments/ramps/components/bulk-import-dialog.tsx Paste-to-rows bulk import dialog; handles multi-currency validation and surfaces unresolved IDs from server.
apps/sdp-api/src/db/repositories/counterparty-account.repository.test.ts Adds four test cases for listBatchRecipients covering base filtering, search, pagination, and the accountIds ID-resolution path including bind-order validation.
packages/sdp-types/src/payments.ts Adds CounterpartyAccountSummary, ListProjectCounterpartyAccountsResponse, and envelope types shared by API and web.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User
    participant BW as BatchSendWizard
    participant API as /api/dashboard
    participant SDP as sdp-api

    U->>BW: Select wallet + asset
    U->>BW: Toggle recipients (table) or open BulkImportDialog

    alt Table selection
        BW->>API: "GET /counterparty/accounts?page=&search="
        API->>SDP: GET /v1/counterparties/accounts
        SDP-->>API: ListProjectCounterpartyAccountsResponse
        API-->>BW: accounts[]
    else Bulk import (paste rows)
        U->>BW: Paste counterparty_wallet_id, currency, amount rows
        BW->>API: "GET /counterparty/accounts?ids=id1,id2,..."
        API->>SDP: "GET /v1/counterparties/accounts?ids=..."
        SDP-->>API: Resolved accounts[]
        API-->>BW: accounts[]
        BW->>BW: Map ids to BatchRecipientEntry
    end

    U->>BW: Click Review
    BW->>API: POST /payments/transfers/batch/estimate
    API->>SDP: POST /v1/payments/transfer-batches/estimate
    SDP->>SDP: listCounterpartyAccountsByIdsInProject (bulk)
    SDP-->>API: PaymentTransferBatchEstimate
    API-->>BW: fees + tx count

    U->>BW: Click Send batch
    BW->>API: POST /payments/transfers/batch
    API->>SDP: POST /v1/payments/transfer-batches
    SDP->>SDP: resolveRecipients (single bulk DB query)
    SDP-->>API: batch + recipients + transfers
    API-->>BW: CreateTransferBatchResult
    BW->>U: Show BatchResultView
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User
    participant BW as BatchSendWizard
    participant API as /api/dashboard
    participant SDP as sdp-api

    U->>BW: Select wallet + asset
    U->>BW: Toggle recipients (table) or open BulkImportDialog

    alt Table selection
        BW->>API: "GET /counterparty/accounts?page=&search="
        API->>SDP: GET /v1/counterparties/accounts
        SDP-->>API: ListProjectCounterpartyAccountsResponse
        API-->>BW: accounts[]
    else Bulk import (paste rows)
        U->>BW: Paste counterparty_wallet_id, currency, amount rows
        BW->>API: "GET /counterparty/accounts?ids=id1,id2,..."
        API->>SDP: "GET /v1/counterparties/accounts?ids=..."
        SDP-->>API: Resolved accounts[]
        API-->>BW: accounts[]
        BW->>BW: Map ids to BatchRecipientEntry
    end

    U->>BW: Click Review
    BW->>API: POST /payments/transfers/batch/estimate
    API->>SDP: POST /v1/payments/transfer-batches/estimate
    SDP->>SDP: listCounterpartyAccountsByIdsInProject (bulk)
    SDP-->>API: PaymentTransferBatchEstimate
    API-->>BW: fees + tx count

    U->>BW: Click Send batch
    BW->>API: POST /payments/transfers/batch
    API->>SDP: POST /v1/payments/transfer-batches
    SDP->>SDP: resolveRecipients (single bulk DB query)
    SDP-->>API: batch + recipients + transfers
    API-->>BW: CreateTransferBatchResult
    BW->>U: Show BatchResultView
Loading

Reviews (7): Last reviewed commit: "feat(sdp-web): receipt-style batch revie..." | Re-trigger Greptile

Comment thread apps/sdp-web/src/app/dashboard/payments/ramps/components/bulk-import-dialog.tsx Outdated
…endpoint

- resolveRecipients does a single listCounterpartyAccountsByIdsInProject
  lookup instead of 1-2 queries per recipient
- cover the accountIds IN-clause filter in listBatchRecipients tests
- rename crypto-recipients endpoint to /counterparties/accounts with a
  type=crypto_account query param
@multipletwigs

Copy link
Copy Markdown
Collaborator Author

@greptileai re-review please — addressed both findings:

  • resolveRecipients now does a single bulk listCounterpartyAccountsByIdsInProject lookup instead of 1–2 queries per recipient (no more N concurrent queries)
  • added accountIds IN-clause filter coverage to listBatchRecipients repo tests (incl. search+ids bind-order guard)

- bulkImport no longer relies on the silent pageSize override; fetchBatchRecipients
  treats page/pageSize as optional and id-resolution omits them
- guard bulk import against exceeding MAX_BATCH_RECIPIENTS with a clear message
  instead of silently disabling the submit button
- use positional keys for the bulk-import error list to avoid React key collisions
@multipletwigs

Copy link
Copy Markdown
Collaborator Author

Addressed all 3 P2 findings:

  • pageSize: 1 hackfetchBatchRecipients now treats page/pageSize as optional and id-resolution omits them, so the call-site no longer depends on the server's silent limit override (and avoids the pageSize > 100 validation trap a literal ids.length would hit).
  • >500 silent dead-endbulkImport now throws a clear A batch can have at most 500 recipients… message (surfaced in the dialog) instead of silently disabling Review; extracted MAX_BATCH_RECIPIENTS.
  • React key collision — bulk-import error list now uses positional keys.

@greptileai re-review please.

Replace the recipient table with a flat, divided list: row click toggles
selection, selected rows reveal an underline amount input with blur-to-deselect,
spacious search, always-shown x/n pagination, row-matched skeletons, and
selection/amount animations. Cap batches at 6 recipients.
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.

1 participant