Skip to content

Interact with your Tokenbound Account (TBA) on local blockchain using nodejs

This guide walks you through transferring ETH to and from from a TBA you own, using nodejs scripts for a TBA on your local blockchain node.

Below are the steps we’ll be following:

Prep:

  1. spin up a local test node (anvil) — you should be able to follow the same steps using an EVM compatible chain’s RPC url as well.
    1. Deploy the registry contract on the canonical address
    2. Deploy an account implementation contract
  2. Deploy a test NFT contract
  3. Mint a token to an address you have the private key for

Walkthrough with scripts:

  1. Initialise the SDK with the NFT’s contract address and token id you just minted
  2. Transfer ETH from your wallet to the associated TBA of your NFT contract + token ID (a TBA is always associated with a (nft contract, token id) pair)
  3. Deploy the Tokenbound account for this NFT contract + token id
  4. Transfer ETH from the TBA to your wallet
Source Code

Setup

Clone the repository

$ gh repo clone anggxyz/tb-deploy-demo
$ cd tb-deploy-demo/contracts/erc6551-reference && forge build && cd ../test-nft && forge build && cd ../../

Spin up a local test node (anvil)

$ anvil

Deploy NFT contract

$ cd contracts/test-nft

Set an account’s private key in .env with key PRIVATE_KEY and RPC url with key RPC_URL

$ forge create NFT --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY --constructor-args "test" "TEST"

The output should look something like

 ...
[⠒] Compiling...
[⠃] Compiling 5 files with 0.8.10
[⠊] Solc 0.8.10 finished in 243.92ms
Compiler run successful!
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0x04ff2f73f02b130516afabd60a42be636cb8febf0d2a04481fc80c645d1aa306

Mint an NFT

Set own account address in .env with key ACCOUNT_ADDRESS

$ cast send --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY CONTRACT_ADDRESS "mintTo(address)" $ACCOUNT_ADDRESS

Deploy the Registry contract

$ cd ../erc6551-reference
$ cast rpc anvil_setCode 0x4e59b44847b379578588920ca78fbf26c0b4956c 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3

make sure you have RPC url set in the .env with key RPC_URL

forge script --fork-url=$RPC_URL script/DeployRegistry.s.sol

Setup tokenbound client

We’ll be storing all the constants in a file called constants.ts

// constants.ts
 
 
export const CONSTANTS = {
  "NFT_CONTRACT": NFT_CONTRACT_ADDRESS,
  "NFT_ID": "1",
  "CHAIN_ID": 31337,
  "RPC":"http://127.0.0.1:8545",
  "ACCOUNT_IMPLEMENTATION": ACCOUNT_IMPLEMENTATION_CONTRACT /** (remove this key if using an EVM compatible chain, this key is only required for local/fresh chain)  **/
}

Create a file called client.ts and initialise and export tokenboundClient and tokenboundAccount

// client.ts
 
 
import { TokenboundClient } from "@tokenbound/sdk";
import { JsonRpcProvider, Wallet, formatEther } from "ethers";
import { CONSTANTS } from "./constants"
import { TBAccountParams } from "@tokenbound/sdk/dist/src/TokenboundClient";
 
const { CHAIN_ID, NFT_CONTRACT, NFT_ID, RPC, ACCOUNT_IMPLEMENTATION } = CONSTANTS;
 
export const provider = new JsonRpcProvider(RPC);
 
 
if (!process.env.TEST_ACCOUNT) {
  console.error ("TEST_ACCOUNT private key undefined in .env");
  process.exit();
}
 
 
export const wallet = new Wallet(process.env.TEST_ACCOUNT, provider);
 
 
const tokenboundClient = new TokenboundClient({
  signer: wallet,
  chainId: CHAIN_ID,
  implementationAddress: ACCOUNT_IMPLEMENTATION as `0x${string}`
});
 
export const tokenBoundAccount = tokenboundClient.getAccount({
  tokenContract: NFT_CONTRACT as TBAccountParams["tokenContract"],
  tokenId: NFT_ID,
});
 
// util function to display balance
const displayBalance = async (address: string) => {
  const balance = await provider.getBalance(address);
  console.log(`balance of ${address}: ${formatEther(balance)}`)
}
 
export default tokenboundClient;

Transfer ETH from your wallet to the associated TBA

Transfer ETH from your wallet to the associated TBA of your NFT contract + token ID (a TBA is always associated with a (nft contract, token id) pair)

We create a new script named 1-transfer.ts, this would transfer funds from wallet → TBA

// 1-transfer.ts
 
 
import { formatEther, parseEther } from "ethers";
import { wallet, provider, tokenBoundAccount, displayBalance } from "./client";
 
// transfer eth from Wallet -> TBA
 
(async () => {
  console.log(`sender:`, wallet.address);
  await displayBalance(wallet.address);
 
  const amount = parseEther("1");
  console.log(`sending ${formatEther(amount)} ETH from ${wallet.address} to ${tokenBoundAccount}`);
 
  await wallet.sendTransaction({
    to: tokenBoundAccount,
    value: amount
  })
  await displayBalance(wallet.address);
  await displayBalance(tokenBoundAccount);
})()

Deploy the Tokenbound account for this NFT contract + token id

Before we can interact with the TBA to transfer funds to and from the account, we would need to deploy the account on the deterministic address that was retrieved in the first step when we executed the function getAccount from the SDK

To deploy the account, we create a new script named 2-deploy.ts with the following code:

// 2-deploy.ts
 
import { CONSTANTS } from "./constants"
import { TBAccountParams } from "@tokenbound/sdk/dist/src/TokenboundClient";
const { NFT_CONTRACT, NFT_ID } = CONSTANTS;
import client, {tokenBoundAccount} from "./client";
 
(async () => {
  console.log(`Tokenbound Address ${tokenBoundAccount} for NFT: ${NFT_CONTRACT} and NFT ID: ${NFT_ID}`);
 
  console.log(`deploying account...`);
 
  const account = await client.createAccount({
    tokenContract: NFT_CONTRACT as TBAccountParams["tokenContract"],
    tokenId: NFT_ID,
  });
 
  console.log(account);
})()

Transfer ETH from the TBA to your wallet

Now that the account is deployed we can transfer ETH from the TBA back to the wallet. Creating a new script called 3-transfer.ts

// 3-transfer.ts
 
import { formatEther, parseEther } from "ethers";
import { TBAccountParams } from "@tokenbound/sdk/dist/src/TokenboundClient";
import client, {wallet, provider, tokenBoundAccount, displayBalance} from "./client";
 
// transfer eth from B -> A
(async () => {
  console.log(`wallet: ${wallet.address}`);
  await displayBalance(wallet.address);
  console.log(`TBA address: ${tokenBoundAccount}`)
  displayBalance(tokenBoundAccount)
 
  const amount = parseEther("1");
  console.log(`sending ${formatEther(amount)} ETH from ${tokenBoundAccount} to ${wallet.address}`);
 
  const executedCall = await client.executeCall({
    account: tokenBoundAccount,
    to: wallet.address  as TBAccountParams["tokenContract"],
    value: amount,
    data: "",
  });
  console.log("\n---\n",executedCall,"\n---\n");
 
  await provider.waitForTransaction(executedCall);
  await displayBalance(wallet.address);
  await displayBalance(tokenBoundAccount);
})()