Skip to content

feat(contracts): add split_stream to divide an existing stream into two - #939

Open
macsonfleek wants to merge 1 commit into
ritik4ever:mainfrom
macsonfleek:feature/stream-splitting
Open

feat(contracts): add split_stream to divide an existing stream into two#939
macsonfleek wants to merge 1 commit into
ritik4ever:mainfrom
macsonfleek:feature/stream-splitting

Conversation

@macsonfleek

Copy link
Copy Markdown

Summary

Adds the split_stream function to the Soroban smart contract, allowing a sender to divide one active stream into two new streams at the current vested point. This implements the feature requested in ritik4ever#671.

What changes

contracts/src/lib.rs

New StreamSplit event struct — emitted when a stream is split:

  • stream_id — the original stream that was closed
  • actor — the sender who initiated the split
  • timestamp — ledger close time
  • original_id — id of the original stream
  • new_ids — the two newly created stream ids
  • split_ratios_bps — the two basis-point ratios used

New split_stream(stream_id, sender, split_ratio_bps) -> (u64, u64) entry point:

  1. Auth: Requires sender.require_auth()
  2. Validation:
    • split_ratio_bps must have exactly 2 entries
    • Ratios must sum to 10000 bps (100%)
    • Both ratios must be non-zero
    • Stream must not be paused or already canceled
    • Caller must be the stream's sender
  3. Split logic:
    • Computes vested at current ledger time using the existing vested_amount() helper
    • Closes the original stream: total_amount = vested, end_time = now, canceled = true
    • The unvested remainder (total_amount - vested) is divided between two new streams according to the basis-point ratio
    • Both child streams inherit the original recipient, token, metadata, and min_claim_interval_seconds
    • Child streams run from now to the original end_time (remaining duration)
    • No token transfers needed — unvested tokens are already escrowed in the contract
  4. Event emission: Publishes a StreamSplit event under (symbol_short!("Stream"), symbol_short!("Split"))

contracts/src/test.rs — 16 new tests

Test | What it verifies -- | -- test_split_stream_basic | 50/50 split at 50% elapsed; original canceled, two 250-token children test_split_stream_uneven_ratios | 30/70 split → 150 and 350 test_split_stream_inherits_remaining_duration | Children start at split time, end at original end_time test_split_stream_claimable_after_split | Claimable amounts grow linearly in children test_split_stream_already_claimed_excluded | Pre-claim of 300 is excluded; unvested 500 splits correctly test_split_stream_token_conservation | Recipient receives exactly the unvested amount from both children test_split_stream_emits_event | StreamSplit event fields match expectations test_split_stream_ratios_not_10000_panics | 4000+4000=8000 panics with expected message test_split_stream_wrong_sender_panics | Non-sender caller panics test_split_stream_canceled_stream_panics | Cannot split a stream that was already canceled test_split_stream_at_end_of_stream_panics | Fully vested stream has nothing to split test_split_stream_zero_ratio_panics | 0+10000 panics (zero ratio rejected) test_split_stream_acceptance_split_at_50_percent_elapsed | Acceptance criterion from #671: split at 50%, verify both halves vest and claim correctly test_split_stream_at_start_no_vesting | Split at t=0 distributes 100% of total test_split_stream_paused_stream_panics | Paused streams cannot be split test_split_stream_wrong_ratio_count_panics | Only 1 ratio entry panics

Acceptance Criteria Verification

Split ratios must sum to 10000 bps.
Validated at entry — panics if ratio_a + ratio_b != 10000.

Already-claimed amounts excluded from split.
vested_amount() returns gross vested; child streams get total_amount - vested. Previously claimed tokens are already with the recipient and are not redistributed.

Test: split at 50% elapsed → verify both halves receive correct amounts.
test_split_stream_acceptance_split_at_50_percent_elapsed creates a 1000-token stream from 0→1000, splits at t=500 with 50/50 ratio, verifies each child has 250 tokens, vests linearly, and the recipient can claim 500 total.

How to test locally

cd contracts
cargo test

How this fits with existing architecture

  • The function follows the same pattern as cancel(): read the stream, validate ownership, adjust total_amount/end_time, mark canceled = true, and emit an event.
  • No new storage keys or database migrations are needed — DataKey::Stream(u64) and DataKey::NextStreamId are reused.
  • The TokenClient is reused to resolve native vs. non-native tokens, though no actual token transfers occur during the split (tokens remain in escrow).
  • The event uses the same (topic1, topic2) tuple pattern as all other events in the contract.

Closes ritik4ever#671

Summary

Adds the split_stream function to the Soroban smart contract, allowing a sender to divide one active stream into two new streams at the current vested point. This implements the feature requested in #671.
What changes
contracts/src/lib.rs

New StreamSplit event struct — emitted when a stream is split:

