Handle NEP141 Transfer Calls and Track the Target Contract's Balance

Learn to use ft_transfer_call function calls. This method needs a bit of Rust logic to make it work with the target contract.

Register the target address, transfer the minimum NEAR balance and make your dApp accept fungible token transfers to create engaging games with real money 🤑

Getting Started

Follow the Project Setup instructions to start the project in your local machine and try the recipes yourself. You can also clone or navigate the repo to get just what you need.

For a NEAR wallet to receive NEP141 payments, for example USDT usdt.tether-token.near  transfers, a simple ft_transfer call will suffice. If the source wallet has the necessary balance, the money will be transferred. But what if you want to notify another contract about this balance transfer and act upon it?

This is what we do in Prompt Wars. When a user submits a prompt, they are actually transferring their fungible token balance to the game contract, the game contract is "notified" and registers the player's prompt to list them in the game.

IMPORTANT

New contracts that should receive NEP141 fungible token transfers need to be registered.

See a useful pattern here.

The ft_receiver contract

Part of the Prompt Wars suite of methods, is the ft_on_transfer method. This method will be called after a successful ft_transfer_call call on the receiving NEP141 funginble token contract.

Read through NEAR's Fungible Token docs to learn more about this pattern.

The important part here is the msg String parameter. Note that the NEP141 callback will strictly call ft_on_transfer on the receiver contract. This method should be publicly available on your contract.

The ft_on_transfer msg Payload

The pattern above expects a String as the msg parameter, this String is formatted by JSON.stringify and sent via the cross-contract call to be unwrapped by this line:

let payload: Payload = serde_json::from_str(&msg).expect("ERR_INVALID_PAYLOAD");

Rust does the work of checking the Payload structure and then passes it to the match block:

match payload {
    Payload::CreateOutcomeTokenArgs(payload) => {
        self.create_outcome_token(sender_id, amount, payload)
    }
};

Where self.create_outcome_token(sender_id, amount, payload) is part of the game's private contract methods.

Note that "private" here is actually NEAR's private, but not Rust private. Meaning that the method can be called across Rust files, but not called from the NEAR CLI or NEAR API.

Calling ft_transfer_call from the client-side

This whole pattern is used from the client-side when a user's wallet is connected. Notice how we format the msg parameter expected by ft_transfer_call:

Unwrapping ft_on_transfer result

If the NEP141 transfer succeeds, then there should be a new balance registered for the target contract, in this case our game.

To check for a successful transfer, remember that create_outcome_token returns an amount. This amount should be greater than 0 and should match the amount we are charging to play the game:

#[private]
pub fn create_outcome_token(
    &mut self,
    sender_id: AccountId,
    amount: WrappedBalance,
    payload: CreateOutcomeTokenArgs,
) -> WrappedBalance {
    self.assert_is_open();
    self.assert_is_not_resolved();
    self.assert_price(amount);

    let outcome_id = sender_id;
    let prompt = payload.prompt;

    match self.outcome_tokens.get(&outcome_id) {
        Some(_token) => env::panic_str("ERR_CREATE_OUTCOME_TOKEN_outcome_id_EXIST"),
        None => {
            self.internal_create_outcome_token(outcome_id, prompt, amount);
        }
    }

    amount
}

This amount is unwrapped on ftTransferCall .

Note that the response of wallet.signAndSendTransaction needs to be parsed because it is formatted as hex. Check this unwrapping function helper here.

Displaying the game contract's balance

This client-side step is the easiest. Just call ft_balance_of  to check the balance of a target accountId on any NEP141 fungible token:

Formatting NEP141 Fungible Token balances

Each fungible token has its own decimals value definition. For USDT for example, it is decimals: 6.

Remember that smart-contracts use fixed-point decimals (no . symbol to distinguish cents as in 1.0 ). For example:

NEAR_ENV=mainnet near view usdt.tether-token.near ft_metadata                                                                                                                    1 ↵

View call: usdt.tether-token.near.ft_metadata()
{
  spec: 'ft-1.0.0',
  name: 'Tether USD',
  symbol: 'USDt',
  icon: "data:image/svg+xml,%3Csvg width='111' height='90' viewBox='0 0 111 90' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M24.4825 0.862305H88.0496C89.5663 0.862305 90.9675 1.64827 91.7239 2.92338L110.244 34.1419C111.204 35.7609 110.919 37.8043 109.549 39.1171L58.5729 87.9703C56.9216 89.5528 54.2652 89.5528 52.6139 87.9703L1.70699 39.1831C0.305262 37.8398 0.0427812 35.7367 1.07354 34.1077L20.8696 2.82322C21.6406 1.60483 23.0087 0.862305 24.4825 0.862305ZM79.8419 14.8003V23.5597H61.7343V29.6329C74.4518 30.2819 83.9934 32.9475 84.0642 36.1425L84.0638 42.803C83.993 45.998 74.4518 48.6635 61.7343 49.3125V64.2168H49.7105V49.3125C36.9929 48.6635 27.4513 45.998 27.3805 42.803L27.381 36.1425C27.4517 32.9475 36.9929 30.2819 49.7105 29.6329V23.5597H31.6028V14.8003H79.8419ZM55.7224 44.7367C69.2943 44.7367 80.6382 42.4827 83.4143 39.4727C81.0601 36.9202 72.5448 34.9114 61.7343 34.3597V40.7183C59.7966 40.8172 57.7852 40.8693 55.7224 40.8693C53.6595 40.8693 51.6481 40.8172 49.7105 40.7183V34.3597C38.8999 34.9114 30.3846 36.9202 28.0304 39.4727C30.8066 42.4827 42.1504 44.7367 55.7224 44.7367Z' fill='%23009393'/%3E%3C/svg%3E",
  reference: null,
  reference_hash: null,
  decimals: 6
}

With decimals being 6, an amount of USDT 1.0 would be represented as:

1000000

But this would be confusing for your users, since they are used to the dot notation.

Fixed-point decimal amount format helpers

Luckily for you, Prompt Wars needed to display formatted amounts and we developed some helpers that may suit your dApp as well:

Last updated