Guide: Full Lifecycle
Create a vesting stream then run withdraw, topup, transfer, and cancel - both APIs.
The stream must be created with cancelableBySender: true, canTopup: true, and transferableBySender: true to allow all operations. Locks only support withdraw.
Create the stream
import { Connection, Keypair } from "@solana/web3.js";
import BN from "bn.js";
import {
buildTransaction, cancel, createVesting, execute, sign, topup, transfer, withdraw,
} from "@streamflow/stream/solana/api";
import { pk } from "@streamflow/common";
import fs from "node:fs";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const env = { connection, programId: pk("HqDGZjaVRXJ9MGRQEw7qDc2rAr6iH1n1kAQdCZaCMfMZ") };
const secret = JSON.parse(fs.readFileSync(process.env.KEYPAIR_PATH!, "utf-8"));
const invoker = Keypair.fromSecretKey(Uint8Array.from(secret));
const start = Math.floor(Date.now() / 1000) + 10;
const created = await createVesting(
{
recipient: invoker.publicKey.toBase58(), // self, so same keypair can withdraw
tokenId: "TokenMintAddress...",
amount: new BN(3_600_000_000),
start,
period: 1, // 1-second periods for quick testing
duration: 3600,
cliffAmount: new BN(1_000_000), // small upfront amount
name: "Lifecycle Test",
cancelableBySender: true,
canTopup: true,
transferableBySender: true,
},
invoker,
{ ...env, isNative: false },
);
const streamId = created.metadataPubKey!.toBase58();
const createBuilt = await buildTransaction(created.instructions, { feePayer: invoker.publicKey }, env);
await sign(createBuilt.transaction, [invoker, ...(created.signers ?? [])]);
await execute(createBuilt, env);
console.log("Created stream:", streamId);Helper - run any lifecycle op
All lifecycle functions return InstructionResult: { instructions } and follow the same 3-phase pattern:
import type { TransactionInstruction } from "@solana/web3.js";
import type { Env } from "@streamflow/stream/solana/api";
async function runOp(
result: { instructions: TransactionInstruction[] },
invoker: Keypair,
): Promise<string> {
const built = await buildTransaction(result.instructions, { feePayer: invoker.publicKey }, env);
await sign(built.transaction, [invoker]);
return execute(built, env);
}Withdraw
// Wait for some tokens to vest
await new Promise((r) => setTimeout(r, 15_000));
const sig = await runOp(await withdraw({ id: streamId }, invoker, env), invoker);
console.log("Withdraw:", sig);Topup
const sig = await runOp(
await topup({ id: streamId, amount: new BN(100_000_000) }, invoker, { ...env, isNative: false }),
invoker,
);
console.log("Topup:", sig);Transfer
Do not pass computeLimit to buildTransaction for transfer() - it embeds its own compute budget instruction.
const transferResult = await transfer(
{ id: streamId, newRecipient: "NewRecipientWallet..." },
invoker,
env,
);
const built = await buildTransaction(transferResult.instructions, { feePayer: invoker.publicKey }, env);
await sign(built.transaction, [invoker]);
const sig = await execute(built, env);
console.log("Transfer:", sig);Cancel
Cancel must be the last operation - all others fail after cancellation. Remaining tokens return to the sender.
const sig = await runOp(await cancel({ id: streamId }, invoker, env), invoker);
console.log("Cancel:", sig);The class-based API has high-level methods (client.withdraw(), client.cancel(), etc.) that sign and submit in one call, and prepare*Instructions() methods for manual transaction control.
Initialize client and create stream
import { SolanaStreamClient, ICluster } from "@streamflow/stream";
import { buildVestingParams } from "@streamflow/stream/solana/api";
import { Keypair } from "@solana/web3.js";
import BN from "bn.js";
import fs from "node:fs";
const client = new SolanaStreamClient({
clusterUrl: "https://api.devnet.solana.com",
cluster: ICluster.Devnet,
});
const secret = JSON.parse(fs.readFileSync(process.env.KEYPAIR_PATH!, "utf-8"));
const invoker = Keypair.fromSecretKey(Uint8Array.from(secret));
const start = Math.floor(Date.now() / 1000) + 10;
const { txId, metadataId: streamId } = await client.create(
buildVestingParams({
recipient: invoker.publicKey.toBase58(),
tokenId: "TokenMintAddress...",
amount: new BN(3_600_000_000),
start,
period: 1,
duration: 3600,
cliffAmount: new BN(1_000_000),
name: "Lifecycle Test",
cancelableBySender: true,
canTopup: true,
transferableBySender: true,
}),
{ sender: invoker, isNative: false },
);
console.log("Created stream:", streamId, "Tx:", txId);Withdraw
await new Promise((r) => setTimeout(r, 15_000));
const { txId } = await client.withdraw(
{ id: streamId },
{ invoker },
);
console.log("Withdraw:", txId);Topup
const { txId } = await client.topup(
{ id: streamId, amount: new BN(100_000_000) },
{ invoker, isNative: false },
);
console.log("Topup:", txId);Transfer
const { txId } = await client.transfer(
{ id: streamId, newRecipient: "NewRecipientWallet..." },
{ invoker },
);
console.log("Transfer:", txId);Cancel
const { txId } = await client.cancel(
{ id: streamId },
{ invoker },
);
console.log("Cancel:", txId);Fine-grained: prepare instructions without submitting
For multi-sig, custom fee payers, or batching multiple operations:
// Returns TransactionInstruction[] only - no signing or submission
const withdrawIxs = await client.prepareWithdrawInstructions(
{ id: streamId },
{ invoker: { publicKey: invoker.publicKey } },
);
const cancelIxs = await client.prepareCancelInstructions(
{ id: streamId },
{ invoker: { publicKey: invoker.publicKey } },
);
// Compose into your own transaction with @solana/web3.js