stream_id — the original stream that was closed
actor — the sender who initiated the split
timestamp — ledger close time
original_id — id of the original stream
new_ids — the two newly created stream ids
split_ratios_bps — the two basis-point ratios used

New split_stream(stream_id, sender, split_ratio_bps) -> (u64, u64) entry point:

Auth: Requires sender.require_auth()
Validation:
    split_ratio_bps must have exactly 2 entries
    Ratios must sum to 10000 bps (100%)
    Both ratios must be non-zero
    Stream must not be paused or already canceled
    Caller must be the stream's sender
Split logic:
    Computes vested at current ledger time using the existing vested_amount() helper
    Closes the original stream: total_amount = vested, end_time = now, canceled = true
    The unvested remainder (total_amount - vested) is divided between two new streams according to the basis-point ratio
    Both child streams inherit the original recipient, token, metadata, and min_claim_interval_seconds
    Child streams run from now to the original end_time (remaining duration)
    No token transfers needed — unvested tokens are already escrowed in the contract
Event emission: Publishes a StreamSplit event under (symbol_short!("Stream"), symbol_short!("Split"))

contracts/src/test.rs — 16 new tests
Test What it verifies
test_split_stream_basic 50/50 split at 50% elapsed; original canceled, two 250-token children
test_split_stream_uneven_ratios 30/70 split → 150 and 350
test_split_stream_inherits_remaining_duration Children start at split time, end at original end_time
test_split_stream_claimable_after_split Claimable amounts grow linearly in children
test_split_stream_already_claimed_excluded Pre-claim of 300 is excluded; unvested 500 splits correctly
test_split_stream_token_conservation Recipient receives exactly the unvested amount from both children
test_split_stream_emits_event StreamSplit event fields match expectations
test_split_stream_ratios_not_10000_panics 4000+4000=8000 panics with expected message
test_split_stream_wrong_sender_panics Non-sender caller panics
test_split_stream_canceled_stream_panics Cannot split a stream that was already canceled
test_split_stream_at_end_of_stream_panics Fully vested stream has nothing to split
test_split_stream_zero_ratio_panics 0+10000 panics (zero ratio rejected)
test_split_stream_acceptance_split_at_50_percent_elapsed Acceptance criterion from #671: split at 50%, verify both halves vest and claim correctly
test_split_stream_at_start_no_vesting Split at t=0 distributes 100% of total
test_split_stream_paused_stream_panics Paused streams cannot be split
test_split_stream_wrong_ratio_count_panics Only 1 ratio entry panics
Acceptance Criteria Verification

Split ratios must sum to 10000 bps.
Validated at entry — panics if ratio_a + ratio_b != 10000.

Already-claimed amounts excluded from split.
vested_amount() returns gross vested; child streams get total_amount - vested. Previously claimed tokens are already with the recipient and are not redistributed.

Test: split at 50% elapsed → verify both halves receive correct amounts.
test_split_stream_acceptance_split_at_50_percent_elapsed creates a 1000-token stream from 0→1000, splits at t=500 with 50/50 ratio, verifies each child has 250 tokens, vests linearly, and the recipient can claim 500 total.

How to test locally

cd contracts
cargo test

How this fits with existing architecture

The function follows the same pattern as cancel(): read the stream, validate ownership, adjust total_amount/end_time, mark canceled = true, and emit an event.
No new storage keys or database migrations are needed — DataKey::Stream(u64) and DataKey::NextStreamId are reused.
The TokenClient is reused to resolve native vs. non-native tokens, though no actual token transfers occur during the split (tokens remain in escrow).
The event uses the same (topic1, topic2) tuple pattern as all other events in the contract.

Closes #671

Closes #

Checklist

  • I used the correct commit type (see above) for the squashed commit message.
  • I kept the change focused on the related issue.
  • I added or updated tests where useful.
  • I updated documentation where behavior changed.
  • I verified the app still builds or explained why verification was skipped.
  • My commits follow Conventional Commits (enforced by commitlint).

Implements the split_stream function that allows a sender to split an
active stream at the current vested point into two new streams. The
original stream is closed and two child streams are created with the
unvested remainder, inheriting the remaining duration.

Key details:
- split_ratio_bps must contain exactly 2 entries summing to 10000 bps
- Already-claimed amounts are excluded from the split
- Paused and already-canceled streams cannot be split
- Emits StreamSplit event with original_id and new_ids

Adds 16 tests covering basic split, uneven ratios, duration inheritance,
claimable verification, token conservation, event emission, and error
cases (invalid ratios, wrong sender, canceled/paused stream, etc.).

Closes ritik4ever#671

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

@macsonfleek is attempting to deploy a commit to the ritik4ever's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@macsonfleek 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 Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 460072a5-c647-4d1a-8002-ea8bb6c60a96


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.

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.

[FEATURE] Add stream splitting: divide one stream into two

1 participant