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 { getBN, getNumberFromBN } from "@streamflow/stream";
Check the SDK for other types and utility functions.
Before creating and manipulating streams, a Solana client 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"
);
Each method requires some shared 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)
};
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 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)
Price-based Streams (also known as Aligned or Dynamic Streams) are a type of Stream where the unlock rate dynamically adjusts based on the price of a token as reported by a price oracle. The unlock percentage scales between minPercentage and maxPercentage based on whether the current price is at or below minPrice or at or above maxPrice. This allows for vesting schedules that accelerate or decelerate based on token price performance.
Price-based Streams require all the standard stream parameters plus additional price-based configuration parameters. The unlock rate is recalculated periodically based on the oracle price, making the vesting schedule responsive to market conditions.
Streamflow's custom oracles can be fetched through the Oracle API
https://oracle-api-public.streamflow.finance/oracle/{mint}For more details creating a Stream with arbitrary configuration see General Stream creation.
The following code will create a Price-based Stream that adjusts its unlock rate based on token price. When the price is at or below minPrice, it unlocks at minPercentage rate. When the price is at or above maxPrice, it unlocks at maxPercentage rate. For prices between these bounds, the unlock rate scales proportionally.
const tokenDecimals = 9;
const totalAmount = getBN(1000, tokenDecimals);
const cliffAmount = getBN(100, tokenDecimals);
const day = 60 * 60 * 24; // in seconds
const twoWeeks = day * 14;
const mint = "DNw99999M7e24g99999999WJirKeZ5fQc6KY999999gK"
const remainingAmount = totalAmount.sub(cliffAmount);
const baseAmountPerPeriod = remainingAmount.divn(twoWeeks);
const priceOracleAddress = await fetch(`https://oracle-api.streamflow.finance/oracle/${mint}`).then(res => res.json()).then(data => data.address);
const createPriceBasedParams: Types.ICreateAlignedStreamData = {
recipient: "4ih00075bKjVg000000tLdk4w42NyG3Mv0000dc0M00", // Recipient address.
tokenId: mint, // Token mint address.
start: 1643363040, // Timestamp (in seconds) when the stream starts.
amount: totalAmount,
period: day, // 1 day for daily unlock updates
cliff: 1643363040, // Cliff timestamp in seconds.
cliffAmount: cliffAmount, // Amount unlocked at the "cliff" timestamp.
amountPerPeriod: baseAmountPerPeriod, // Base release rate (will be adjusted based on price).
name: "Price-based Vesting for Jane Doe.", // The stream name or subject.
transferableBySender: false, // not possible for aligned streams
automaticWithdrawal: false, // not possible for aligned streams
// Price-based configuration parameters - NOTE: all of these must be present in the creation order for the created Stream to be price-based
minPrice: 0.05, // Minimum price threshold. If current price <= minPrice, minPercentage is used.
maxPrice: 1.0, // Maximum price threshold. If current price >= maxPrice, maxPercentage is used.
minPercentage: 1, // Minimum unlock percentage (in basis points, e.g., 1 = 0.01%). Used when price <= minPrice.
maxPercentage: 1000, // Maximum unlock percentage (in basis points, e.g., 1000 = 10%). Used when price >= maxPrice.
// If either of these are omitted or oracleType is set to "none" the resulting Stream will behave like a regular Stream
oracleType: "test", // "test" is passed if you are using Streamflow's price oracles, you can also pass "pyth" if you are passing a Pyth price oracle to priceOracle
priceOracle: priceOracleAddress, // Address of the price oracle, either a Streamflow oracle address, or a Pyth oracle address.
skipInitial: false, // [optional] Whether to skip initial unlock amount update when the stream starts. Defaults to false.
tickSize: 1, // [optional] Tick size for percentage calculations when price is between minPrice and maxPrice. Defaults to 1.
};
try {
const { ixs, tx, metadata } = await client.create(createPriceBasedParams, solanaCreateParams);
} catch (exception) {
}
const { ixs } = await client.prepareCreateStreamInstructions(createPriceBasedParams, solanaCreateParams)
You can also create a Stream that behaves similar to a token Lock and unlocks at a specific price. There are some technical limitations that cause a small portion of the tokens to be unlocked over time that depends on the contract amount and number of decimals, but for most cases the time it would take for the whole contract to be unlocked would be measured in decades or even hundreds of years. You can achieve this with the following settings:
const targetPrice = 100; // Price at which you want tokens to unlock
const minPrice = targetPrice - 1;
const maxPrice = targetPrice;
const minPercentage = 0;
const maxPercentage = 100;
const totalAmount = new BN(1000000);
const amountPerPeriod = totalAmount;
const cliffAmount = new BN(0); // No cliff for price-based lock
const createPriceBasedParams: Types.ICreateAlignedStreamData = {
recipient: "4ih00075bKjVg000000tLdk4w42NyG3Mv0000dc0M00", // Recipient address.
tokenId: mint, // Token mint address.
start: 1643363040, // Timestamp (in seconds) when the stream starts.
amount: totalAmount,
period: 30, // 30 seconds so in case the target price is reached, the oracle has 30 seconds to fetch the price and update the contract release rate
cliff: 1643363040, // Cliff timestamp in seconds (should match start for price-based lock).
cliffAmount: cliffAmount, // Amount unlocked at the "cliff" timestamp.
amountPerPeriod: amountPerPeriod, // Will be updated to 1 as long as price below targetPrice,
// meaning that tokens are unlocked at rate of 1 smallest denominator of the token per each period (if token has 9 decimals that is 0.000000001 token every 30 seconds)
name: "Price-based Lock for Jane Doe.",
transferableBySender: false, // not possible for aligned streams
automaticWithdrawal: false, // not possible for aligned streams
// Price-based configuration parameters - NOTE: all of these must be present in the creation order for the created Stream to be price-based
minPrice, // Minimum price threshold. If current price <= minPrice, minPercentage is used.
maxPrice, // Maximum price threshold. If current price >= maxPrice, maxPercentage is used.
minPercentage, // Minimum unlock percentage (in basis points, e.g., 1 = 0.01%). Used when price <= minPrice.
maxPercentage, // Maximum unlock percentage (in basis points, e.g., 1000 = 10%). Used when price >= maxPrice.
// If either of these are omitted or oracleType is set to "none" the resulting Stream will behave like a regular Stream
oracleType: "test", // "test" is passed if you are using Streamflow's price oracles, you can also pass "pyth" if you are passing a Pyth price oracle to priceOracle
priceOracle: priceOracleAddress, // Address of the price oracle, either a Streamflow oracle address, or a Pyth oracle address.
skipInitial: false, // In this case it should not be set to true
// These parameters make sense for a price-based token lock
floorPrice: 20, // lower bound targetPrice - at or below this price tokens will be unlocked
expiryTime: 1643364640, // If the targetPrice has not been reached by this point in time, the tokens will be unlocked
expiryPercentage: maxPercentage, // The percentage unlock rate of tokens after expiryTime, it should be maxPercentage or 100
};
When you fetch a price-based stream using getOne(), get() or searchStreams(), it automatically returns an AlignedContract instance if the stream is price-based. You can check if a stream is aligned and access its price-based properties:
import { isAligned, getNumberFromBN } from "@streamflow/stream";
const tokenDecimals = 9; // Token decimals for the stream's token
const stream = await client.getOne({
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA",
});
// Check if the stream is price-based (aligned)
if (isAligned(stream)) {
console.log("Min Price:", stream.minPrice);
console.log("Max Price:", stream.maxPrice);
console.log("Min Percentage:", stream.minPercentage);
console.log("Max Percentage:", stream.maxPercentage);
console.log("Tick Size:", stream.tickSize);
console.log("Oracle Type:", stream.oracleType); // "test", "pyth", or "none"
console.log("Price Oracle Address:", stream.priceOracle);
console.log("Initial Price:", stream.initialPrice);
console.log("Last Price:", stream.lastPrice);
console.log("Last Amount Update Time:", stream.lastAmountUpdateTime);
console.log("Initial Amount Per Period:", getNumberFromBN(stream.initialAmountPerPeriod, tokenDecimals));
console.log("Expiry Time:", stream.expiryTime);
console.log("Expiry Percentage:", stream.expiryPercentage);
console.log("Floor Price:", stream.floorPrice);
console.log("Proxy Address:", stream.proxyAddress);
}
All standard stream properties (like amount, withdrawnAmount, unlocked(), etc.) are also available on aligned streams, as AlignedContract extends the base Contract class.
Note: Price-based Streams use a separate proxy program (Aligned Unlocks) to manage the dynamic unlock logic. The unlock rate is recalculated periodically based on the oracle price. Automatic withdrawal and rate updates are not supported for price-based streams.
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).
};
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).
};
// Note: You can add isNative: true for wSOL streams to the solanaInteractParams
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.
};
try {
const { tx } = await client.transfer(data, solanaInteractParams);
} catch (exception) {
// handle exception
}
const cancelStreamParams: ICancelData = {
id: "AAAAyotqTZZMAAAAmsD1JAgksT8NVAAAASfrGB5RAAAA", // Identifier of a stream to be canceled.
};
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
}
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
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 |
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