Skip to content

Feat/distributor client batch distribute - #592

Open
Fayedamz wants to merge 4 commits into
Fundable-Protocol:mainfrom
Fayedamz:feat/distributor-client-batch-distribute
Open

Feat/distributor client batch distribute#592
Fayedamz wants to merge 4 commits into
Fundable-Protocol:mainfrom
Fayedamz:feat/distributor-client-batch-distribute

Conversation

@Fayedamz

@Fayedamz Fayedamz commented Jul 30, 2026

Copy link
Copy Markdown

Summary

Closes #194. Adds a batchDistribute() method directly on DistributorClient so callers have a single, ergonomic entry point for large distributions without needing to import the lower-level prepareBatch* utilities manually.

What changed

DistributorClient.ts

Imported BatchDistributionConfig, BatchDistributionResult, prepareBatchEqualDistribution, and prepareBatchWeightedDistribution from utils/batchDistribution
Added two typed overload signatures for batchDistribute():
Equal distribution — accepts { sender, token, total_amount, recipients, config? }, dispatches to prepareBatchEqualDistribution
Weighted distribution — accepts { sender, token, recipients, amounts, config? }, dispatches to prepareBatchWeightedDistribution
Dispatch logic is a single "amounts" in params check — no duplication of batching logic, which lives entirely in the existing utility module
Both overloads are fully JSDoc'd with usage examples
DistributorClient.test.ts

Added 22 new tests in a batchDistribute describe block:

Equal distribution: single batch, multi-batch splitting, default batch size (100), total_amount passed unchanged to every batch, empty recipients rejection, invalid maxRecipientsPerBatch (0, -1), onBatchStart/onBatchComplete callbacks, error propagation from distributeEqual, no amountBatches in result.

Weighted distribution: single batch, parallel recipient/amount splitting, correct slices passed to distributeWeighted, length mismatch rejection, empty recipients rejection, non-integer batch size rejection, error propagation from distributeWeighted, progress callbacks, Address object inputs.

Design notes

The existing prepareBatchEqualDistribution and prepareBatchWeightedDistribution functions in batchDistribution.ts were not modified. batchDistribute is purely a dispatch layer — all batching logic, validation, and callback handling remains in the utility module, keeping concerns separated and avoiding duplication.

Testing

No runtime environment was available in this workspace, but all files pass TypeScript diagnostics with zero errors. Tests follow the established vitest + vi.mock patterns used throughout the SDK test suite and can be verified by running pnpm test in packages/sdk/.

closes #194

Summary by CodeRabbit

  • New Features

    • Added batched token distribution for large recipient lists, supporting equal and weighted allocations.
    • Added an emergency pause control for payment streams, allowing administrators to temporarily halt key stream operations.
    • Added validation for Stellar token contract IDs, while continuing to support native XLM tokens.
  • Bug Fixes

    • Invalid token addresses are now blocked before payment stream creation.

Fayedamz added 4 commits July 29, 2026 17:54
- Add validateContractId() to stream-validation.ts using StrKey.isValidContract
- Apply contract ID validation to token field in paymentStreamSchema (validations.ts)
- Guard token contract address in CreatePaymentStream handleFormSubmit
- Native XLM token ('native') is correctly exempted from contract ID check

Closes Fundable-Protocol#379
Replace all relative path traversals into packages/sdk/src with the
workspace alias @fundable/sdk across apps/web:

- apps/web/src/lib/api.ts: consolidate PaymentStreamClient,
  DistributorClient and createBatches into single @fundable/sdk import;
  remove mid-file stray import
- apps/web/src/hooks/use-distribute.ts: createBatches from @fundable/sdk
- apps/web/src/hooks/use-distribution-transaction.ts: DistributorClient
  from @fundable/sdk

Relative traversals (../../../../packages/sdk/src/...) bypass pnpm
workspace resolution and cause failures in standalone Next.js builds.

Closes Fundable-Protocol#368
…-stream

Add a global emergency pause switch to the Soroban payment stream contract
to halt all payouts and stream creation if a vulnerability is detected.

Contract changes (contracts/payment-stream/src/lib.rs):
- Add Error variants ContractPaused(17), AlreadyPaused(18), NotPaused(19)
- Add EmergencyPausedEvent and EmergencyUnpausedEvent contracttypes
- Add private assert_not_paused() guard called at the top of create_stream,
  deposit, withdraw and withdraw_max
- Add emergency_pause(env) – admin-only, sets 'paused' flag in instance
  storage, emits EmergencyPausedEvent, rejects if already paused
- Add emergency_unpause(env) – admin-only, clears 'paused' flag, emits
  EmergencyUnpausedEvent, rejects if not currently paused
- Add is_paused(env) -> bool – read-only, returns current circuit-breaker state
- All three new functions are fully doc-commented
- Storage uses instance slot with TTL bump, zero extra persistent footprint

