Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions contracts/streaming/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ pub struct CancelEvent {
}

#[soroban_sdk::contractevent]
pub struct StreamTransferEvent {
pub stream_id: u64,
pub old_recipient: Address,
pub new_recipient: Address,
pub struct TopUpEvent {
pub stream_id: u64,
pub additional_amount: i128,
Expand Down Expand Up @@ -178,6 +182,23 @@ impl StreamingContract {
id
}

// ── Write: Transfer ────────────────────────────────────────────────────────

/// Transfer a token stream right to a new address
pub fn transfer_stream(env: Env, stream_id: u64, new_recipient: Address) {
let mut stream = Self::load_stream(&env, stream_id);
stream.recipient.require_auth();
let old_recipient = stream.recipient;
if stream.cancelled {
panic!("cannot transfer a cancelled stream");
}
if new_recipient == old_recipient {
panic!("new_recipient must differ from current recipient");
}

stream.recipient = new_recipient.clone();

// ── Persist stream ───────────────────────────────────────────────────
/// Top up an existing stream with additional funds.
///
/// Increases `deposited_amount` and recalculates `amount_per_second` over
Expand Down Expand Up @@ -250,6 +271,11 @@ impl StreamingContract {
.persistent()
.set(&DataKey::Stream(stream_id), &stream);

Self::remove_from_index(&env, DataKey::ReceivedBy(old_recipient.clone()), stream_id);
Self::push_to_index(&env, DataKey::ReceivedBy(new_recipient.clone()), stream_id);

StreamTransferEvent { stream_id, old_recipient, new_recipient }
.publish(&env);
Self::extend_stream_ttl(&env, stream_id);

// ── Emit event ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -505,6 +531,22 @@ impl StreamingContract {
);
}

/// Append a stream ID to an address index list.
fn remove_from_index(env: &Env, key: DataKey, id: u64) {
let mut indexes: Vec<u64> = env
.storage()
.persistent()
.get(&key)
.unwrap_or(Vec::new(env));

let position = indexes.iter().position(|id| id == id);
if let Some(i) = position {
indexes.remove(i as u32);
}

env.storage().persistent().set(&key, &indexes);
}

/// Extend the TTL of a stream entry (~30 days).
fn extend_stream_ttl(env: &Env, id: u64) {
env.storage().persistent().extend_ttl(
Expand Down
68 changes: 65 additions & 3 deletions contracts/streaming/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ extern crate std;

use super::*;
use soroban_sdk::{
testutils::{Address as _, Ledger},
token::{Client as TokenClient, StellarAssetClient},
Address, Env,
Address, Env, log, testutils::{Address as _, Ledger}, token::{Client as TokenClient, StellarAssetClient}, vec
};

// ─── Test helpers ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -402,6 +400,12 @@ fn test_incrementing_ids() {
}
}

#[test]
fn test_transfer_stream() {
let t = TestEnv::setup();
let now = 1_000_000u64;
t.set_time(now);

// ─── top up ───────────────────────────────────────────────────────────────────

#[test]
Expand All @@ -413,6 +417,45 @@ fn test_top_up_increases_deposited_amount() {
let params = t.default_params(now);
let total = params.total_amount;

// Approve contract to pull funds.
t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));

let stream_id = client.create_stream(&t.sender, &params);

assert_eq!(stream_id, 1);

let stream = client.get_stream(&stream_id);
assert_eq!(stream.sender, t.sender);
assert_eq!(stream.recipient, t.recipient);
assert_eq!(stream.deposited_amount, total);
assert_eq!(stream.withdrawn_amount, 0);
assert!(!stream.cancelled);

let new_recipient = Address::generate(&t.env);
client.transfer_stream(&stream_id, &new_recipient);

let stream = client.get_stream(&stream_id);
assert_eq!(stream.sender, t.sender);
assert_eq!(stream.recipient, new_recipient);
assert_eq!(stream.deposited_amount, total);
assert_eq!(stream.withdrawn_amount, 0);
assert!(!stream.cancelled);

let old_id = client.get_received_streams(&t.recipient);
assert_eq!(old_id, Vec::new(&t.env));

let new_id = client.get_received_streams(&new_recipient);
assert_eq!(new_id, vec![&t.env, 1]);

}

#[should_panic(expected = "cannot transfer a cancelled stream")]
#[test]
fn test_transfer_stream_panic() {
let t = TestEnv::setup();
let now = 1_000_000u64;
t.set_time(now);

t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
let stream_id = client.create_stream(&t.sender, &params);

Expand Down Expand Up @@ -458,6 +501,25 @@ fn test_top_up_mid_stream_recalculates_rate() {
let params = t.default_params(now);
let total = params.total_amount;

// Approve contract to pull funds.
t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));

let stream_id = client.create_stream(&t.sender, &params);

assert_eq!(stream_id, 1);

let stream = client.get_stream(&stream_id);
assert_eq!(stream.sender, t.sender);
assert_eq!(stream.recipient, t.recipient);
assert_eq!(stream.deposited_amount, total);
assert_eq!(stream.withdrawn_amount, 0);
assert!(!stream.cancelled);

client.cancel(&stream_id);

let new_recipient = Address::generate(&t.env);
client.transfer_stream(&stream_id, &new_recipient);
}
t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
let stream_id = client.create_stream(&t.sender, &params);

Expand Down
Loading