Complete reference for integrating FlowStar's token streaming smart contract into your dApp.
- Admin Functions
- Core Functions
- Query Functions
- Metadata Functions
- Delegation Functions
- Contract Info Functions
- Authorization
- Error Codes
- Gas Estimates
- Type Definitions
Initializes the contract with an admin address. Must be called exactly once after deployment; subsequent calls panic.
Signature:
pub fn initialize(env: Env, admin: Address)Parameters:
admin: Address- The account that will have administrative control over the contract (upgrade, pause, migrate)
Authorization Required:
adminmust authorize the transaction
Preconditions:
- Contract must not already be initialized (no
Adminkey in instance storage)
Returns:
()on success- Panics with
"already initialized"if called a second time
Behavior:
- Sets
Adminin instance storage toadmin - Sets
Pausedflag tofalse - Bumps instance storage TTL to ~1 day
Example - CLI:
soroban contract invoke \
--id CXXXXX \
-- \
initialize \
--admin GXXXXXXHalts all write operations contract-wide. While paused, calls to create_stream, create_streams_batch, withdraw, cancel, transfer_stream, and top_up will panic with "contract is paused".
Signature:
pub fn pause(env: Env)Parameters:
- None (admin identity is read from instance storage)
Authorization Required:
- The stored admin address must authorize the transaction
Preconditions:
- Contract must be initialized
Returns:
()on success
Behavior:
- Sets
Pausedflag totruein instance storage - Bumps instance storage TTL
- Emits a
PauseEventwith the current ledger timestamp
Example - CLI:
soroban contract invoke \
--id CXXXXX \
--source GADMIN \
-- \
pauseResumes all write operations after a pause.
Signature:
pub fn unpause(env: Env)Parameters:
- None
Authorization Required:
- The stored admin address must authorize the transaction
Preconditions:
- Contract must be initialized
Returns:
()on success
Behavior:
- Sets
Pausedflag tofalsein instance storage - Bumps instance storage TTL
- Emits an
UnpauseEventwith the current ledger timestamp
Example - CLI:
soroban contract invoke \
--id CXXXXX \
--source GADMIN \
-- \
unpauseReplaces the contract's Wasm bytecode with a new version. Only the admin can call this.
Signature:
pub fn upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>)Parameters:
admin: Address- Must match the stored admin address (validated on-chain)new_wasm_hash: BytesN<32>- Hash of the new Wasm blob, which must already be uploaded to the network viasoroban contract upload
Authorization Required:
adminmust authorize the transaction
Preconditions:
adminmust equal the stored admin address; panics with"unauthorized"otherwise- New Wasm hash must exist on the network
Returns:
()on success
Behavior:
- Calls
env.deployer().update_current_contract_wasm(new_wasm_hash)to perform the in-place upgrade - Storage layout is not migrated automatically; call
migrateafter upgrading if the new version requires it
Example - CLI:
# 1. Upload new wasm first
soroban contract upload \
--wasm target/wasm32-unknown-unknown/release/streaming.wasm \
--source GADMIN
# 2. Upgrade using the returned hash
soroban contract invoke \
--id CXXXXX \
--source GADMIN \
-- \
upgrade \
--admin GADMIN \
--new_wasm_hash <32-byte-hex-hash>Post-upgrade data migration hook. Call this once after upgrade when the new contract version requires storage layout changes.
Signature:
pub fn migrate(env: Env)Parameters:
- None
Authorization Required:
- The stored admin address must authorize the transaction
Preconditions:
- Contract must be initialized
Returns:
()on success
Behavior:
- Sets
Pausedtofalse(unfreezes the contract after an upgrade) - Any version-specific storage migrations are implemented here in future contract versions
Example - CLI:
soroban contract invoke \
--id CXXXXX \
--source GADMIN \
-- \
migrateCreates a new payment stream with optional cliff vesting.
Signature:
pub fn create_stream(sender: Address, params: StreamParams) -> u64Parameters:
sender: Address- The account funding and owning the stream (must authorize)params: StreamParams- Stream configuration object containing:recipient: Address- Account receiving the fundstoken: Address- Token contract address (SEP-41)total_amount: i128- Total amount to stream (in token's smallest unit)start_time: u64- Stream start time (UNIX seconds)end_time: u64- Stream end time (UNIX seconds)cliff_time: u64- Time before which no funds unlock (except cliff_amount)cliff_amount: i128- Amount unlocked immediately at cliff (smallest unit)
Returns:
u64- Unique stream ID
Authorization Required:
sendermust authorize the transactionsendermust have approved the streaming contract to transfertotal_amounttokens
Preconditions:
start_time < end_timecliff_time >= start_timecliff_amount <= total_amounttotal_amount > 0- Token contract must be valid SEP-41
Example - CLI:
soroban contract invoke \
--id CXXXXX \
-- \
create_stream \
--sender GXXXXXX \
--recipient GXXXXXX \
--token CXXXXXX \
--total_amount 1000000000 \
--start_time 1700000000 \
--end_time 1702592000 \
--cliff_time 1700000000 \
--cliff_amount 100000000Example - JavaScript (Stellar SDK):
import { Address, Contract, nativeToScVal } from '@stellar/stellar-sdk';
const contract = new Contract(STREAM_CONTRACT_ID);
const params = {
recipient: new Address(recipientAddress).toScVal(),
token: new Address(tokenAddress).toScVal(),
total_amount: nativeToScVal(1000000000n, { type: 'i128' }),
start_time: nativeToScVal(Math.floor(Date.now() / 1000), { type: 'u64' }),
end_time: nativeToScVal(Math.floor(Date.now() / 1000) + 86400 * 30, { type: 'u64' }),
cliff_time: nativeToScVal(Math.floor(Date.now() / 1000), { type: 'u64' }),
cliff_amount: nativeToScVal(100000000n, { type: 'i128' }),
};
const result = await invoke(
'create_stream',
[new Address(senderAddress).toScVal(), nativeToScVal(params, { type: 'map' })],
);Creates multiple token streams in a single atomic transaction. All streams are validated before any funds are transferred — if any stream fails validation the entire batch is rejected with no side-effects.
Signature:
pub fn create_streams_batch(
env: Env,
sender: Address,
streams: Vec<CreateStreamInput>,
) -> Result<Vec<u64>, StreamError>Parameters:
sender: Address- The account funding all streams in the batch (must authorize)streams: Vec<CreateStreamInput>- Between 1 and 20 stream definitions. EachCreateStreamInputcontains:recipient: Address- Account receiving the fundstoken: Address- Token contract address (SEP-41)total_amount: i128- Total amount for this stream (smallest unit)start_time: u64- Stream start time (UNIX seconds)end_time: u64- Stream end time (UNIX seconds)cliff_time: u64- Cliff time (UNIX seconds)cliff_amount: i128- Amount unlocked at cliff (smallest unit)
Authorization Required:
sendermust authorize the transactionsendermust have approved the contract to spend the sum of alltotal_amountvalues for each token used across the batch (separate approvals per distinct token)
Preconditions:
streamsmust not be empty; returnsStreamError::BatchEmptyotherwisestreams.len() <= 20; returnsStreamError::BatchSizeExceededotherwise- Each stream entry must satisfy the same per-stream validation rules as
create_stream
Returns:
Ok(Vec<u64>)- Stream IDs in the same order as the inputstreamsvectorErr(StreamError)- First validation error encountered (no streams are created)
Errors:
BatchEmpty(12) —streamsvector is emptyBatchSizeExceeded(11) — more than 20 streams in the batchInvalidAmount(1),InvalidTimeRange(2),InvalidCliff(3),SelfStream(4) — per-stream validation failures
Example - JavaScript:
const streams = [
{
recipient: new Address('GRECIPIENT1...').toScVal(),
token: new Address(tokenAddress).toScVal(),
total_amount: nativeToScVal(1000_0000000n, { type: 'i128' }),
start_time: nativeToScVal(now, { type: 'u64' }),
end_time: nativeToScVal(now + 86400 * 30, { type: 'u64' }),
cliff_time: nativeToScVal(now, { type: 'u64' }),
cliff_amount: nativeToScVal(0n, { type: 'i128' }),
},
{
recipient: new Address('GRECIPIENT2...').toScVal(),
// ...same fields...
},
];
const streamIds = await invoke('create_streams_batch', [
new Address(senderAddress).toScVal(),
nativeToScVal(streams, { type: 'vec' }),
]);
// Returns [1n, 2n, ...]Withdraws available funds from a stream to the recipient's account.
Signature:
pub fn withdraw(stream_id: u64, amount: i128) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to withdraw fromamount: i128- Amount to withdraw (in token's smallest unit)
Authorization Required:
- The stream recipient must authorize the transaction
Preconditions:
- Stream must exist
- Recipient must have at least
amountwithdrawable - Stream must not be cancelled
Returns:
Ok(())on successErr(StreamError)on failure
Example - JavaScript:
const streamId = 1n;
const withdrawAmount = 100000000n; // 10 USDC (7 decimals)
const result = await invoke('withdraw', [
nativeToScVal(streamId, { type: 'u64' }),
nativeToScVal(withdrawAmount, { type: 'i128' }),
]);Cancels a stream and returns remaining funds to the sender.
Signature:
pub fn cancel(stream_id: u64) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to cancel
Authorization Required:
- The stream sender must authorize the transaction
Preconditions:
- Stream must exist
- Stream must not already be cancelled
Returns:
Ok(())on successErr(StreamError)on failure
Example - JavaScript:
const streamId = 1n;
const result = await invoke('cancel', [
nativeToScVal(streamId, { type: 'u64' }),
]);Transfers stream ownership to a new recipient.
Signature:
pub fn transfer_stream(stream_id: u64, new_recipient: Address) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to transfernew_recipient: Address- New recipient address
Authorization Required:
- The current stream recipient must authorize the transaction
Preconditions:
- Stream must exist
new_recipientmust be different from current recipient- Stream must not be cancelled
Returns:
Ok(())on successErr(StreamError)on failure
Example - JavaScript:
const streamId = 1n;
const newRecipient = 'GXXXXX...';
const result = await invoke('transfer_stream', [
nativeToScVal(streamId, { type: 'u64' }),
new Address(newRecipient).toScVal(),
]);Adds additional funds to an existing stream.
Signature:
pub fn top_up(stream_id: u64, additional_amount: i128) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to top upadditional_amount: i128- Additional amount to add (smallest unit)
Authorization Required:
- The stream sender must authorize the transaction
- Sender must have approved the contract for
additional_amounttokens
Preconditions:
- Stream must exist
- Stream must not be cancelled
additional_amount > 0
Returns:
Ok(())on successErr(StreamError)on failure
Example - JavaScript:
const streamId = 1n;
const additionalAmount = 50000000n; // Add 5 USDC
const result = await invoke('top_up', [
nativeToScVal(streamId, { type: 'u64' }),
nativeToScVal(additionalAmount, { type: 'i128' }),
]);Extends the stream's time-to-live in storage. Anyone may call this — no authorization required. Useful for keeping a long-running stream accessible without modifying its data.
Signature:
pub fn bump_stream(stream_id: u64) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to bump
Authorization Required:
- None — any caller may extend a stream's TTL
Preconditions:
- Stream must exist
Returns:
Ok(())on successErr(StreamError)on failure
Behavior:
- Extends the
Stream(id)persistent storage entry TTL to ~30 days (PERSISTENT_TTL_LEDGERS) - Emits a
StreamBumpedEvent - Does not modify any stream data
Example - JavaScript:
const streamId = 1n;
const result = await invoke('bump_stream', [
nativeToScVal(streamId, { type: 'u64' }),
]);Permanently removes all on-chain data for a completed or cancelled stream, reclaiming storage. Either the sender or recipient may call this.
Signature:
pub fn cleanup_stream(env: Env, caller: Address, stream_id: u64)Parameters:
caller: Address- The account initiating cleanup (must be sender or recipient)stream_id: u64- Stream ID to remove
Authorization Required:
callermust authorize the transaction
Preconditions:
- Stream must exist
callermust be eitherstream.senderorstream.recipient; panics with"only sender or recipient may clean up a stream"otherwise- Stream must be cancelled or fully drained after
end_time; panics with"stream must be cancelled or fully completed before cleanup"otherwise
Returns:
()on success
Behavior:
- Removes the stream from all active and archive index lists for both sender and recipient
- Deletes the
Stream(id)persistent storage entry - Deletes the
StreamMetadata(id)entry if present - Deletes the
Delegate(id)entry if present
Example - JavaScript:
await invoke('cleanup_stream', [
new Address(senderAddress).toScVal(),
nativeToScVal(streamId, { type: 'u64' }),
]);Fetches a stream by ID.
Signature:
pub fn get_stream(stream_id: u64) -> Result<Stream, StreamError>Parameters:
stream_id: u64- Stream ID to retrieve
Returns:
- Stream object with all fields
Err(StreamError::NotFound)if stream doesn't exist
Example:
const streamId = 1n;
const stream = await query('get_stream', [nativeToScVal(streamId, { type: 'u64' })]);Returns the amount available for withdrawal from a stream at current time.
Signature:
pub fn get_withdrawable(stream_id: u64) -> i128Parameters:
stream_id: u64- Stream ID
Returns:
i128- Withdrawable amount (in token's smallest unit)
Example:
const streamId = 1n;
const withdrawable = await query('get_withdrawable', [
nativeToScVal(streamId, { type: 'u64' }),
]);Lists stream IDs sent by an address (paginated).
Signature:
pub fn get_sent_streams(sender: Address, offset: u32, limit: u32) -> Vec<u64>Parameters:
sender: Address- Sender addressoffset: u32- Pagination offsetlimit: u32- Maximum results (recommended: 100)
Returns:
- Vector of stream IDs
Example:
const senderAddress = 'GXXXXX...';
const offset = 0;
const limit = 100;
const streamIds = await query('get_sent_streams', [
new Address(senderAddress).toScVal(),
nativeToScVal(offset, { type: 'u32' }),
nativeToScVal(limit, { type: 'u32' }),
]);Lists stream IDs received by an address (paginated).
Signature:
pub fn get_received_streams(recipient: Address, offset: u32, limit: u32) -> Vec<u64>Parameters:
recipient: Address- Recipient addressoffset: u32- Pagination offsetlimit: u32- Maximum results (recommended: 100)
Returns:
- Vector of stream IDs
Returns total number of streams sent by an address.
Signature:
pub fn get_sent_stream_count(sender: Address) -> u32Parameters:
sender: Address- Sender address
Returns:
u32- Total count
Returns total number of streams received by an address.
Signature:
pub fn get_received_stream_count(recipient: Address) -> u32Parameters:
recipient: Address- Recipient address
Returns:
u32- Total count
Returns paginated stream IDs for completed or cancelled streams where address is the sender. Streams move to the archive index when they are cancelled or fully drained.
Signature:
pub fn get_archived_sent_streams(
env: Env,
address: Address,
offset: u32,
limit: u32,
) -> Vec<u64>Parameters:
address: Address- Sender address to look upoffset: u32- Zero-based pagination offsetlimit: u32- Maximum number of IDs to return
Returns:
Vec<u64>- Archived stream IDs (may be empty if none exist or offset is out of range)
Example - JavaScript:
const archivedIds = await query('get_archived_sent_streams', [
new Address(senderAddress).toScVal(),
nativeToScVal(0, { type: 'u32' }),
nativeToScVal(100, { type: 'u32' }),
]);Returns paginated stream IDs for completed or cancelled streams where address is the recipient.
Signature:
pub fn get_archived_received_streams(
env: Env,
address: Address,
offset: u32,
limit: u32,
) -> Vec<u64>Parameters:
address: Address- Recipient address to look upoffset: u32- Zero-based pagination offsetlimit: u32- Maximum number of IDs to return
Returns:
Vec<u64>- Archived stream IDs (may be empty if none exist or offset is out of range)
Example - JavaScript:
const archivedIds = await query('get_archived_received_streams', [
new Address(recipientAddress).toScVal(),
nativeToScVal(0, { type: 'u32' }),
nativeToScVal(100, { type: 'u32' }),
]);Attaches or replaces human-readable metadata on a stream. Only the sender can update.
Signature:
pub fn update_stream_metadata(
env: Env,
stream_id: u64,
metadata: StreamMetadata,
) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to updatemetadata: StreamMetadata- Metadata to store, containing:name: String- Short display name for the streamcategory: String- Freeform category label (e.g."payroll","vesting")memo: String- Longer freeform note
Authorization Required:
stream.sendermust authorize the transaction
Preconditions:
- Stream must exist
Returns:
Ok(())on successErr(StreamError::StreamNotFound)if the stream does not exist
Behavior:
- Stores the
StreamMetadatastruct in persistent storage underStreamMetadata(stream_id) - Bumps the metadata entry TTL to ~30 days
Example - JavaScript:
await invoke('update_stream_metadata', [
nativeToScVal(streamId, { type: 'u64' }),
nativeToScVal(
{ name: 'Alice Salary', category: 'payroll', memo: 'Q3 2026' },
{ type: 'map' },
),
]);Returns the metadata for a stream, if any has been set.
Signature:
pub fn get_stream_metadata(env: Env, stream_id: u64) -> Option<StreamMetadata>Parameters:
stream_id: u64- Stream ID to query
Returns:
Some(StreamMetadata)if metadata exists for this streamNoneif no metadata has been set
Example - JavaScript:
const metadata = await query('get_stream_metadata', [
nativeToScVal(streamId, { type: 'u64' }),
]);
// { name: 'Alice Salary', category: 'payroll', memo: 'Q3 2026' } or nullRegisters a delegate address that can authorize withdraw calls on behalf of the stream's recipient. Useful for automating withdrawals via a bot or smart contract without granting full account control.
Signature:
pub fn set_delegate(
env: Env,
stream_id: u64,
delegate: Address,
) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to configuredelegate: Address- Address that will be permitted to callwithdraw
Authorization Required:
stream.recipientmust authorize the transaction
Preconditions:
- Stream must exist
Returns:
Ok(())on successErr(StreamError::StreamNotFound)if the stream does not exist
Behavior:
- Stores
delegatein persistent storage underDelegate(stream_id) - Bumps the delegate entry TTL to ~30 days
- The delegate is cleared automatically if the stream is transferred via
transfer_stream
Example - JavaScript:
await invoke('set_delegate', [
nativeToScVal(streamId, { type: 'u64' }),
new Address(delegateAddress).toScVal(),
]);Removes the registered delegate for a stream. After this call, only the recipient can authorize withdrawals.
Signature:
pub fn remove_delegate(env: Env, stream_id: u64) -> Result<(), StreamError>Parameters:
stream_id: u64- Stream ID to update
Authorization Required:
stream.recipientmust authorize the transaction
Preconditions:
- Stream must exist
Returns:
Ok(())on successErr(StreamError::StreamNotFound)if the stream does not exist
Behavior:
- Deletes the
Delegate(stream_id)entry from persistent storage (no-op if no delegate was set)
Example - JavaScript:
await invoke('remove_delegate', [
nativeToScVal(streamId, { type: 'u64' }),
]);Returns the delegate address for a stream, if one has been set.
Signature:
pub fn get_delegate(env: Env, stream_id: u64) -> Option<Address>Parameters:
stream_id: u64- Stream ID to query
Returns:
Some(Address)if a delegate is registeredNoneif no delegate has been set
Example - JavaScript:
const delegate = await query('get_delegate', [
nativeToScVal(streamId, { type: 'u64' }),
]);
// 'GDELEGATE...' or nullReturns the contract's version number as a u32. Useful for on-chain version checks after an upgrade.
Signature:
pub fn version(_env: Env) -> u32Parameters:
- None
Returns:
u32- Current contract version (currently1)
Example - JavaScript:
const v = await query('version', []);
// 1Returns the human-readable contract name as a Soroban String.
Signature:
pub fn name(env: Env) -> soroban_sdk::StringParameters:
- None
Returns:
String- Contract name (currently"FlowStar Streaming")
Example - JavaScript:
const contractName = await query('name', []);
// 'FlowStar Streaming'All write operations require transaction authorization from a specific account:
import { TransactionBuilder, Address } from '@stellar/stellar-sdk';
// The signer's account must match the required authorizer for the operation
const tx = new TransactionBuilder(account, {
fee: '1000000',
networkPassphrase: 'Test SDF Network ; September 2015',
})
.addOperation(contract.call('create_stream', ...args))
.setTimeout(300)
.build();
// Sign with the authorized account
const signedXdr = await wallet.sign(tx);| Code | Name | Description | Recovery |
|---|---|---|---|
| 1 | NotFound | Stream does not exist | Verify stream ID exists |
| 2 | Unauthorized | Caller is not authorized for this operation | Use correct wallet address |
| 3 | InvalidAmount | Amount is negative or zero | Use positive amount > 0 |
| 4 | InvalidTime | Start/end times are invalid | Ensure start_time < end_time |
| 5 | InvalidCliff | Cliff configuration is invalid | Ensure cliff_time >= start_time |
| 6 | AlreadyCancelled | Stream is already cancelled | Cannot modify cancelled streams |
| 7 | InsufficientFunds | Insufficient balance to execute operation | Add more funds or reduce amount |
| 8 | InvalidToken | Token contract is not valid SEP-41 | Verify token address |
| 9 | TransferFailed | Token transfer failed (likely insufficient allowance) | Approve contract for amount |
| 10 | InsufficientWithdrawable | No funds available to withdraw | Wait for cliff or unlock period |
Approximate gas costs on Stellar's Soroban (in stroops = 0.0000001 XLM):
| Operation | Min Fee | Estimated Fee (15% buffer) | Notes |
|---|---|---|---|
| create_stream | 500,000 | 575,000 | + token approval (~500k) |
| withdraw | 200,000 | 230,000 | Varies by stream state |
| cancel | 150,000 | 172,500 | Varies by amount returned |
| transfer_stream | 100,000 | 115,000 | Quick operation |
| top_up | 200,000 | 230,000 | Similar to withdraw |
| bump_stream | 100,000 | 115,000 | Minimal cost |
| get_stream | 50,000 | N/A | Read-only, no fee |
| get_withdrawable | 50,000 | N/A | Read-only, no fee |
interface Stream {
id: u64;
sender: Address;
recipient: Address;
token: Address;
deposited_amount: i128;
withdrawn_amount: i128;
start_time: u64;
end_time: u64;
cliff_time: u64;
cliff_amount: i128;
amount_per_second: i128;
cancelled: boolean;
linear_amount: i128;
duration: i128;
}
interface StreamParams {
recipient: Address;
token: Address;
total_amount: i128;
start_time: u64;
end_time: u64;
cliff_time: u64;
cliff_amount: i128;
}
// Used by create_streams_batch; same fields as StreamParams but a distinct type
interface CreateStreamInput {
recipient: Address;
token: Address;
total_amount: i128;
start_time: u64;
end_time: u64;
cliff_time: u64;
cliff_amount: i128;
}
interface StreamMetadata {
name: string;
category: string;
memo: string;
}