In this guide, you will discover how to quickly deploy a new staking platform and run a staking pool for a real-world business use-case , that generates profit and distributes it to investors. Usually, investors must acquire a share of the business to be eligible for dividends.
The staking pool acts as a dividend distribution point, where investors stake ERC-20 fractions to
receive rewards in cryptocurrency. ERC-20 fractions act as shares, where each share is a percentage of the total asset, allowing multiple buyers to own a portion of the business proportional to their investment.
Before proceeding, you must first issue a digital twin NFT representing a real-world asset via the Issuance API. Then, fractionalize the NFT into ERC-20 fractions using the Fraction API. Issuance and fractionalization are beyond the scope of this material.
NoteThis guide relies on the
viemlibrary, but you are free to use any other Web3 provider such asweb3.jsorethers.js.
Preface
There are three entities involved in the process:
- Admin - Deploys a staking platform.
- Issuer - Deploys a staking pool.
- Investor - Stakes, claims rewards and unstakes its position.
Below is a simplified diagram outlining the entire process, along with the key actions of each entity:
- Admin deploys a platform.
- Issuer sends a request to the staking platform to create a new staking pool.
- The staking platform processes the request and deploys a staking pool.
- Investor stakes the ERC-20 fractions.
- Issuer transfers rewards to the staking pool for further distribution.
- Investor claims the distributed rewards.
The end result of this guide is a deployed staking platform, a staking pool, and the interaction of an Investor with the staking pool, including staking, claiming rewards, and unstaking.
Imports
Create a new typescript project, use the following imports and install the required dependencies.
import { api, handleApiResponse, getEnv, ApiResponse, Transaction, ApiClient} from "@evergonlabs/tmi-protocol-api-client";
import { Address, createWalletClient, Hash, http, writeContract} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import { erc20Abi } from 'viem';| Import | Description |
|---|---|
"evergonlabs/tmi-protocol-api-client" | This library is required to construct data objects that are passed as input when executing transactions via the viem.writeContract function and to handle the API responses. |
"viem" | The viem library is used to create the wallet client, which is then used to send transactions to deploy a platform, pool, distribute rewards, stake, claim rewards, and unstake. |
"viem/accounts" | The privateKeyToAccount import is used when defining a wallet client via the createWalletClient call. |
"sepolia" | This import is required because the platform will be deployed to the Sepolia testnet. |
erc20Abi | The ERC-20 ABI interface is used to approve the Issuer balance spending for rewards distribution and to approve the Staker balance spending for staking, unstaking, claiming rewards. |
Defining the API client
The @evergonlab/tmi-protocol-api-client library uses an API client to send transactions. The env property sets the environment to either "test" or "prod". Begin by defining the API client in your code:
const apiClient = api.createClient({
env: getEnv("test"),
});Deploying a staking platform
To begin the deployment you must first create the Admin client by using the createWalletClient function:
const adminClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});| Property | Description |
|---|---|
account | Specify your private key using the privateKeyToAccount function. |
chain | Set the chain property to sepolia. |
transport | Pass your RPC provider URL to the transport property to interact with the EVM network (e.g., Alchemy ). |
ImportantThe process of creating Admin, Issuer, and Investor wallet accounts is identical across this material. As a result, the descriptions of parameters and similar input values apply to all wallet instances.
Define the sendTransaction function that will be responsible for sending transactions using the writeContract function of the viem library.
The transaction argument expects a Transaction type which must be a data object containing transaction details. The viemClient argument expects a client instance created using the createWalletClient function.
The returned Hash is used throughout this guide to get the transaction response.
async function sendTransaction(viemClient:any, transaction: Transaction): Promise<Hash> {
return await viemClient.writeContract({
args: transaction.details.args,
functionName: transaction.details.functionName,
abi: transaction.details.abi,
address: transaction.details.address as Address,
});
}Define the deployPlatform function that will:
- Create a data object for the transaction using the
await api.stakingTemplates.rwa.createPlatformcall. - Pass the data object to the
sendTransactionfunction to deploy the staking platform. - Handle the returned transaction hash using the
handleApiResponsefunction. - Return the
platformDetailsobject, which contains the address of the staking platform to be used in subsequent function calls.
async function deployPlatform(apiClient: ApiClient) {
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.createPlatform({
client: apiClient,
body: {
// pay attention: we are using string literals here
chainId: `${sepolia.id}`,
isSoulbound: true,
adminAddress: adminClient.account.address,
erc721: {
symbol: "TT",
name: "Test",
baseUri: "ipfs://QmYwAPJzv5CZsnAzt8QvV6pMMN4HgfMNXYxa9avYo5bFwK",
},
},
}),
);
const platformDetails = handleApiResponse(
await api.stakingTemplates.rwa.getPlatformDeployEvent({
query: {
chainId: `${sepolia.id}`,
hash: await sendTransaction(adminClient, transactionData),
},
}),
);
return platformDetails;
}| Property | Description |
|---|---|
client | Expects an instance of the api.createClient to make API calls. |
chainId | Specify the chain id using the imported sepolia. |
adminAddress | Represents an account address that must be granted the 'Admin' role for the staking platform. |
erc721 | Initializes the staking position token collection, including the collection name, symbol, and base URL. Each time someone stakes, the corresponding NFT will be issued to the user’s account as confirmation of the staking position. |
isSoulbound | Determines if the minted NFT will be non-transferable and tied to one address. |
Deploy the platform by passing the apiClient to the deployPlatform function call and store the result in the platformReceipt variable. This will provide the address of the staking platform for use in subsequent calls.
const platformReceipt = await deployPlatform(apiClient)
RolesWhen deploying, the Admin entity becomes the owner of the platform, with the
'Admin'role assigned automatically. Only one'Admin'is allowed per platform.The Admin entity can then grant the following roles to other entities: :
'Creator', and'Staker'.
'Creator'pool - Grants permission to deploy a staking pool.
'Staker'- Enables staking capabilities for an account.
At this stage, we have successfully deployed the staking platform. The address of the platform can be obtained via the diamondAddress property:
const diamondAddress = platformReceipt.diamondAddressDeploying a staking pool
First, to deploy a staking pool, you must create the wallet client for the Issuer entity:
const issuerClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});To allow Issuer deploying a new staking pool, the Admin entity must grant the 'Creator' role.
Define the grantCreatorRole function, which generates a new transaction object using the grantRole function, and then sends it via the sendTransaction invocation.
async function grantCreatorRole(viemClient: any,
apiClient: ApiClient,
diamondAddress: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.staking.roles.grantRole({
client: apiClient,
body: {
platform: diamondAddress,
addresses: [issuerClient.account.address],
role: 'Creator'
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
client | Expects an instance of the api.createClient to make API calls. |
platform | Set the address of the platform by specifying the diamondAddress variable. |
addresses | Specify the Issuer address in the array that must be granted with the 'Creator' role. |
role | Select the role to grant. For this code example, set it to 'Creator'. |
Pass the issuerClient, apiClient and diamondAddress arguments to the grantCreatorRole function call:
const roleReceipt = await grantCreatorRole(issuerClient, apiClient, diamondAddress)After granting the 'Creator' role to the Issuer you can begin deploying the staking pool.
Define the deployPool function with viemClient, apiClient, and diamondAddress as arguments to create a new data object for a transaction and send it using the sendTransaction function:
async function deployPool(viemClient: any, apiClient: ApiClient, diamondAddress: string ): Promise<Hash> {
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.createPool({
client: apiClient,
body: {
platform: diamondAddress,
startDate: new Date(),
hardcapAmount: "100000",
distribution: {
duration: "3600",
distributor: issuerClient.account.address,
},
erc20Input: {
contract: "0xffffffffffffffffffffffffffffffffffffffff",
amountOfTokensPerPacket: "20",
},
erc20Reward: {
contract: "0xffffffffffffffffffffffffffffffffffffffff",
amountOfTokensPerPacket: "5",
},
},
})
);
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
platform | Specifies the address of the platform, represented by the diamondAddress variable. |
startDate | Defines the time when a pool begins. The pool can start at a specified future time, if provided. Must be of the Date type. |
hardcapAmount | Specifies the maximum number of packets a pool can accept from users. |
duration | Defines the duration in seconds after which rewards are distributed. |
distribution | An object containing the details of reward distribution. |
distributor | An account address responsible for distributing rewards. |
erc20Input | An object containing details about the input ERC-20 fractions that users must stake in the pool. |
contract | Specifies the smart contract address of the ERC-20 asset used as input for the pool. |
amountOfTokensPerPacket | Defines the number of tokens required to form one input packet. |
erc20Reward | An object containing details about the ERC-20 reward asset that users will receive. |
contract | Specifies the smart contract address whose tokens are used as rewards in the pool. |
amountOfTokensPerPacket | Defines the number of reward tokens required to form one reward packet. |
Pass the issuerClient, apiClient and diamondAddress arguments to the deployPool function call to create a new staking pool.
const poolHash = await deployPool(issuerClient, apiClient, diamondAddress)
ImportantThe result of this operation is a transaction hash, which is assigned to the
poolHashvariable. This hash is necessary to obtain the unique pool identifier,poolId, and is required for all entities that will interact with the staking pool later in this tutorial.
To obtain the poolId, parse the transaction hash using the getCreatePoolEvent function. The result will then be properly parsed for further use with the handleApiResponse function.
const poolDetails = handleApiResponse(
await api.stakingTemplates.rwa.getCreatePoolEvent({
query: {
chainId: `${sepolia.id}`,
hash: poolHash
},
}),
);For ease of use, we create a new poolId variable and assign to it the value obtained from the poolDetails:
const poolId = poolDetails.poolIdAt this point, the staking pool has been successfully deployed, and the next steps will involve the Investor interacting with it.
Staking
To begin staking ERC-20 fractions, you must create the wallet client for the Investor entity:
const investorClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});To enable Investor interacting with the staking platform, Issuer must grant the 'Staker' role to it.
Define the grantStakerRole function whose invocation will rely on the investorClient wallet data, apiClient and the address(diamondAddress), of the staking platform.
The result of this operation is optional:
async function grantStakerRole(viemClient: any, apiClient: ApiClient, diamondAddress: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.staking.roles.grantRole({
client: apiClient,
body: {
platform: diamondAddress,
addresses: [investorClient.account.address],
role: 'Staker'
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}Pass the investorClient, apiClient and diamondAddress arguments to the grantCreatorRole function call. The successful invocation grants the Investor the 'Staker' role.
const stakerRoleReceipt = await grantStakerRole(investorClient, apiClient, diamondAddress)After successfully granting the role, the Investor must approve the staking platform to allow spending a specific amount of ERC-20 fractions intended for staking.
Define the approveERC20Fractions function that will execute the approval by using the writeContract function which was imported from the viem library:
async function approveERC20Fractions(viemClient: any, spender:any,
amount: number, tokenAddress: string): Promise<Hash> {
const txHash = await viemClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spender, amount],
});
return txHash
}| Property | Description |
|---|---|
address | The address of the smart contract whose ERC-20 tokens are used as input in the staking pool. |
abi | Refers to a predefined set of Application Binary Interfaces (ABIs) for interacting with ERC-20 tokens. This function depends on the erc20Abi import from the viem library. |
functionName | The name of the function to call on the smart contract; in this case, we call the 'approve' function on the smart contract defined in the address property. |
args | Data used in the invocation of the 'approve' function. It expects the spender and amount arguments. |
spender | Specifies the address of the staking platform that will spend the amount of ERC-20 tokens. |
amount | Determines the amount of ERC-20 tokens to be approved for spending on the staking operation. |
Invoke the approveERC20Fractions function passing all required arguments:
const amount = 50000
const tokenAddress = '0xffffffffffffffffffffffffffffffffffffffff'
const investorApproveHash = approveERC20Fractions(investorClient, diamondAddress,
amount, tokenAddress)After the successful approval, you can begin staking the ERC-20 fractions.
Define the stakeFractions function, which will utilize the stake function from the @evergonlabs library. This function will create the transaction data object and send it via sendTransaction.
async function stakeFractions(viemClient: any, apiClient: ApiClient,
diamondAddress: string, poolId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.stake({
client: apiClient,
body: {
platform: diamondAddress,
poolId: poolId,
amount: amount
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
platform | The address of the staking platform. |
poolId | The unique identifier of the pool where tokens are staked. |
amount | The amount of tokens to be staked. |
Note that when executing the stakeFractions function transaction we pass the amountPackets variable specifying the number of packets to stake.
The amountPackets value ('2500') differs from amount ('50000') because, when deploying the staking pool, we set the amountOfTokensPerPacket variable to "20". This means that to stake '50000' tokens, we need to create 2500 packets. Therefore, we specify 2500 as the input value to indicate how many packets should be staked.
const amountPackets = '2500'
const stakeReceipt = await stakeFractions(investorClient, apiClient,
diamondAddress, poolId, amountPackets)After invoking the function, the Investor successfully stakes 2500 packets, or 50000 tokens, in other words. This action triggers the staking platform to issue a staking position NFT, which is used by an account to confirm its ownership of the stake in the pool.
ImportantThe staking position NFT data is mandatory to unstake, restake, and claim rewards. You need to obtain it by parsing the hash of the
stakeoperation
Parse the hash assigned to the stakeReceipt variable by calling the getStakeEvent function:
const stakeData = await api.stakingTemplates.rwa.getStakeEvent({
query: {
chainId: `${sepolia.id}`,
hash: stakeReceipt
}
})Then assign the unique identifier of the staking position to the stakeId variable:
const stakeId = stakeData.data!.stakeIdNow you can use the stakeId as an input for the unstake, restake, and claimRewards functions to manage your stake.
Distributing rewards
To begin distributing rewards for the Investor, the Issuer must first approve the staking platform to spend the specified amount of tokens.
Define the approveERC20Rewards function that will execute the approval by using the writeContract function which was imported from the viem library:
async function approveERC20Rewards(viemClient: any, spender:any,
amount: number, tokenAddress: string): Promise<Hash> {
const txHash = await viemClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spender, amount],
});
return txHash
}| Property | Description |
|---|---|
address | The address of the smart contract whose ERC-20 tokens are used as reward in the staking pool. |
abi | Refers to a predefined set of Application Binary Interfaces (ABIs) for interacting with ERC-20 tokens. This function depends on the erc20Abi import from the viem library. |
functionName | The name of the function to call on the smart contract; in this case, we call the 'approve' function on the smart contract defined in the address property. |
args | Data used in the invocation of the 'approve' function. It expects the spender and amount arguments. |
spender | Specifies the address of the staking platform that will spend the amount of ERC-20 tokens. |
amount | Determines the amount of ERC-20 reward tokens to be approved for spending. |
Then pass the required arguments to the approveERC20Rewards function to invoke it:
const amountRewards = 500000
const rewardsSCAddress = '0xffffffffffffffffffffffffffffffffffffffff'
const issuerApproveHash = approveERC20Rewards(investorClient, diamondAddress,
amount, tokenAddress)Now you can begin distributing rewards as an Issuer entity.
Define the distributeRewards function, which will construct the data object via the notifyRewards invocation and send a transaction using this object in the sendTransaction call:
async function distributeRewards(viemClient: any, apiClient: ApiClient,
diamondAddress: string, poolId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.notifyRewards({
client: apiClient,
body: {
platform: diamondAddress,
poolId: poolId,
amount: amount
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
platform | The address of the staking platform. |
poolId | The unique identifier of the pool where rewards must be distributed. |
amount | The amount of tokens to be distributed as rewards. |
Then execute the distributeRewards function by passing all required arguments. The result of this operation is optional:
const rewardsHash = distributeRewards(issuerClient, apiClient, diamondAddress, poolId, `50000`)After executing the function, the staking pool has received rewards for distribution, and the Investor can now begin claiming rewards.
Claiming rewards
Define the claimRewards function for the Investor to receive their distributed amount:
async function claimRewards(viemClient: any, apiClient: ApiClient,
diamondAddress: string, stakeId: string,): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.getReward({
client: apiClient,
body: {
platform: diamondAddress,
stakeId: stakeId
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
platform | Specifies the address of the staking platform. The diamondAddress variable will be used as an input when invoking the claimRewards function. |
stakeId | The unique identifier of the staking position. |
Invoke the claimRewards function with the required arguments. The result of this operation is optional:
const claimHash = claimRewards(investorClient, apiClient, diamondAddress, stakeId)The Investor has successfully claimed their rewards. The following sections of this material cover partial unstaking and unstaking operations.
Partial unstaking
Partial unstaking refers to the process of withdrawing a portion of the staked tokens while leaving the remaining tokens staked.
Define the pUnstake function which will construct the data object via the partialUnstake invocation and send a transaction using this object in the sendTransaction call:
async function pUnstake(viemClient: any, apiClient: ApiClient,
diamondAddress: string, stakeId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.partialUnstake({
client: apiClient,
body: {
platform: diamondAddress,
stakeId: stakeId,
amount: amount
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
platform | Specifies the address of the staking platform. The diamondAddress variable will be used as an input when invoking the pUnstake operation. |
stakeId | The unique identifier of the staking position. The stakeId variable will be used as an input when invoking the pUnstake operation. |
amount | The number of input packets to withdraw from the position, reducing its size. |
Invoke the pUnstake operation by passing the required arguments. The result of this operation is optional:
const pUnstakeHash = pUnstake(investorClient, apiClient, diamondAddress, stakeId, '1000')As you can see, the invocation withdraws '1000' packets from the current staking position.
Unstaking
Unstaking means that an account withdraws all input packets from the current position, effectively closing it.
Define the closePosition function which will construct the data object via the unstake invocation and send a transaction using this object in the sendTransaction call:
async function closePosition(viemClient: any, apiClient: ApiClient,
diamondAddress: string, stakeId: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.unstake({
client: apiClient,
body: {
platform: diamondAddress,
stakeId: stakeId,
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}| Property | Description |
|---|---|
platform | Specifies the address of the staking platform. The diamondAddress variable will be used as an input when invoking the closePosition operation. |
stakeId | The unique identifier of the staking position. The stakeId variable will be used as an input when invoking the closePosition operation. |
Invoke the closePosition function with the required arguments. The result of this operation is optional:
const closePositionHash = closePosition(investorClient, apiClient, diamondAddress, stakeId)The Investor has withdrawn all input packets from their position.
Closing remarks
Well done! You've successfully grasped the essential concepts of using the Fractions API, and we've explored all the methods included in the fractions template.
At this point, you should have a strong understanding of how each process functions and how to navigate them efficiently.
