Assets module
The assets module provides a set of utilities related to balances, fees, vesting, locks and stakes on Joystream.
Some of the available features include:
- Retrieving account balances available for different purposes (ie. making transfers, paying different kinds of fees, staking etc.)
- Conversion between
JOY
andHAPI
, supporting multiple variable types (number
,BigInt
,string
,BN
) - Establishing all costs associated with executing a specific runtime extrinsic (tx fees, platform fees, bloat bonds, deposits, transfers etc.), how they would affect existing balances and whether an account has sufficient funds to cover them.
Conversion
HAPI to JOY
import { hapiToJoy } from '@joystream/sdk-core/assets' import BN from 'bn.js' // Providing string value log(hapiToJoy('25000000000')) // Providing BigInt value log(hapiToJoy(BigInt(12_750) * BigInt(5_000_000_000))) // Providing number value: log(hapiToJoy(999_999_999)) // Providing BN value log(hapiToJoy(new BN(15_430).mul(new BN(42_000_000_000))))
JOY to HAPI
import { joyToHapi } from '@joystream/sdk-core/assets' log(joyToHapi(1).toString()) log(joyToHapi(12.00001).toString())
Constants
The following constants can be imported from @joystream/sdk-core/assets
:
// Number of decimal places that JOY token supports
export const JOY_DECIMALS = 10
// How much HAPI (smallest JOY token units) makes up 1 JOY
export const HAPI_PER_JOY = 10 ** JOY_DECIMALS
// Joystream existential deposit (in HAPI)
export const EXISTENTIAL_DEPOSIT = BigInt(266_666_560)
Treasury accounts
The assets module exports treasury accounts of different runtime modules, which are typically used to store bloat bonds and other deposits:
import { treasuryAccounts, forumThreadAccount, tokenAmmTreasuryAccount, } from '@joystream/sdk-core/assets' // Module-wide treasuries log(`Content treasury: ${treasuryAccounts.content}`) log(`Project token treasury: ${treasuryAccounts.projectToken}`) log(`Proposals discussion treasury: ${treasuryAccounts.proposalsDiscussion}`) log(`Storage treasury: ${treasuryAccounts.storage}`) // Specific entity-scoped treasuries const threadId = 1 const threadAccount = forumThreadAccount(threadId) log(`Forum thread ${threadId} treasury: ${threadAccount}`) const crtId = 1 const crtAmmAccount = tokenAmmTreasuryAccount(crtId) log(`Token ${crtId} AMM treasury: ${crtAmmAccount}`)
AssetsManager
The main way of interacting with the assets module is through the AssetsManager
class.
It allows you to retrieve account balances, estimate extrinsic costs and more...
Initializing
Standalone
import { createApi } from '@joystream/sdk-core/chain'
import { AssetsManager } from '@joystream/sdk-core/assets'
const api = await createApi(`wss://mainnet.joystream.dev/rpc`)
const assets = new AssetsManager(api)
via JoystreamToolbox
import { createJoystreamToolbox } from '@joystream/sdk-core/toolbox'
const joystreamToolbox = await createJoystreamToolbox({
nodeWsEndpoint: 'wss://mainnet.joystream.dev/rpc',
// ...
})
const { assets } = joystreamToolbox
Checking account balances
The AssetsManager
provides a simple way to retrieve account balances in a format that's typically more useful (than, for example, api.derive.balances.all
) in context of Joystream:
import { knownAddresses } from '@joystream/sdk-core/keys' // ... const { assets } = joystreamToolbox const { alice } = knownAddresses const balances = await assets.getBalances(alice) log(balances)
Balances type
The balances are represented by the following abstraction:
export type Balances = {
// All funds, including locked and reserved
total: bigint
// All funds EXCEPT reserved
free: bigint
// All funds that can be used to pay transaction fees and other fee-like costs
feeUsable: bigint
// All funds that are free to be transferred to another account
transferrable: bigint
}
This representation is directly tied to the abstraction of a Cost
which is described later in this document.
The Balances
representation is still a work in progress and will be expanded with information about vesting and active stakes/locks in the future.
Estimating extrinsic costs
Imagine a user of your application wants to add a new video to Joystream.
Executing content.createVideo
extrinsic involves paying multiple different costs, such as:
- Transaction fee - based on the extrinsic arguments and size,
- Data fee - based on the size of associated video assets (thumbnail, video media file, subtitles etc.),
- Data object bloat bond - based on the number of assets associated with the video and the current bloat bond value in the runtime storage module,
- Video bloat bond - based on the value in the runtime content module.
Typically you would need to calculate those costs in advance in order to:
- Inform the user about them,
- Validate whether the user has sufficient balance to cover them.
AssetsManager
provides a unified interface for dealing with a variety of different costs and allows you display detailed summaries and validate balances without having to write your own logic for each Joystream extrinsic separately.
Take a look at the following example:
import { knownAddresses } from '@joystream/sdk-core/keys' // ... const { txm, data, assets } = joystreamToolbox const { contentFees } = data const { alice } = knownAddresses // Prepare the transaction const tx = txm.meta.content.createVideo({ actor: { Member: 1 }, channelId: 1, assets: { expectedDataSizeFee: contentFees.dataObjectPerMegabyteFee, objectCreationList: [ { ipfsContentId: '0x01', size_: 12_345_678 }, { ipfsContentId: '0x02', size_: 34_567_890 }, // ... ], }, storageBucketsNumWitness: 1, expectedDataObjectStateBloatBond: contentFees.dataObjectStateBloatBondValue, expectedVideoStateBloatBond: contentFees.videoStateBloatBondValue, meta: { title: 'Example video', thumbnailPhoto: 0, video: 1, // ... }, }) // Establish and log all costs const costs = await assets.costsOf(tx, alice) log(costs)
Costs interface
If you run the code above, you will notice that each of the listed costs conforms to the following interface:
export interface Cost {
// What kind of cost is this (for example: MembershipFee)
kind: CostKind
// Whether paying this cost requires the account to stay alive
// (and therefore its totalBalance to stay above EXISTENTIAL_DEPOSIT)
requiresKeepAlive: boolean
// What happens with the funds? (ie. are they burned? deposisted? transferred?)
destiny: FundsDestiny
// Which balance type is used to pay the the cost (eg. free, feeUsable, transferrable)
paidFrom: BalanceType
// Value in HAPI
value: bigint
}
For more details and exact definitions of CostKind
, BalanceType
and FundsDestiny
types, see packages/core/src/assets/types.ts
.
Having such detiled and flexible abstraction of a Cost
enables multiple other features that AssetsManager
provides (see more examples below).
Checking how costs affect balances
AssetsManager
allows you to check how the estimated extrinsic costs will affect different types of balances of a given account:
import { knownAddresses } from '@joystream/sdk-core/keys' // ... const { txm, assets } = joystreamToolbox const { alice } = knownAddresses // Prepare the transaction const tx = txm.meta.members.buyMembership({ handle: 'Example', controllerAccount: alice, rootAccount: alice, metadata: { name: 'Alice', }, }) // Estimate extrinsic costs const costs = await assets.costsOf(tx, alice) // Check current balances const currentBalances = await assets.getBalances(alice) // Check the minimum amount of each balance required to cover given costs const requiredBalances = assets.requiredBalances(costs) // Estimate the new values of balances after paying given costs const balancesAfter = await assets.estimateBalancesAfter(alice, costs) log('Current:', currentBalances) log('Required: ', requiredBalances) log('After: ', balancesAfter)
Ensuring sufficient balances
To check whether an account has sufficient balances to cover all provided costs, you can simply use the canPay
method:
const hasSufficientFunds = await assets.canPay(alice, costs)
if (!hasSufficientFunds) {
console.log("Can't cover the extrinsic costs!")
} else {
console.log('OK!')
}
Estimating costs of multiple extrinsics
If your application/script sends multiple extrinsics at once, either because it runs some operations in batches or deals with more complex, multi-step workflows, you may want to estimate the costs of all those extrinsics together beforehand.
Not all costs can be accurately predicted in advance! (especially if one of the extrinsics you send affects the cost(s) of another)
Imagine a scenario where you batch multiple projectToken.buyOnAmm
calls for the same token.
Each of those calls will increase the price of the token and affect the costs of subsequent calls. AssetsManager
will not take those intermediate runtime state changes into account, leading to an underestimation of the total cost.
AssetsManager
supports all available batch extrinsics (utility.batch
, utility.forceBatch
, utility.batchAll
), so if you're using them to group your extrinsics together, you can simply pass the batch extrinsic to the costsOf
method:
import { knownAddresses } from '@joystream/sdk-core/keys' // ... const { api, txm, assets } = joystreamToolbox const { alice } = knownAddresses // Prepare a batch transaction to buy 5 memberships const batchTx = api.tx.utility.batch( Array.from({ length: 5 }, (_, i) => txm.meta.members.buyMembership({ handle: `Member ${i}`, controllerAccount: alice, rootAccount: alice, }) ) ) // Estimate total costs associated with the batchTx const costs = await assets.costsOf(batchTx, alice) log(costs)
Alternatively, if you're sending extrinsics one-by-one, you can estimate the costs separately and then merge them into a single array:
import { channelRewardAccount } from '@joystream/sdk-core/utils' import { joyToHapi } from '@joystream/sdk-core/assets' import { knownAddresses } from '@joystream/sdk-core/keys' // ... const { txm, assets } = joystreamToolbox const { alice } = knownAddresses const payout1 = txm.meta.content.makeChannelPayment({ memberId: 1, channelRewardAccount: channelRewardAccount(1), amount: joyToHapi(10), }) const payout2 = txm.meta.content.makeChannelPayment({ memberId: 1, channelRewardAccount: channelRewardAccount(2), amount: joyToHapi(5), }) // Estimate total costs const costs = ( await Promise.all([payout1, payout2].map((tx) => assets.costsOf(tx, alice))) ).flat() log(costs)
This costs representation will work perfectly fine with methods like canPay
, estimateBalancesAfter
etc.