Skip to content

feat: add batch payment integration for settling multiple USDC invoices via Spraay#1730

Open
plagtech wants to merge 3 commits into
RequestNetwork:masterfrom
plagtech:feat/spraay-batch-payments
Open

feat: add batch payment integration for settling multiple USDC invoices via Spraay#1730
plagtech wants to merge 3 commits into
RequestNetwork:masterfrom
plagtech:feat/spraay-batch-payments

Conversation

@plagtech
Copy link
Copy Markdown

@plagtech plagtech commented Jun 1, 2026

What

Adds batch payment support for settling multiple Request Network invoices in a single on-chain transaction, powered by Spraay Protocol's multi-recipient batch transfer contracts.

Related issue: #1729

Why

Businesses using Request Network for invoicing often need to pay multiple invoices at once — monthly vendor payments, contractor payroll, DAO grants, etc. Today this requires N separate transactions. Batch payment reduces this to 1 transaction with ~80% gas savings.

What's Included

New files:

  • packages/payment-processor/src/payment/spraay-batch-payer.ts — Core batch payment logic
  • packages/payment-processor/src/payment/spraay-utils.ts — Chain configs, contract addresses, helpers
  • packages/payment-processor/test/spraay-batch-payer.test.ts — Unit tests
  • docs/integrations/spraay-batch-payments.md — Integration guide

Changes:

  • packages/payment-processor/src/index.ts — Export new batch payment module
  • README.md — Add Spraay batch payments to integrations section

How It Works

Request Network Invoices
    │
    ▼
SpraayBatchPayer.payPendingInvoices()
    │
    ├── Fetches pending USDC invoices for payer address
    ├── Validates recipients & amounts
    ├── Checks USDC balance
    ├── Approves Spraay batch contract (if needed)
    └── Calls batchTransfer() — single transaction
    │
    ▼
All invoices settled atomically

Usage

import { SpraayBatchPayer } from "@requestnetwork/payment-processor";

const spraay = new SpraayBatchPayer(signer, 8453); // Base

// Auto-discover and pay all pending USDC invoices
const result = await spraay.payPendingInvoices(requestClient, payerAddress);
console.log(`Paid ${result.recipientCount} invoices: ${result.explorerUrl}`);

Supported Chains

Base, Ethereum, Arbitrum, Polygon, BNB Chain, Avalanche

Gas Comparison

Invoices Individual Batched (Spraay) Savings
5 ~$0.05 ~$0.02 60%
10 ~$0.10 ~$0.02 80%
30 ~$0.30 ~$0.03 90%

(Base L2 gas estimates)

Testing

cd packages/payment-processor
yarn test --grep "SpraayBatchPayer"

Checklist

  • Follows existing payment-processor patterns
  • TypeScript strict mode compliant
  • Unit tests included
  • Documentation added
  • No breaking changes to existing APIs
  • Works with existing @requestnetwork/request-client.js

References

@MantisClone
Copy link
Copy Markdown
Contributor

Hello @plagtech, thank you for submitting your first pull request to the requestNetwork repository. We value your contribution and encourage you to review our contribution guidelines to ensure your submission meets our standards. Please note that every merged PR is automatically enrolled in our Best PR Initiative, offering a chance to win $500 each quarter. Our team is available via GitHub Discussions or Discord if you have any questions. Welcome aboard!

@MantisClone
Copy link
Copy Markdown
Contributor

Thank you for your submission! As you prepare for the review process, please ensure that your PR title, description, and any linked issues fully comply with our contribution guidelines. A clear explanation of your changes and their context will help expedite the review process. Every merged PR is automatically entered into our Best PR Initiative, offering a chance to win $500 every quarter. We appreciate your attention to detail and look forward to reviewing your contribution!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR adds SpraayBatchPayer, a new class that batches multiple Request Network USDC invoice payments into a single on-chain batchTransfer call via Spraay Protocol's contracts, targeting Base, Ethereum, Arbitrum, Polygon, BNB Chain, and Avalanche.

  • Core payer (spraay-batch-payer.ts): validates invoices, checks balance, approves the Spraay contract, executes batchTransfer, and returns a structured result with per-invoice status; payPendingInvoices auto-discovers pending USDC invoices from a Request Network client.
  • Utilities (spraay-utils.ts): hardcoded Spraay contract addresses, USDC addresses, explorer URLs, chain names, and minimal ABIs for the six supported chains.
  • Tests (spraay-batch-payer.test.ts): unit tests for validation paths, missing-payee skipping, and cross-chain USDC filtering; no live-node tests for the actual transfer flow.

