Skip to content
On this page

Transfer TIP-3 Tokens

Now that we have minted TIP-3 tokens, it's time to transfer them. As explained in the transfer Using an Account section, the TIP-3 standard has two implementations for the transfer concept, both of which we will cover. It's worth noting that both of these methods are implemented within the MultiWalletTIP-3 contract, so we will be exploring them in detail.

Step 1: Write Transfer Script

In the code sample below, we will demonstrate how to utilize both if the transfer functions of the TIP-3 standard using locklift tool and MultiWalletTIP3 contract.

We use the previously written script stats from the mint tip3 tokens section for the following script.

INFO

Before we start to write our scripts we need to make sure that there is a file named 06-transfer-tip3.ts in the script folder in the project root.

Transferring tokens using everscale-inpage-provider MultiWalletTIP3 contract:

WARNING

  • Notice that if the notify parameter be true for the transaction, the change will be sent back to the sender accounts tokenWallet contract !!

So if you want the change back into your account contract leave the notify unchecked !!


typescript
/*
     Transfer tokens using transfer method
  */

// WE know that bob doesn't have any token wallet of that root since we didn't it
console.log(`Alice has token wallet ? ${
  (
    await getWalletData(
      aliceMultiWalletContract,
      tokenRootContract.address
    )
  ).tokenWallet.toString() != zeroAddress.toString()
} \n
   Alice balance before transfer: ${
     (
       await getWalletData(
         aliceMultiWalletContract,
         tokenRootContract.address
       )
     ).balance /
     10 ** deployRootFromDeployerParams.decimals
   }`);

console.log(`Bob has token wallet ? ${
  (
    await getWalletData(
      bobMultiWalletContract,
      tokenRootContract.address
    )
  ).tokenWallet.toString() != zeroAddress.toString()
} \n
  Bob balance before transfer: ${
    (
      await getWalletData(
        bobMultiWalletContract,
        tokenRootContract.address
      )
    ).balance /
    10 ** deployRootFromDeployerParams.decimals
  }`);

// Amount to transfer
const transferAmount: number =
  10 * 10 ** deployRootFromDeployerParams.decimals;

/*
    Transfer with deployment of a wallet for the recipient account.

    Don't pay attention to notify and payload yet, we'll get back to them.
  */
await aliceMultiWalletContract.methods
  .transfer({
    _amount: transferAmount,
    _recipient: bobMultiWalletContract.address,
    _deployWalletValue: locklift.utils.toNano(2), // We assume bob doesn't have any token wallet
    _tokenRoot: tokenRootContract.address,
  })
  .sendExternal({
    publicKey: signerAlice.publicKey!,
  });

// Fetching the balances after the normal transfer function
console.log(
  `Alice balance after transfer: ${
    (
      await getWalletData(
        aliceMultiWalletContract,
        tokenRootContract.address
      )
    ).balance /
    10 ** deployRootFromDeployerParams.decimals
  }`
); // >> 100
console.log(
  `Bob balance after transfer: ${
    (
      await getWalletData(
        bobMultiWalletContract,
        tokenRootContract.address
      )
    ).balance /
    10 ** deployRootFromDeployerParams.decimals
  }`
); // >> 100

/*
     Transfer tokens to deployed token wallet using transferToWallet method
  */
const transferToWalletAmount: number =
  15 * 10 ** deployRootFromDeployerParams.decimals;

await aliceMultiWalletContract.methods
  .transferToWallet({
    _amount: transferToWalletAmount,
    _recipientTokenWallet: (
      await getWalletData(
        bobMultiWalletContract,
        tokenRootContract.address
      )
    ).tokenWallet,
    _tokenRoot: tokenRootContract.address,
  })
  .sendExternal({
    publicKey: signerAlice.publicKey!,
  });

