RWA fraction market
This guide outlines the process of deploying a new market using Evergonlab's api-client
. In this use case, ERC-721 or ERC-1155 assets are locked in the vault as collateral to issue fractionalized ERC-20 tokens, which are then sold to investors. The assets held in the vault represent real-world twin NFTs.
Once investors purchase the fractionalized tokens, they must stake them in the staking pool to receive dividends generated by the real-world assets. For detailed instructions on deploying the staking platform and running a staking campaign to fully implement the RWA market use case, refer to the Profit Sharing Pools guide.
Note
This guide relies on the
viem
library, but you are free to use any other Web3 provider such asweb3.js
orethers.js
.
Preface
There are three entities involved in the process:
- Admin - Deploys the fraction market.
- Issuer - Creates the sale.
- Investor - Purchases fractions.
Below is a simplified diagram outlining the entire process, along with the key actions of each entity:

- Admin deploys the Fraction Market.
- The Issuer submits a request to the Fraction Platform to approve the spending of collateral that will be locked in the Vault.
- The Issuer submits a request to the Fraction Platform to create a new market.
- The Fraction Platform processes the request and creates a new Market with the specified number of fractions to be sold to investors. During this process, the platform sets both the softcap and hardcap. If the softcap is not reached, users can claim their invested funds, and the Issuer can unwrap their collateral from the Vault.
- The Investor purchases a specified number of fractions.
- The Investor stakes the purchased fractions into the Staking Pool.
- The Investor can claim rewards after a specified period, as defined in the Staking Pool configuration.
The goal of this guide is to deploy the Fraction Platform, where a Market will be launched to sell fractions to investors.
Important
Note that this guide does not cover the staking steps 6 and 7 outlined in the diagram. For a complete RWA market use case implementation, refer to the Profit Sharing Pools guide.
Additionally, you must deploy a new smart contract and mint the corresponding ERC-721/1155 NFTs, which will represent real-world assets to be locked in the Vault.
Imports
Create a new typescript project, use the following imports and install the required dependencies.
import { api, handleApiResponse, getEnv, Transaction, ApiClient } from "@evergonlabs/tmi-protocol-api-client";
import { Address, createWalletClient, Hash, http} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
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 fraction platform, market, create fractions, approve the sale, buy fractions, and perform other actions. |
"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. |
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 the Fraction 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 ). |
Important
The 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.fractions.market.deployMarket
call. - Pass the data object to the
sendTransaction
function to deploy the fraction platform. - Handle the returned transaction hash using the
handleApiResponse
function. - Return the
platformDetails
object, which contains the address of the fraction platform and the Vault to be used in subsequent function calls. This operation requires the use of thegetMarketDeployedEvent
function, which is included in the API client; It retrieves the platform details by using the transaction hash.
async function deployPlatform(apiClient: ApiClient, viemClient: any) {
const transactionData = handleApiResponse(
await api.fractions.market.deployMarket({
client: apiClient,
body: {
chainId: `${sepolia.id}`,
adminAddress: viemClient.account.address,
discount: {
burnForDiscount: {
burnableTokenAddress: "0xffffffffffffffffffffffffffffffffff"
}
},
cap: {
maxHardCap: 1000000n
},
fee: {
percent: 10
}
},
}))
const platformDetails = handleApiResponse(
await api.fractions.market.getMarketDeployedEvent({
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 fraction platform. |
burnableTokenAddress | Specifies the address of a token that can be burned (sent to the address) in order to receive a discount on platform fees or services. |
fee | Denotes the discount amount to be provided to a user. |
Deploy the platform by passing the apiClient
and adminClient
to the deployPlatform
function call and store the result in the platformReceipt
variable. This will provide the address of the fraction platform for use in subsequent calls.
const platformReceipt = await deployPlatform(apiClient, adminClient)
Roles
When 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: :
'ISSUER'
, and'INVESTOR'
.
'ISSUER'
pool - Grants permission to deploy a market.
'INVESTOR'
- Grants an account the ability to purchase fractions.
At this stage, we have successfully deployed the fraction platform. The address of the platform can be obtained via the diamondAddress
property:
const diamondAddress = platformReceipt.diamondAddress
When the fraction platform is created, a Vault/Wrapper is deployed alongside it, responsible for wrapping and locking various assets as collateral. The Vault's address is required by Issuers who will create their sales. The wrapper address is returned in the platformReceipt
variable:
const wrapperAddress = platformReceipt.wrapperAddress
Creating the market
Before the Issuer creates a new market, they must go through the KYC/KYB process using a wallet supported by ComPilot's service. For example, this could be MetaMask. The Issuer's address will then be allowed to create a new market.
After the successful KYC/KYB, the Issuer must approve the Vault to allow the spending of ERC-721 assets, which will be locked as collateral.
Create the wallet client for the Issuer entity:
const issuerClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});
Next, define the approvePlatformWrapper
function, which will be responsible for sending a transaction to approve the wrapper. This function depends on the approveWrapper
function provided by the API client:
async function approvePlatformWrapper(apiClient: ApiClient, viemClient: any, diamondAddress: string, wrapperAddress: string) {
const transactionData = handleApiResponse(
await api.fractions.approvals.approveWrapper({
client: apiClient,
body: {
marketAddress: diamondAddress,
wrapperAddress: wrapperAddress
}}));
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult;
}
Call the approvePlatformWrapper
operation to approve the Vault. The result of this operation is optional:
const approveResult = approvePlatformWrapper(apiClient, issuerClient, diamondAddress, wrapperAddress);
After approving the Vault, you must specify the asset and to be wrapped as the collateral. This guide relies on the ERC-721 as the digital-twin NFT, representing the RWA asset. Therefore, we must use the approveWrapperToSpendErc721
function provided by the API client.
Define the approveCollateral
function to create the necessary logic for asset approval:
async function approveCollateral(apiClient: ApiClient, viemClient: any, wrapperAddress: string, rwaTokenAddress: string) {
const transactionData = handleApiResponse(
await api.fractions.approvals.approveWrapperToSpendErc721({
client: apiClient,
body: {
wrapperAddress: wrapperAddress,
erc721Address: rwaTokenAddress
}
}))
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult;
}
Then invoke the approveCollateral
operation to approve the spending of the ERC-721 asset by the Vault:
const approveAssetResult = approveCollateral(apiClient, issuerClient, wrapperAddress, "0xfffffffffffffffffffffffffffffffffffffffffff");
To allow Issuer deploying a new market, the Admin entity must grant the 'ISSUER'
role.
Define the grantIssuerRole
function, which generates a new transaction object using the grantRole
function, and then sends it via the sendTransaction
invocation.
async function grantIssuerRole(client: ApiClient, viemClient: any, diamondAddress: string) {
const transactionData = handleApiResponse(
await api.fractions.roles.grantRole({
body: {
chainId: `${sepolia.id}`,
marketAddress: diamondAddress,
users: [issuerClient.account.address],
role: 'ISSUER'
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult;
}
Property | Description |
---|---|
client | Expects an instance of the api.createClient to make API calls. |
viemClient | Must specify the adminClient which will grant the 'ISSUER' role to the Issuer entity. |
marketAddress | Set the address of the platform by specifying the diamondAddress variable. |
users | Specify the Issuer address in the array that must be granted with the 'ISSUER' role. |
role | Select the role to grant. For this code example, set it to 'ISSUER' . |
Pass the apiClient
, adminClient
, and diamondAddress
as arguments to the grantCreatorRole
function call:
const marketReceipt = await grantIssuerRole(apiClient, adminClient, diamondAddress)
After granting the 'ISSUER'
role to the Issuer you can begin deploying the market.
Define the createMarket
function with viemClient
, apiClient
, and diamondAddress
as arguments to create a new data object for a transaction and send it using the sendTransaction
function. To create a new market we use the createFractions
method:
async function createMarket(client: ApiClient, viemClient: any, diamondAddress: string) {
const transactionData = handleApiResponse(
await api.fractions.createFractions({
client: apiClient,
body: {
chainId: `${sepolia.id}`,
market: diamondAddress,
wrappedTokens: [{
type: 'ERC721',
address: '0xffffffffffffffffffffffffffffffffffff',
tokenIds: [11111n, 22222n, 33333n, 44444n],
}],
funding: {
pricePerFraction: 5n,
address: '0xfffffffffffffffffffffffffffffffffff',
},
fractions: {
token: '0xfffffffffffffffffffffffffffffffffff',
symbol: 'TST',
name: 'TEST'
},
timeBoundary: {
start: 1741683643801,
end: 1751683643801
},
cap: {
soft: 1000000000n,
hard: 2000000000n
}
}
})
)
const marketDetails = handleApiResponse(
await api.fractions.getFractionsCreatedEvent({
query: {
chainId: `${sepolia.id}`,
hash: await sendTransaction(viemClient, transactionData),
},
}),
);
return marketDetails;
}
Property | Description |
---|---|
client | Expects an instance of the api.createClient to make API calls. |
viemClient | Must specify the issuerClient which will create a new market. |
chainId | Specify the chain id using the imported sepolia . |
market | Set the address of the platform by specifying the diamondAddress variable. |
wrappedTokens | An object where you have to specify details for NFTs that will be locked in the vault. |
type | Expects the standard of the asset to be locked; Can be: 'ERC20' ,'ERC721' or 'ERC1155' . |
address | The smart contract address of the asset to be locked. |
tokenIds | An array of unique identifiers of the NFTs to be locked. |
funding | An object containing details about the market funding. |
pricePerFraction | Sets the price to be paid for one fraction in the specified currency; For instance, USDC. |
address | Specifies the smart contract address whose tokens will be used as the payment for fractions. |
fractions | An object containing details about the fraction tokens. |
token | Represents the smart contract address of the fractions to be sold on the market. |
symbol | The symbol of the fraction. |
name | The name of the fraction. |
timeBoundary | An object that sets the time boundary for the market. |
start | The start time of the market. Must be specified in the UNIX milliseconds. |
end | The end time of the market. Must be specified in the UNIX milliseconds. |
cap | An object that sets the softcap and the hardcap for the market in the currency specified in the funding object. |
softCap | The softcap of the market. |
hardcap | The hardcap of the market. |
Pass the issuerClient
, apiClient
and diamondAddress
arguments to the createMarket
function call to create a new market.
const marketDetails = await createMarket(issuerClient, apiClient, diamondAddress)
Important
The result of this operation provides details about the created market. These details are necessary to obtain the unique market identifier,
campaignId
, which is required for all entities that will interact with the market later in this tutorial.
For ease of use, we create a new campaignId
variable and assign to it the value obtained from the marketDetails
:
const campaignId = marketDetails.campaignId
At this point, the market has been successfully deployed, but it has not yet been activated for investors to purchase fractions. Before any fractions can be sold, each newly created market must be approved by the Admin.
Define the approveMarket
function, which will take the adminClient
, apiClient
, and diamondAddress
as arguments. As shown, we specify the campaignId
that was retrieved in the previous step:
async function approveMarket(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigInt) {
const transactionData = handleApiResponse(
await api.fractions.approveSale({
client: apiClient,
body: {
chainId: `${sepolia.id}`,
marketAddress: diamondAddress,
campaignId: campaignId,
}})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Invoke the approveMarket
function to activate the token sale. The result of this operation is optional:
const marketApproveReceipt = await approveMarket(adminClient, apiClient, diamondAddress, campaignId)
The next steps will involve the Investor interacting with it and purchasing fractions.
Purchasing fractions
To start buying the 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 market, Admin must grant the 'INVESTOR'
role to it.
Define the grantInvestorRole
function whose invocation will rely on the investorClient
wallet data, apiClient
and the address(diamondAddress
), of the fraction platform.
The result of this operation is optional:
async function grantInvestorRole(client: ApiClient, viemClient: any, diamondAddress: string) {
const transactionData = handleApiResponse(
await api.fractions.grantRole({
body: {
chainId: `${sepolia.id}`,
marketAddress: diamondAddress,
users: [investorClient.account.address],
role: 'INVESTOR'
}
})
)
const roleResult = handleApiResponse(
await api.fractions.getMarketDeployEvent({
query: {
chainId: `${sepolia.id}`,
hash: await sendTransaction(viemClient, transactionData),
},
}),
);
return roleResult;
}
Pass the adminClient
, apiClient
and diamondAddress
arguments to the grantInvestorRole
function call. The successful invocation grants the Investor the 'INVESTOR'
role.
const investorRoleReceipt = await grantInvestorRole(adminClient, apiClient, diamondAddress)
Once the role has been successfully granted, the Investor can begin purchasing fractions.
Define the buyFractions
function, which will take the investorClient
, apiClient
, diamondAddress
, campaignId
, and amountToBuy
as arguments. The function generates the data object and sends the transaction using the sendTransaction
.
async function buyFractions(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigInt, amountToBuy: bigInt) {
const transactionData = handleApiResponse(
await api.fractions.purchaseSale({
client: apiClient,
body: {
chainId: `${sepolia.id}`,
market: diamondAddress,
campaignId: campaignId,
amountToBuy: amountToBuy,
}})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Invoke the buyFractions
function to purchase 100
tokens, passing all the required arguments. The return value of this operation is optional:
const buyFractionsReceipt = await buyFractions(investorClient, apiClient, diamondAddress, campaignId, 100n)
After purchasing fractions, the Investor must withdraw them from the platform to his address.
Define the claimFractions
function that will rely on the withdrawSale
call. It expects the investorClient
, apiClient
, diamondAddress
, and campaignId
. The function generates the data object and sends the transaction using the sendTransaction
.
async function claimFractions(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigint) {
const transactionData = handleApiResponse(
await api.fractions.withdrawSale({
client: apiClient,
body: {
market: diamondAddress,
campaignId: campaignId
}})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Pass the required arguments to the claimFractions
function call to withdraw all available fractions from the platform to the Investor's address:
const claimResult = await claimFractions(investorClient, apiClient, diamondAddress, campaignId)
Completing the sale preemptively
In the event that the softcap is reached while the hardcap is not yet reached, the Admin can preemptively complete the sale by executing the completeSale
function. This will release the funds collected from the fraction sales to the Issuer's address.
Define the terminateSale
function, which will accept the issuerClient
, apiClient
, diamondAddress
, and campaignId
as arguments. The function generates the data object and sends the transaction using sendTransaction
.
async function terminateSale(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigint) {
const transactionData = handleApiResponse(
await api.fractions.comleteSale({
client: apiClient,
body: {
chainId: `${sepolia.id}`,
marketAddress: diamondAddress,
campaignId: campaignId
}})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Pass the required arguments in the terminateSale
invocation to complete the sale and withdraw the collected funds to the Issuer's wallet address:
const terminateResult = await terminateSale(issuerClient, apiClient, diamondAddress, campaignId)
Closing remarks
Well done! You've gained a strong grasp of the essential concepts behind the RWA Fraction API
, and we've explored all the methods available in the RWA template. At this point, you should have a clear understanding of how each process operates and how to manage them efficiently.
Updated 1 day ago