Confidence Score: 3/5

Not safe to merge without resolving the chainId/signer mismatch: passing a chainId override in BatchPaymentRequest routes all contract lookups to that chain's addresses while the signer's actual network is ignored, silently making RPC calls to the wrong contracts.

The BatchPaymentRequest.chainId field lets callers override the chain used for contract address lookups, but payInvoices never validates that the signer's provider is actually on that chain. A signer connected to Base combined with chainId: 1 will query and transact against Ethereum contract addresses on Base — wrong decimals(), wrong balanceOf, and an approve/batchTransfer sent to whatever contract happens to live at those addresses on Base. Beyond that, the existing unaddressed thread comment about batchTransfer bypassing Request Network's payment-detection events means invoices will remain in 'created' state after payment, which is a fundamental correctness issue for the integration.

packages/payment-processor/src/payment/spraay-batch-payer.ts — the chainId override in payInvoices and the batchTransfer payment-detection bypass

Important Files Changed

Filename Overview
packages/payment-processor/src/payment/spraay-batch-payer.ts Core batch payment class; contains a P1 chain/signer mismatch issue where request.chainId can silently route all contract calls to wrong addresses, and a dead-code "failed" status in PaymentSettlement
packages/payment-processor/src/payment/spraay-utils.ts Contract addresses, USDC addresses, explorer URLs, and ABIs — looks correct and well-organized
packages/payment-processor/test/spraay-batch-payer.test.ts Unit tests covering validation paths and invoice filtering; no test for the chainId override mismatch scenario
docs/integrations/spraay-batch-payments.md Integration documentation; accurate description of usage, supported chains, and gas savings

Sequence Diagram

sequenceDiagram
    participant Caller
    participant SpraayBatchPayer
    participant ERC20_USDC
    participant SpraayContract

    Caller->>SpraayBatchPayer: payInvoices(request)
    SpraayBatchPayer->>SpraayBatchPayer: validate chainId, addresses, invoice count
    SpraayBatchPayer->>ERC20_USDC: decimals()
    ERC20_USDC-->>SpraayBatchPayer: 6
    SpraayBatchPayer->>SpraayBatchPayer: parse amounts, compute total
    SpraayBatchPayer->>ERC20_USDC: balanceOf(sender)
    ERC20_USDC-->>SpraayBatchPayer: balance
    SpraayBatchPayer->>ERC20_USDC: allowance(sender, spraayContract)
    ERC20_USDC-->>SpraayBatchPayer: allowance
    alt "allowance < total"
        SpraayBatchPayer->>ERC20_USDC: approve(spraayContract, total)
        ERC20_USDC-->>SpraayBatchPayer: receipt (null check + status check)
    end
    SpraayBatchPayer->>SpraayContract: batchTransfer(token, recipients[], amounts[])
    SpraayContract->>ERC20_USDC: transferFrom(sender, recipient_i, amount_i) x N
    SpraayContract-->>SpraayBatchPayer: tx receipt (null check + status check)
    SpraayBatchPayer-->>Caller: BatchPaymentResult
Loading

Reviews (3): Last reviewed commit: "fix: throw on reverted batch transfer in..." | Re-trigger Greptile

Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts Outdated
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts Outdated
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts Outdated
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts
…, scope USDC to active chain, type request client
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts
Comment thread packages/payment-processor/src/payment/spraay-batch-payer.ts
Comment on lines +144 to +146
const chainId = request.chainId ?? this.chainId;
const batchContractAddr = SPRAAY_BATCH_CONTRACTS[chainId];
const tokenAddr = request.tokenAddress ?? USDC_ADDRESSES[chainId];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 request.chainId override silently uses wrong chain's contracts

BatchPaymentRequest.chainId lets callers override the chain used to look up contract addresses, but the signer is never validated against that chain. If a caller constructs new SpraayBatchPayer(signer, 8453) with a Base-connected signer and then passes chainId: 1, every RPC call — token.decimals(), token.balanceOf(), token.allowance(), the approve, and batchTransfer — is sent through the Base signer to the Ethereum-chain contract addresses on the Base network. The decimals() call may return garbage or revert (the Ethereum USDC address is a different contract on Base), and if it happens to return a value, the approve and batch-transfer will go to the wrong address entirely. The chainId field in BatchPaymentRequest should either be removed (the signer's network is the authoritative source) or validated at the start of payInvoices via await this.signer.provider?.getNetwork() with an explicit mismatch error.

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.

2 participants