// Fetching the balances after utilizing the transferToWallet function
console.log(
  `Alice balance after transfer to wallet: ${
    (
      await getWalletData(
        aliceMultiWalletContract,
        tokenRootContract.address
      )
    ).balance /
    10 ** deployRootFromDeployerParams.decimals
  }`
); // >> 50
console.log(
  `Bob balance after transfer to wallet: ${
    (
      await getWalletData(
        bobMultiWalletContract,
        tokenRootContract.address
      )
    ).balance /
    10 ** deployRootFromDeployerParams.decimals
  }`
);
typescript
import {
  ProviderRpcClient,
  Address,
  Contract,
  Transaction,
  FullContractState,
} from 'everscale-inpage-provider';
import * as tip3Artifacts from 'tip3-docs-artifacts';
import { provider, providerAddress } from './useProvider';

/**
 * We develop two more methods in order to reduce the mass of the script
 */

// This function will extract the public key of the sender
async function extractPubkey(
  provider: ProviderRpcClient,
  providerAddress: Address
): Promise<string> {
  // Fetching the user public key
  const accountFullState: FullContractState = (
    await provider.getFullContractState({ address: providerAddress })
  ).state!;

  const senderPublicKey: string = await provider.extractPublicKey(
    accountFullState.boc
  );

  return senderPublicKey;
}

// Defining the interface of what does the extractPubkey function returns
interface walletData {
  tokenWallet: Address;
  balance: number;
}
// This function will extract the wallets data from the wallets mapping from the multi wallet tip-3 contract
async function getWalletData(
  MWContract: Contract<
    tip3Artifacts.FactorySource['MultiWalletTIP3']
  >,
  tokenRootAddress: Address
): Promise<walletData> {
  const walletData = (
    await MWContract.methods.wallets().call()
  ).wallets.map(item => {
    if (item[0].toString() == tokenRootAddress.toString()) {
      return item[1];
    }
  });
  let balance: number = 0;
  let tokenWallet: Address = tip3Artifacts.zeroAddress;
  if (walletData.length != 0) {
    balance = Number(walletData[0]!.balance);
    tokenWallet = walletData[0]!.tokenWallet;
  }
  return { tokenWallet: tokenWallet, balance: balance };
}

