This package allows you to create
, createMultiple
, withdraw
, cancel
, topup
, transfer
, update
a token stream.
You can also getOne
stream and get
multiple streams.
npm i -s @streamflow/stream
or
yarn add @streamflow/stream
API Documentation available here: docs site →
Most common imports:
import { BN } from "bn.js";
import { GenericStreamClient, getBN, getNumberFromBN } from "@streamflow/stream";
Check the SDK for other types and utility functions.
Before creating and manipulating streams, a chain-specific or generic StreamClient instance must be created. All stream functions are methods on this instance.
// Solana
import { StreamflowSolana } from "@streamflow/stream";
const solanaClient = new StreamflowSolana.SolanaStreamClient(
"https://api.mainnet-beta.solana.com"
);
// Aptos
import { StreamflowAptos } from "@streamflow/stream";
const aptosClient = new StreamflowAptos.AptosStreamClient(
"https://fullnode.mainnet.aptoslabs.com/v1"
);
// Sui
import { StreamflowSui } from "@streamflow/stream";
const suiClient = new StreamflowSui.SuiStreamClient(
"https://fullnode.testnet.sui.io:443"
);
GenericStreamClient provides an isomorphic interface to work with streams agnostic of chain.
import { GenericStreamClient } from "@streamflow/stream";
const client = new GenericStreamClient<Types.IChain.Solana>({
chain: IChain.Solana, // Blockchain
clusterUrl: "https://api.mainnet-beta.solana.com", // RPC cluster URL
cluster: ICluster.Mainnet, // (optional) (default: Mainnet)
// ...rest chain specific params e.g. commitment for Solana
});
Each method requires chain-specific parameters. Here are the common patterns: These are defined in:
// Solana parameters for Create
const solanaCreateParams = {
sender: wallet, // SignerWalletAdapter or Keypair of Sender account, can be only PublicKey if you are not using the client to initiate the signing and execution of the transaction
isNative: false // [optional] Whether Stream should be paid with Solana native token (wSOL)
};
// Solana parameters for Withdraw, TopUp, Update, Cancel etc.
const solanaInteractParams = {
invoker: wallet, // SignerWalletAdapter or Keypair signing the transaction, can be only PublicKey if you are not using the client to initiate the signing and execution of the transaction
isNative: false // [optional] Whether Stream should be paid with Solana native token (wSOL)
};
// Aptos parameters
const aptosParams = {
senderWallet: wallet, // AptosWalletAdapter Wallet of sender
tokenId: "0x1::aptos_coin::AptosCoin" // Aptos Coin type
};
// Sui parameters
const suiParams = {
senderWallet: wallet, // WalletContextState | Keypair
tokenId: "0x2::sui::SUI" // Sui token type
};
NOTE: All timestamp parameters are in seconds.
Vesting Streams are a type of Stream used to linearly unlock a given amount of tokens over a period of time. You can specify the amount, cliff, start time and releasePeriod along with other configuration that controls who can transfer or cancel the contract. For a Stream to be considered Vesting in the Streamflow App it has to match the criteria from the isVesting
helper function (or buildStreamType
returns StreamType.Vesting
). The stream must not allow top-ups, the cliff amount must not be too close to the total amount and it must not be a Dynamic token lock.
For more details creating a Stream with arbitrary configuration see General Stream creation. For creating multiple Streams at once see Create Multiple Streams.
The categorization criteria and other helper functions are defined in contractUtils.ts
return (
!streamData.canTopup &&
!isCliffCloseToDepositedAmount(streamData) &&
!isDynamicLock(streamData.minPrice, streamData.maxPrice, streamData.minPercentage, streamData.maxPercentage)
);
The following code will create a Vesting Stream that unlocks 20% of the locked tokens at the start with the remaining 80% unlocked daily over a period of 2 weeks.
const tokenDecimals = 9;
const totalAmount = getBN(1000, tokenDecimals);
const cliffAmount = getBN(200, tokenDecimals); // optional
const day = 60 * 60 * 24; // in seconds
const twoWeeks = day * 14;
const remainingAmount = totalAmount.sub(cliffAmount);
const amountPerPeriod = remainingAmount.divn(twoWeeks);
const createVestingParams: Types.ICreateStreamData = {
recipient: "4ih00075bKjVg000000tLdk4w42NyG3Mv0000dc0M00", // Recipient address.
tokenId: "DNw99999M7e24g99999999WJirKeZ5fQc6KY999999gK", // Token mint address.
start: 1643363040, // In this case start time will be the time of unlock
amount: totalAmount,
period: day, // 1 day for daily unlocks
cliff: 1643363040, // Cliff should match start
cliffAmount: cliffAmount, // Amount unlocked at the "cliff" timestamp.
amountPerPeriod: amountPerPeriod,
name: "Transfer to Jane Doe.", // The stream name or subject.
canTopup: false,
};
// Using the client to trigger the transaction signing and execution
try {
const { ixs, tx, metadata } = await client.create(createVestingParams, solanaCreateParams);
} catch (exception) {
// handle exception
}
// Creating the transaction yourself
// note: in this case you do not have to pass a Signer or Keypair in solanaCreateParams, you can pass only the PublicKey
const { ixs } = await client.prepareCreateStreamInstructions(createVestingParams, solanaCreateParams)
A token lock is a Stream that has specific configuration making the unlock happen (almost) instantenously at a certain point in time. For a Stream to be considered a Token Lock in the Streamflow App it must match the criteria defined in the isTokenLock
function (or for the buildStreamType
type function to return StreamType.Lock
). The Stream must not allow top-ups or auto-withdrawal and it can not be transferable by the sender or cancelable by either the sneder or the recipient. The cliffAmount
must be equal to or greater than depositedAmount.subn(1)
. Dynamic (price-based) locks conform to a different criteria.
The categorization criteria and other helper functions are defined in contractUtils.ts
return (
!streamData.canTopup &&
!streamData.automaticWithdrawal &&
!streamData.cancelableBySender &&
!streamData.cancelableByRecipient &&
!streamData.transferableBySender &&
(isCliffCloseToDepositedAmount(streamData) ||
isDynamicLock(streamData.minPrice, streamData.maxPrice, streamData.minPercentage, streamData.maxPercentage))
);
const isCliffCloseToDepositedAmount = (streamData: { depositedAmount: BN; cliffAmount: BN }): boolean => {
return streamData.cliffAmount.gte(streamData.depositedAmount.sub(new BN(1)));
const tokenDecimals = 9;
const totalAmount = getBN(1000, tokenDecimals);
const cliffAmount = totalAmount.subn(1);
const createTokenLockParams: Types.ICreateStreamData = {
recipient: "4ih00075bKjVg000000tLdk4w42NyG3Mv0000dc0M00", // Recipient address.
tokenId: "DNw99999M7e24g99999999WJirKeZ5fQc6KY999999gK", // Token mint address.
start: 1643363040, // In this case start time will be the time of unlock
amount: totalAmount,
period: 1, // 1 second
cliff: 1643363040, // Cliff should match start
cliffAmount: cliffAmount, // Amount unlocked at the "cliff" timestamp.
amountPerPeriod: new BN(1), // Remaining "1" which is smallest token denominator will be unlocked in 1 second
name: "Transfer to Jane Doe.", // The stream name or subject.
// The settings below are only necessary if you wish the Stream to be labelled as a Token Lock on the Streamflow App (or if you use the smae criteria for categorization of Streams)
canTopup: false,
cancelableBySender: false,
cancelableByRecipient: false,
transferableBySender: false,
transferableByRecipient: false,
};
// Using the client to trigger the transaction signing and execution
try {
const { ixs, tx, metadata } = await client.create(createTokenLockParams, solanaCreateParams);
} catch (exception) {
// handle exception
}
// Creating the transaction yourself
// note: in this case you do not have to pass a Signer or Keypair in solanaCreateParams, you can pass only the PublicKey
const { ixs } = await client.prepareCreateStreamInstructions(createTokenLockParams, solanaCreateParams)
This covers arbitrary configuration for creating a Stream - depending on the configuration it may fall into different categories in the Streamflow App if the Streamflow protocol is used. For more details on automatic withdrawal see Automatic Withdrawal.
const createStreamParams: Types.ICreateStreamData = {
recipient: "4ih00075bKjVg000000tLdk4w42NyG3Mv0000dc0M00", // Recipient address.
tokenId: "DNw99999M7e24g99999999WJirKeZ5fQc6KY999999gK", // Token mint address.
start: 1643363040, // Timestamp (in seconds) when the stream/token vesting starts.
amount: getBN(100, 9), // depositing 100 tokens with 9 decimals mint.
period: 1, // Time step (period) in seconds per which the unlocking occurs.
cliff: 1643363160, // Vesting contract "cliff" timestamp in seconds.
cliffAmount: new BN(10), // Amount unlocked at the "cliff" timestamp.
amountPerPeriod: getBN(5, 9), // Release rate: how many tokens are unlocked per each period.
name: "Transfer to Jane Doe.", // The stream name or subject.
canTopup: false, // Whether additional tokens can be deposited after creation, setting to FALSE will effectively create a vesting contract.
canUpdateRate: false, // settings to TRUE allows sender to update amountPerPeriod
cancelableBySender: true, // Whether or not sender can cancel the stream.
cancelableByRecipient: false, // Whether or not recipient can cancel the stream.
transferableBySender: true, // Whether or not sender can transfer the stream.
transferableByRecipient: false, // Whether or not recipient can transfer the stream.
automaticWithdrawal: true, // Whether or not a 3rd party (e.g. cron job, "cranker") can initiate a token withdraw/transfer.
withdrawalFrequency: 10, // Relevant when automatic withdrawal is enabled. If greater than 0 our withdrawor will take care of withdrawals. If equal to 0 our withdrawor will skip, but everyone else can initiate withdrawals.
partner: undefined, // (optional) Partner's wallet address (string | undefined).
};
// Use appropriate chain-specific parameters (see Chain-specific Parameters section above)
try {
const { ixs, tx, metadata } = await client.create(createStreamParams, solanaCreateParams);
} catch (exception) {
// handle exception
}
const recipients = [
{
recipient: "4ih00075bKjVg000000tLdk4w42NyG3Mv0000dc0M00", // Recipient address.
amount: getBN(100, 9), // depositing 100 tokens with 9 decimals mint.
name: "January Payroll", // The stream name/subject.
cliffAmount: getBN(10, 9), // amount released on cliff for this recipient
amountPerPeriod: getBN(1, 9), //amount released every specified period epoch
},
];
const createMultiStreamsParams: ICreateMultipleStreamData = {
recipients: recipients, // Array of recipient objects.
tokenId: "DNw99999M7e24g99999999WJirKeZ5fQc6KY999999gK", // Token mint address.
start: 1643363040, // Timestamp (in seconds) when the stream/token vesting starts.
period: 1, // Time step (period) in seconds per which the unlocking occurs.
cliff: 1643363160, // Vesting contract "cliff" timestamp in seconds.
canTopup: false, // setting to FALSE will effectively create a vesting contract.
cancelableBySender: true, // Whether or not sender can cancel the stream.
cancelableByRecipient: false, // Whether or not recipient can cancel the stream.
transferableBySender: true, // Whether or not sender can transfer the stream.
transferableByRecipient: false, // Whether or not recipient can transfer the stream.
automaticWithdrawal: true, // Whether or not a 3rd party (e.g. cron job, "cranker") can initiate a token withdraw/transfer.
withdrawalFrequency: 10, // Relevant when automatic withdrawal is enabled. If greater than 0 our withdrawor will take care of withdrawals. If equal to 0 our withdrawor will skip, but everyone else can initiate withdrawals.
partner: undefined, // (optional) Partner's wallet address (string | undefined).
};
try {
const { txs } = await client.createMultiple(createMultiStreamsParams, solanaCreateParams);
} catch (exception) {
// handle exception
}
Please note that transaction fees for the scheduled transfers are paid upfront by the stream creator (sender).
All Stream Clients return ICreateResult
object (createMultiple
returns an Array) that has the following structure
interface ICreateResult {
ixs: (TransactionInstruction | TransactionPayload)[];
txId: string;
metadataId: MetadataId;
}
metadataId
is the id of the created stream.
To fetch the unlocked amount from a Stream see Fetching unlocked amount
const withdrawStreamParams: Types.IWithdrawData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier (address) of a stream to be withdrawn from.
amount: getBN(100, 9), // Requested amount to withdraw. If stream is completed, the whole amount will be withdrawn.
};
try {
const { ixs, tx } = await client.withdraw(withdrawStreamParams, solanaInteractParams);
} catch (exception) {
// handle exception
}
This configuration controls whether automatic withdrawal are enabled for a Stream. automaticWithdrawal
enables or disables this functionality. If set to true
it will be possible for 3rd parties to withdraw the Stream on the recipient's behalf. This is also controlled by withdrawalFrequency
. Setting withdrawalFrequency
to >0
will cause our own withdrawor to automatically withdraw funds to the recipient's wallet with the specifed frequency. Passing 0
will make our withdrawor ignore the Stream for auto-withdrawing, but it will be possible for others to initiate the witdrawal process.
const topupStreamParams: ITopUpData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier (address) of a stream to be topped up.
amount: getBN(100, 9), // Specified amount to topup (increases deposited amount).
};
// Use appropriate chain-specific parameters (see Chain-specific Parameters section above)
// Note: For Solana, you can add isNative: true for wSOL streams
try {
const { ixs, tx } = await client.topup(topupStreamParams, solanaInteractParams);
} catch (exception) {
// handle exception
}
const data: ITransferData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier (address) of the stream to be transferred
newRecipient: "99h00075bKjVg000000tLdk4w42NyG3Mv0000dc0M99", // Identifier (address) of a stream to be transferred.
};
// Use appropriate chain-specific parameters (see Chain-specific Parameters section above)
try {
const { tx } = await client.transfer(data, solanaInteractParams);
} catch (exception) {
// handle exception
}
const cancelStreamParams: ICancelData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier of a stream to be canceled.
};
// Use appropriate chain-specific parameters (see Chain-specific Parameters section above)
try {
const { ixs, tx } = await client.cancel(cancelStreamParams, solanaInteractParams);
} catch (exception) {
// handle exception
}
const updateStreamParams: IUpdateData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier of a stream to update.
enableAutomaticWithdrawal: true, // [optional], allows to enable AW if it wasn't, disable is not possible
withdrawFrequency: 60, // [optional], allows to update withdrawal frequency, may result in additional AW fees
amountPerPeriod: getBN(10, 9), // [optional], allows to update release amount effective on next unlock
}
// Use appropriate chain-specific parameters (see Chain-specific Parameters section above)
try {
const { ixs, tx } = await client.update(updateStreamParams, solanaInteractParams);
} catch (exception) {
// handle exception
}
const data: IGetOneData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier of a stream
};
try {
const stream = await client.getOne(data);
} catch (exception) {
// handle exception
}
const stream = await client.getOne({
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA",
});
const unlocked = stream.unlocked(tsInSeconds); // BN amount unlocked at the tsInSeconds
console.log(getNumberFromBN(unlocked, 9));
const stream = await client.getOne({
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA",
});
const withdrawn = stream.withdrawnAmount; // bn amount withdrawn already
console.log(getNumberFromBN(withdrawn, 9));
const remaining = stream.remaining(9); // amount of remaining funds
console.log(remaining);
const data: IGetAllData = {
address: "99h00075bKjVg000000tLdk4w42NyG3Mv0000dc0M99",
type: StreamType.All, // StreamType.Vesting, StreamType.Lock, StreamType.Payment
direction: StreamDirection.All, // StreamDirection.Outgoing, StreamDirection.Incoming
};
try {
const streams = client.get(data);
} catch (exception) {
// handle exception
}
Solana RPC is pretty rich in what data it can allow to filter by, so we expose a separate searchStreams
method on SolanaStreamClient
:
// All parameters are optional, so in theory you can just fetch all Streams
const params = {
mint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
sender: "AKp8CxGbhsrsEsCFUtx7e3MWyW7SWi1uuSqv6N4BEohJ",
recipient: "9mqcpDjCHCPmttJp2t477oJ71NdAvJeSus8BcCrrvwy5",
}
// nativeStreamClient is exposed on a GenericStreamClient, you can also use SolanaStreamClient directly
// Return an Array of objects {publicKey: PublicKey, account: Stream}
const streams = await client.nativeStreamClient.searchStreams(params);
GenericStreamClient
wraps all errors when making on-chain calls with ContractError
error class:
contractErrorCode
property that can be further mapped to a specific Contract errorcreateMultiple
method errors are wrapped individually for every recipient addressContractErrorCode
and SolanaContractErrorCode
to see short description for each errorA public map of protocol errors is available here.
Streamflow protocol program IDs
Solana | |
---|---|
Devet | HqDGZjaVRXJ9MGRQEw7qDc2rAr6iH1n1kAQdCZaCMfMZ |
Mainnet | strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m |
Aptos | |
---|---|
Testnet | 0xc6737de143d91b2f99a7e490d4f8348fdfa3bdd1eb8737a27d0455f8a3625688 |
Mainnet | 0x9009d93d52576bf9ac6dc6cf10b870610bcb316342fef6eff80662fbbfce51b0 |
Sui | |
---|---|
Testnet | 0xf1916c119a6c917d4b36f96ffc0443930745789f3126a716e05a62223c48993a |
Mainnet | 0xa283fd6b45f1103176e7ae27e870c89df7c8783b15345e2b13faa81ec25c4fa6 |
All BN amounts are denominated in their smallest units.
E.g, if the amount is 1 SOL than this amount in lamports is 1000 \* 10^9 = 1_000_000_000.
And new BN(1_000_000_000)
is used.
Use getBN
and getNumberFromBN
utility functions for conversions between BN
and Number
types.
getBN(1, 9)
is equal to new BN(1_000_000_000)
getNumberFromBN(new BN(1_000_000_000), 9)
will return 1