feat(payments): batch send — recipient selection, bulk import, custom mints#549
Conversation
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.
Greptile SummaryThis 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
Confidence Score: 4/5Safe to merge after fixing
Important Files Changed
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
%%{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
Reviews (7): Last reviewed commit: "feat(sdp-web): receipt-style batch revie..." | Re-trigger Greptile |
…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
|
@greptileai re-review please — addressed both findings:
|
- 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
|
Addressed all 3 P2 findings:
@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.
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)GET /v1/counterparties/crypto-recipientsreturns active Solanacrypto_walletaccounts (counterparty name + address + label), with search and pagination. Adds anidsfilter so a specific set of wallet ids can be resolved in one query (powers bulk import).getCounterpartyAccountByIdInProjectandlistCounterpartyAccountsByIdsInProject, scoped by org + project.counterpartyId—recipients[].counterpartyIdis 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)counterparty_wallet_id, currency-or-mint, amountlines 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.Testing
tsc --noEmitclean (both apps),biome checkclean on touched files.sdp-apibatch transfer suite green (7/7), including a new test for the derive-counterpartyId path.Notes