async function main() {
  try {
    // Required contracts addresses
    const tokenRootAddress: Address = new Address(
      '<YOUR_TOKEN_ROOT_ADDRESS>'
    );
    const receiverMWAddress: Address = new Address(
      '<RECEIVER_MULTI_WALLET_ADDRESS>'
    );
    const senderMWAddress: Address = new Address(
      '<SENDER_MULTI_WALLET_ADDRESS>'
    );

    // creating an instance of the target contracts
    const tokenRootContract: Contract<
      tip3Artifacts.FactorySource['TokenRoot']
    > = new provider.Contract(
      tip3Artifacts.factorySource['TokenRoot'],
      tokenRootAddress
    );

    const senderMWContract: Contract<
      tip3Artifacts.FactorySource['MultiWalletTIP3']
    > = new provider.Contract(
      tip3Artifacts.factorySource['MultiWalletTIP3'],
      senderMWAddress
    );

    const receiverMWContract: Contract<
      tip3Artifacts.FactorySource['MultiWalletTIP3']
    > = new provider.Contract(
      tip3Artifacts.factorySource['MultiWalletTIP3'],
      receiverMWAddress
    );
    const [decimals, symbol] = await Promise.all([
      Number(
        (
          await tokenRootContract.methods
            .decimals({ answerId: 0 })
            .call()
        ).value0
      ),
      (await tokenRootContract.methods.symbol({ answerId: 0 }).call())
        .value0,
    ]);

    const tokenAmount: number = 10 * 10 ** decimals;

    // checking if the sender has enough tokens to send
    if (
      (
        await getWalletData(
          senderMWContract,
          tokenRootContract.address
        )
      ).balance <
      tokenAmount * 10 ** decimals
    ) {
      console.log('Low balance !');

      return 'Failed';
    }
    const senderPubkey: string = await extractPubkey(
      provider,
      providerAddress
    );
    if (
      senderPubkey !=
      (await senderMWContract.methods.owner({}).call()).owner
    ) {
      console.log(
        'You are not the owner of the sender multi wallet contract !'
      );

      return 'Failed';
    }

    // Checking recipient has a deploy wallet of that token root
    let recipientOldWalletData: walletData = await getWalletData(
      receiverMWContract,
      tokenRootContract.address
    );

    // Fetching the old balance of the receiver before transfer and determining if the receiver has a token wallet and deploy one by setting the deployWalletValue if not
    let oldBal: number = recipientOldWalletData.balance;

    let deployWalletValue: number = 0;

    if (
      recipientOldWalletData.tokenWallet.toString() ==
      tip3Artifacts.zeroAddress.toString()
    ) {
      deployWalletValue = 2 * 10 ** 9;
    }

    // Transferring token
    const { transaction: transferRes } =
      await senderMWContract.methods
        .transfer({
          _amount: tokenAmount,
          _recipient: receiverMWContract.address,
          _deployWalletValue: deployWalletValue,
          _tokenRoot: tokenRootContract.address,
        })
        .sendExternal({ publicKey: senderPubkey });

    // Throwing an error if transaction aborted and checking the confirmation of the transfer if not aborted
    if (transferRes.aborted) {
      console.log(
        `Transaction aborted !: ${
          (transferRes.exitCode, transferRes.resultCode)
        }`
      );
    } else {
      const newBal: number = (
        await getWalletData(
          receiverMWContract,
          tokenRootContract.address
        )
      ).balance;
      // Checking if the tokens are received successfully
      if (newBal > oldBal) {
        console.log(
          `${
            tokenAmount / 10 ** decimals
          } ${symbol}'s transferred successfully`
        );

        return `tx Hash: ${transferRes.id.hash}`;
      } else {
        console.error(
          `Transferring tokens failed, tx Hash: ${
            (transferRes.exitCode, transferRes.exitCode)
          }`
        );
      }
    }

    /*
      Using transferToWallet function
    */

    // Fetching the receiver balance before utilizing th transfer to wallet function
    oldBal = recipientOldWalletData.balance;

    // Transferring token using the receiver token wallet address
    const { transaction: transferToWalletRes } =
      await senderMWContract.methods
        .transferToWallet({
          _amount: tokenAmount,
          _recipientTokenWallet: recipientOldWalletData.tokenWallet,
          _tokenRoot: tokenRootContract.address,
        })
        .sendExternal({ publicKey: senderPubkey });

    // Throwing an error if transaction aborted and checking the confirmation of the transfer if not aborted
    if (transferToWalletRes.aborted) {
      throw new Error(
        `Transaction aborted !: ${
          (transferToWalletRes.exitCode,
          transferToWalletRes.resultCode)
        }`
      );
    } else {
      const newBal = (
        await getWalletData(
          receiverMWContract,
          tokenRootContract.address
        )
      ).balance;

      // Checking if the tokens are received successfully
      if (newBal > oldBal) {
        console.log(
          `${
            tokenAmount / 10 ** decimals
          } ${symbol}'s transferred successfully`
        );

        return `tx Hash: ${transferToWalletRes.id.hash}`;
      } else {
        throw new Error(
          `Transferring tokens failed , tx Hash: ${
            (transferToWalletRes.exitCode,
            transferToWalletRes.exitCode)
          }`
        );
      }
    }
  } catch (e: any) {
    throw new Error(`Failed ${e.message}`);
  }
}

Step 2: Transfer Tokens

Use this command to transfer TIP-3 tokens:

shell
npx locklift run -s ./scripts/06-transfer-tip3.ts -n local
buildStructure

Congratulations, you have successfully transferred TIP-3 tokens from one to another Wallet using a custom contract 🎉

Transfer TIP-3 tokens

Token Root address

Sender multi wallet address

Recipient multi wallet address

Amount

GIF

Transfer TIP-3 tokens to Token Wallet

Token Root address

Sender multi wallet address

Recipient multi wallet address

Amount

GIF