Test changes (contracts/payment-stream/src/test.rs):
- setup_paused_contract() helper to reduce boilerplate across 16 new tests
- test_is_paused_default_false
- test_emergency_pause_sets_flag
- test_emergency_unpause_clears_flag
- test_emergency_pause_emits_event
- test_emergency_unpause_emits_event
- test_emergency_pause_already_paused (#should_panic Fundable-Protocol#18)
- test_emergency_unpause_when_not_paused (#should_panic Fundable-Protocol#19)
- test_create_stream_blocked_when_paused (#should_panic Fundable-Protocol#17)
- test_withdraw_blocked_when_paused (#should_panic Fundable-Protocol#17)
- test_withdraw_max_blocked_when_paused (#should_panic Fundable-Protocol#17)
- test_deposit_blocked_when_paused (#should_panic Fundable-Protocol#17)
- test_operations_resume_after_unpause
- test_pause_unpause_cycle (3 cycles)
- test_non_admin_cannot_emergency_pause (#should_panic Unauthorized)
- test_non_admin_cannot_emergency_unpause (#should_panic Unauthorized)
- test_read_operations_work_while_paused

Closes Fundable-Protocol#503
Add a single batchDistribute() method on DistributorClient that acts as
a convenient entry point for large-scale distributions. Callers no longer
need to import prepareBatchEqualDistribution / prepareBatchWeightedDistribution
directly from the batchDistribution utils module.

DistributorClient.ts:
- Import BatchDistributionConfig, BatchDistributionResult,
  prepareBatchEqualDistribution, prepareBatchWeightedDistribution from
  utils/batchDistribution
- Add two typed overload signatures for batchDistribute():
    1. Equal distribution: { sender, token, total_amount, recipients, config? }
    2. Weighted distribution: { sender, token, recipients, amounts, config? }
- Implementation dispatches to the correct prepare* helper by checking
  whether 'amounts' is present in the params object
- Full JSDoc on both overloads with usage examples

__tests__/DistributorClient.test.ts:
- 22 new tests in the batchDistribute describe block
- Equal distribution cases: single batch, multi-batch splitting, default
  batch size, total_amount unchanged across batches, empty recipients,
  invalid maxRecipientsPerBatch (0, -1), progress callbacks, error
  propagation, no amountBatches in result
- Weighted distribution cases: single batch, parallel recipient/amount
  splitting, correct slices passed to distributeWeighted, length mismatch
  rejection, empty recipients, invalid batch size, error propagation,
  progress callbacks, Address object inputs

Closes Fundable-Protocol#194
@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@Fayedamz Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Web validation and SDK wiring

Layer / File(s) Summary
Token contract validation
apps/web/src/lib/stream-validation.ts, apps/web/src/lib/validations.ts, apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
Adds Stellar contract ID validation and applies it to payment-stream schema validation and form submission.
Packaged SDK imports
apps/web/src/hooks/*, apps/web/src/lib/api.ts
Switches distribution clients and batching helpers from local SDK source paths to @fundable/sdk.

Payment-stream emergency circuit breaker

Layer / File(s) Summary
Pause controls and mutation guards
contracts/payment-stream/src/lib.rs
Adds admin pause/unpause methods, pause status reads, events, errors, TTL updates, and guards for stream creation, deposits, and withdrawals.
Pause behavior verification
contracts/payment-stream/src/test.rs
Tests pause state, authorization, events, blocked mutations, resumed operations, repeated cycles, and read-only behavior.

SDK batch distribution

Layer / File(s) Summary
Batch distribution API
packages/sdk/src/DistributorClient.ts
Adds equal and weighted batchDistribute overloads that delegate to batch preparation helpers.
Batch distribution verification
packages/sdk/src/__tests__/DistributorClient.test.ts
Tests batching, validation, callbacks, error propagation, slicing, and address inputs for both distribution modes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant PaymentStreamContract
  participant StreamStorage
  Admin->>PaymentStreamContract: emergency_pause()
  PaymentStreamContract->>StreamStorage: store paused flag and extend TTL
  PaymentStreamContract-->>Admin: publish EmergencyPaused event
  Admin->>PaymentStreamContract: create_stream(), deposit(), or withdraw()
  PaymentStreamContract->>StreamStorage: read paused flag
  PaymentStreamContract-->>Admin: return ContractPaused error
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: sofeel

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated payment-stream validation and emergency pause contract changes beyond the batch-distribution SDK utility. Split the unrelated web and contract changes into separate PRs and keep this one focused on DistributorClient.batchDistribute and its tests.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: adding batch distribution to the distributor client.
Linked Issues check ✅ Passed The PR adds DistributorClient.batchDistribute with typed overloads and transaction splitting, matching issue #194's goal.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/sdk/src/DistributorClient.ts

Parsing error: Cannot read file '/tsconfig.json'.

packages/sdk/src/__tests__/DistributorClient.test.ts

Parsing error: Cannot read file '/tsconfig.json'.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/sdk/src/__tests__/DistributorClient.test.ts (1)

508-519: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't actually pin the default maxRecipientsPerBatch to 100.

With 50 recipients and any default ≥ 50, batchCount would still be 1 — this test would pass even if the default were silently changed to, say, 60. Use a recipient count > 100 (e.g., 150) and assert batchCount === 2 with batch sizes [100, 50] to actually verify the documented default of 100.

♻️ Proposed strengthening
-      it("uses default batch size (100) when config is omitted", async () => {
-        const recipients = makeRecipients(50);
+      it("uses default batch size (100) when config is omitted", async () => {
+        const recipients = makeRecipients(150);
         const result = await client.batchDistribute({
           sender: SENDER,
           token: TOKEN,
-          total_amount: 50000n,
+          total_amount: 150000n,
           recipients,
         });

-        expect(result.batchCount).toBe(1);
-        expect(mockContractClient.distribute_equal).toHaveBeenCalledTimes(1);
+        expect(result.batchCount).toBe(2);
+        expect(result.recipientBatches[0]).toHaveLength(100);
+        expect(result.recipientBatches[1]).toHaveLength(50);
+        expect(mockContractClient.distribute_equal).toHaveBeenCalledTimes(2);
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/src/__tests__/DistributorClient.test.ts` around lines 508 - 519,
Strengthen the “uses default batch size (100) when config is omitted” test by
generating 150 recipients, asserting batchCount is 2, and verifying the
distribute_equal calls use batch sizes of 100 and 50. Keep the configuration
omitted so the test directly pins the default maxRecipientsPerBatch to 100.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 205-209: Correct the pause documentation near the global emergency
pause declaration to enumerate the actual blocked and permitted entry points,
including that cancel_stream, pause_stream, resume_stream, set_delegate, and
revoke_delegate remain callable unless their guards are changed. Keep the
documented behavior aligned with the existing assert_not_paused coverage rather
than claiming only admin operations remain available.

In `@contracts/payment-stream/src/test.rs`:
- Around line 1909-1934: Update test_emergency_pause_emits_event and
test_emergency_unpause_emits_event to import EmergencyPausedEvent and
EmergencyUnpausedEvent from the crate root, then assert the emitted event’s
topic and payload match the corresponding event type and expected values.
Replace the non-specific events.len() > 0 checks while preserving each test’s
existing setup and contract calls.

In `@packages/sdk/src/DistributorClient.ts`:
- Around line 344-354: Update the discriminator in the batch distribution method
around prepareBatchWeightedDistribution to require that params.amounts is an
array, rather than only checking for the amounts key. This must route undefined
or otherwise invalid amounts values to the existing validation path while
preserving weighted distribution for valid arrays.

---

Nitpick comments:
In `@packages/sdk/src/__tests__/DistributorClient.test.ts`:
- Around line 508-519: Strengthen the “uses default batch size (100) when config
is omitted” test by generating 150 recipients, asserting batchCount is 2, and
verifying the distribute_equal calls use batch sizes of 100 and 50. Keep the
configuration omitted so the test directly pins the default
maxRecipientsPerBatch to 100.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ff571eba-9af3-4d96-ac23-7a3895733983

📥 Commits

Reviewing files that changed from the base of the PR and between 375c936 and af86c0f.

📒 Files selected for processing (10)
  • apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
  • apps/web/src/hooks/use-distribute.ts
  • apps/web/src/hooks/use-distribution-transaction.ts
  • apps/web/src/lib/api.ts
  • apps/web/src/lib/stream-validation.ts
  • apps/web/src/lib/validations.ts
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs
  • packages/sdk/src/DistributorClient.ts
  • packages/sdk/src/__tests__/DistributorClient.test.ts

Comment on lines +205 to +209
/// Activate the global emergency pause switch.
///
/// When active, all calls to `create_stream`, `deposit`, `withdraw`, and
/// `withdraw_max` will be rejected with `Error::ContractPaused`.
/// Admin-only operations (fee management, pause/unpause) remain available.

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Doc claims contradict the actual guard scope; cancel_stream still moves funds while paused.

The doc says only admin operations remain available during a pause, but cancel_stream (Line 809) is caller-facing, is not guarded by assert_not_paused, and transfers escrowed tokens out of the contract (Line 845-849). pause_stream, resume_stream, set_delegate, and revoke_delegate also remain callable. Either add the guard to the fund-moving paths you intend to halt, or correct the doc to state exactly which entry points are blocked.

🔧 Option: guard cancel_stream and fix the doc
     /// When active, all calls to `create_stream`, `deposit`, `withdraw`, and
-    /// `withdraw_max` will be rejected with `Error::ContractPaused`.
-    /// Admin-only operations (fee management, pause/unpause) remain available.
+    /// `withdraw_max`, `cancel_stream` will be rejected with
+    /// `Error::ContractPaused`. Stream lifecycle controls (`pause_stream`,
+    /// `resume_stream`), delegation management, and read-only accessors remain
+    /// available, as do admin operations (fee management, pause/unpause).
// in cancel_stream
pub fn cancel_stream(env: Env, stream_id: u64) {
    Self::assert_not_paused(&env);
    ...
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/lib.rs` around lines 205 - 209, Correct the
pause documentation near the global emergency pause declaration to enumerate the
actual blocked and permitted entry points, including that cancel_stream,
pause_stream, resume_stream, set_delegate, and revoke_delegate remain callable
unless their guards are changed. Keep the documented behavior aligned with the
existing assert_not_paused coverage rather than claiming only admin operations
remain available.

Comment on lines +1909 to +1934
/// `emergency_pause` emits the correct event.
#[test]
fn test_emergency_pause_emits_event() {
let env = Env::default();
env.mock_all_auths();

let (client, _, _, _, _, _, _) = setup_paused_contract(&env);
client.emergency_pause();

let events = env.events().all();
assert!(events.len() > 0);
}

/// `emergency_unpause` emits the correct event.
#[test]
fn test_emergency_unpause_emits_event() {
let env = Env::default();
env.mock_all_auths();

let (client, _, _, _, _, _, _) = setup_paused_contract(&env);
client.emergency_pause();
client.emergency_unpause();

let events = env.events().all();
assert!(events.len() > 0);
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

These event tests can never fail — assert the actual event.

setup_paused_contract already emits events (initialize/mint), so events.len() > 0 holds even if emergency_pause/emergency_unpause published nothing. The doc comments claim the "correct event" is verified, but neither topic nor payload is checked.

💚 Assert topic and payload instead
     fn test_emergency_pause_emits_event() {
         let env = Env::default();
         env.mock_all_auths();
 
-        let (client, _, _, _, _, _, _) = setup_paused_contract(&env);
+        let (client, contract_id, admin, _, _, _, _) = setup_paused_contract(&env);
         client.emergency_pause();
 
-        let events = env.events().all();
-        assert!(events.len() > 0);
+        let paused_at = env.ledger().timestamp();
+        let expected = (
+            contract_id,
+            ("EmergencyPaused",).into_val(&env),
+            EmergencyPausedEvent { paused_by: admin, paused_at }.into_val(&env),
+        );
+        assert!(env.events().all().contains(expected));
     }

EmergencyPausedEvent / EmergencyUnpausedEvent need importing from the crate root for this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 1909 - 1934, Update
test_emergency_pause_emits_event and test_emergency_unpause_emits_event to
import EmergencyPausedEvent and EmergencyUnpausedEvent from the crate root, then
assert the emitted event’s topic and payload match the corresponding event type
and expected values. Replace the non-specific events.len() > 0 checks while
preserving each test’s existing setup and contract calls.

Comment on lines +344 to +354
): Promise<BatchDistributionResult> {
if ("amounts" in params) {
// Weighted distribution
return prepareBatchWeightedDistribution(this, {
sender: params.sender,
token: params.token,
recipients: params.recipients,
amounts: params.amounts,
config: params.config,
});
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Discriminator doesn't guard against an amounts key present with undefined value.

"amounts" in params returns true even if params.amounts is undefined (e.g., from a spread of a conditionally-built object). That would route into prepareBatchWeightedDistribution, which would throw a confusing TypeError on undefined.length rather than the clear validation errors this API otherwise provides. Consider Array.isArray(params.amounts) for a more robust runtime discriminator.

🛡️ Proposed fix
-    if ("amounts" in params) {
+    if (Array.isArray((params as { amounts?: bigint[] }).amounts)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
): Promise<BatchDistributionResult> {
if ("amounts" in params) {
// Weighted distribution
return prepareBatchWeightedDistribution(this, {
sender: params.sender,
token: params.token,
recipients: params.recipients,
amounts: params.amounts,
config: params.config,
});
}
): Promise<BatchDistributionResult> {
if (Array.isArray((params as { amounts?: bigint[] }).amounts)) {
// Weighted distribution
return prepareBatchWeightedDistribution(this, {
sender: params.sender,
token: params.token,
recipients: params.recipients,
amounts: params.amounts,
config: params.config,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/src/DistributorClient.ts` around lines 344 - 354, Update the
discriminator in the batch distribution method around
prepareBatchWeightedDistribution to require that params.amounts is an array,
rather than only checking for the amounts key. This must route undefined or
otherwise invalid amounts values to the existing validation path while
preserving weighted distribution for valid arrays.

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.

[SDK] Implement DistributorClient.batchDistribute utility

1 participant