Consider the following contracts:
contract;
use std::{context::balance_of, context::msg_amount, token::*};
use token_abi::Token;
impl Token for Contract {
fn mint_coins(mint_amount: u64, a: u32) {
mint(mint_amount);
}
fn mint_to_addresses(mint_amount: u64, addresses: [Address; 3]) {
let mut counter = 0;
while counter < 3 {
mint_to_address(mint_amount, addresses[counter]);
counter = counter + 1;
}
}
fn burn_coins(burn_amount: u64, a: u32) {
burn(burn_amount);
}
fn force_transfer_coins(coins: u64, asset_id: ContractId, target: ContractId) {
force_transfer_to_contract(coins, asset_id, target);
}
fn transfer_coins_to_output(coins: u64, asset_id: ContractId, recipient: Address) {
transfer_to_address(coins, asset_id, recipient);
}
fn get_balance(target: ContractId, asset_id: ContractId) -> u64 {
balance_of(target, asset_id)
}
fn get_msg_amount() -> u64 {
msg_amount()
}
}
contract;
use std::{
call_frames::{
contract_id,
msg_asset_id,
},
context::msg_amount,
token::{
mint_to_address,
transfer_to_address,
},
};
abi LiquidityPool {
#[storage(write)]
fn set_base_token(base_token_id: b256) -> ();
#[storage(read), payable]
fn deposit(recipient: Address);
#[storage(read), payable]
fn withdraw(recipient: Address);
}
storage {
base_token: b256 = 0x0000000000000000000000000000000000000000000000000000000000000000,
}
impl LiquidityPool for Contract {
#[storage(write)]
fn set_base_token(base_token_id: b256) {
storage.base_token.write(base_token_id);
}
#[storage(read), payable]
fn deposit(recipient: Address) {
log(msg_asset_id());
log(msg_amount());
log(msg_amount());
assert(ContractId::from(storage.base_token.read()) == msg_asset_id());
assert(0 < msg_amount());
// Mint two times the amount.
let amount_to_mint = msg_amount() * 2;
// Mint some LP token based upon the amount of the base token.
mint_to_address(amount_to_mint, recipient);
}
#[storage(read), payable]
fn withdraw(recipient: Address) {
assert(contract_id() == msg_asset_id());
assert(0 < msg_amount());
// Amount to withdraw.
let amount_to_transfer = msg_amount() / 2;
// Transfer base token to recipient.
transfer_to_address(amount_to_transfer, ContractId::from(storage.base_token.read()), recipient);
}
}
The first contract is a contract that represents a simple token.
The second contract, as its name suggests, represents a simplified example of a liquidity pool contract. The method deposit() expects you to supply an arbitrary amount of the base_token
. As a result, it mints double the amount of the liquidity asset to the calling address. Analogously, if you call withdraw()
supplying it with the liquidity asset, it will transfer half that amount of the base_token
back to the calling address except for deducting it from the contract balance instead of minting it.
The first step towards interacting with any contract in the TypeScript SDK is using the typegen
CLI utility to generate type-safe bindings for the contract methods:
$ npx fuels typegen -i ./contract/out/debug/*-abi.json -o ./contract-types
Next, let's setup a Wallet
and seed it with some coins. We will need these coins to deploy the contracts and to interact with them.
const provider = new Provider('http://127.0.0.1:4000/graphql');
const PRIVATE_KEY = '0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301';
const wallet = Wallet.fromPrivateKey(PRIVATE_KEY, provider);
await seedTestWallet(wallet, [{ assetId: NativeAssetId, amount: bn(100_000) }]);
Let's now deploy both the contracts and set them up.
const tokenContractBytecode = readFileSync(
join(__dirname, '../test-projects/token_contract/out/debug/token_contract.bin')
);
const tokenContractFactory = new ContractFactory(tokenContractBytecode, tokenContractABI, wallet);
const tokenContract = await tokenContractFactory.deployContract();
const tokenContractID = tokenContract.id;
const liquidityPoolContractBytecode = readFileSync(
join(__dirname, '../test-projects/liquidity-pool/out/debug/liquidity-pool.bin')
);
const liquidityPoolContractFactory = new ContractFactory(
liquidityPoolContractBytecode,
liquidityPoolABI,
wallet
);
const liquidityPoolContract = await liquidityPoolContractFactory.deployContract();
const liquidityPoolContractID = liquidityPoolContract.id;
await liquidityPoolContract.functions.set_base_token(tokenContractID).call();
Next, let's mint some tokens and transfer them to our wallet.
await tokenContract.functions.mint_coins(500, 1).call();
await tokenContract.functions
.transfer_coins_to_output(
200,
{
value: tokenContract.id,
},
{
value: wallet.address.toB256(),
}
)
.txParams({
variableOutputs: 1,
})
.call();
Now, let's deposit some tokens into the liquidity pool contract. Since we have to transfer assets to the contract, we create the appropriate callParams
and chain them to the method call.
await liquidityPoolContract.functions
.deposit({
value: wallet.address.toB256(),
})
.callParams({
forward: {
amount: bn(100),
assetId: tokenContractID.toB256(),
},
})
.call();
As a final demonstration, let's use all our liquidity asset balance to withdraw from the pool and confirm we retrieved the initial amount. For this, we get our liquidity asset balance and supply it to the withdraw()
function via callParams
.
const lpTokenBalance = await wallet.getBalance(liquidityPoolContractID.toB256());
await liquidityPoolContract.functions
.withdraw({
value: wallet.address.toB256(),
})
.callParams({
forward: {
amount: lpTokenBalance,
assetId: liquidityPoolContractID.toB256(),
},
})
.call();