English
EnglishRussian

Interact with smart contracts

Although everscale-inpage-provider supports sending external messages, the main use case for using the extension is sending internal messages from the user's wallet. This works the same way we sent messages from wallets in the chapter on smart contracts. To proceed, you'll need to read at least the first three chapters of the Smart Contracts section, plus it's desirable to learn more about distributed programming.



To help you get ahead in the tutorial, we transferred the ownership of the TokenRoot contract to a special proxy contract so that any user can mint tokens. Please use "Mint to user" button to mint 10 tokens to your wallet.



Add token to users assets list

To help user with token balance tracking you can ask him to add your token to the wallet's asset list.


import {Address} from 'everscale-inpage-provider'; const TokenRootContractAddress = new Address("0:13d2105fbd7fb7665eb7845703e507b192557a4048c8d27fa8ee08828db76cb0") const currentProviderState = await provider.getProviderState(); await provider.addAsset({ account: currentProviderState.permissions.accountInteraction.address, params: { rootContract: TokenRootContractAddress, }, type: 'tip3_token' })


Getting maxBet

First, we need to download data from the blockchain. We create an instance of the TokenDice.tsol, and load maxBet (the value of the maximum bet the contract can now accept). This is a simple method that looks like this (ABI):

// TokenDice.abi.json // { "ABI version": 2, // .... { "name": "maxBet", "inputs": [], "outputs": [{"name":"value0","type":"uint128"}] } // ... // } // In the solidty interface: // function maxBet() public view returns (uint128);

To execute it, the inpage-provder downloads the full state of the contract and executes tvm locally, execute an external message and receiving a response.


import {Address} from 'everscale-inpage-provider'; export const TokenDiceContractAddress = new Address("0:19128985f2d034a0a7b8dad5b23946aff3e63fe68c13243feb58124cef9acbb6"); const tokenDiceContract = new provider.Contract(TokenDiceAbi, TokenDiceContractAddress); const {value0: maxBet} = await tokenDiceContract.methods.maxBet().call();


Getting TokenRoot decimals & symbol

We've obtained the value of the maximum stake with full accuracy. Next, we need to load the metadata from TokenRoot.tsol to display the name of the token and values in integers instead of cents.


To do this, we need to call two methods in the TokenRoot contract:

function symbol() external view responsible returns (string); function decimals() external view responsible returns (uint8);

As you can see, unlike the previous one, these methods are marked as responsible. This means that they're intended to be called internally with messages from another contract and return a response to the caller. That is, these methods can be called from other contracts to find out what name or decimal places this Root has.


Of course, to avoid creating two different methods for the local call and the call by another contract, the inpage-provider can also execute responsible methods. To do this, we need to specify answerId: 0 in the call, like contract.methods.symbol({answerId: 0}).call(), where answerId indicates which function to call in response to the request. Provider will download full contract state and run tvm locally by emulating internal message with infinity gas.

// Interface // function symbol() override external view responsible returns (string) // { // ... abi // "functions": [ // // ... // { // "name": "symbol", // "inputs": [{"name": "answerId", "type": "uint32"}], // "outputs": [{"name": "value0", "type": "string"}] // }, // ], // } import TokenRootAbi from "../abi/TokenRoot.abi.json"; const TokenRootContractAddress = new Address("0:13d2105fbd7fb7665eb7845703e507b192557a4048c8d27fa8ee08828db76cb0") const tokenRootContract = new provider.Contract(TokenRootAbi, TokenRootContractAddress); const {value0: symbol} = await tokenRootContract.methods.symbol({answerId: 0}).call(); const {value0: decimals} = await tokenRootContract.methods.decimals({answerId: 0}).call();

Now we can display the highest bid in human readable form

import BigNumber from "bignumber.js"; const maxBetBeauty = `${new BigNumber(maxBet).shiftedBy(-1 * parseInt(decimals)).toFixed(2, BigNumber.ROUND_DOWN)} ${symbol}`;


Work with TokenWallet

To display the user's account balance and roll the die, we need to instantiate the TokenWallet contract. As you should know from the Distributed Programming chapter of Smart Contracts section - in Tip-3 a small smart contract wallet was created for each token owner. This contract stores only one user's account balance. To transfer tokens to another user, you need to interact with TokenWallet, not TokenRoot.


The address of such a wallet contract depends on the Root contract and the owner of the wallet, and is most easily determined by calling walletOf method of the Root contract

const currentProviderState = await provider.getProviderState(); // Wallet must be connected! const userAddress = currentProviderState.permissions.accountInteraction.address; const {value0: userTokenWalletAddress} = await tokenRootContract.methods.walletOf({answerId: 0, walletOwner: userAddress}).call();

After we got the user's TokenWallet address, we can get its balance:

import TokenWalletAbi from "./abi/TokenWallet.abi.json"; const userTokenWalletContract = new provider.Contract(TokenWalletAbi, userTokenWalletAddress); const {state} = await userTokenWalletContract.getFullState(); const isDeployed = state?.isDeployed; if (!isDeployed) { // TokenWallet not deployed, balance = 0 } else { const {value0: userTokenBalance } = await userTokenWalletContract.methods.balance({answerId: 0}).call(); }


Roll the die

We're ready for the dice game. The script of the game is very simple:

  • The player select a number from 1 to 6.
  • He makes a bet that's not higher than maxBet.
  • He sends a token transfer from his wallet to the TokenDice wallet by attaching a payload indicating which number he bets on.
  • After receiving a token acceptance message, the dice contract gets a new random number from 1 to 6 (from 0 to 5 :-)), and if the user guesses it, it transfers back an amount multiplied by 6.

Let's remember how the transaction starts. The user sends an external message to his wallet asking to call the TokenWallet contract with certain data and attach N Venoms to this message.


This external message is currently broadcasted over the network but cannot be traced in any way (this will be improved with the implementation of the EMQ protocol). In a normal situation, such a message is added to the block within seconds (in rare cases under a heavy load it may take longer)


Since we've no way to check if our message is in the "mempool", all wallets set an expiration time of two minutes. This means that if the message doesn't get into the block within minutes, it'll never get there. The wallet simply keeps track of all new blocks, and if the message gets into the block, it'll return the transaction to you or you'll get an error message if it's expired.


After the wallet receives an external message, it creates an outgoing internal message. And when the first transaction is done, you can already be sure that the whole chain of calls will definitely take place. If the network is underutilized, then the whole chain of calls hits one or two block. But testnet always has at least 4 threads to show network performance under load, so it takes longer for the chain to complete (in the worst case 3-6 seconds per contract in the call chain).


So let's write the code for the game:

// Player selected dice value const betDiceValue = 6; // Amount of tokens to play with, 1 token const amountOfTokens = '1000000000'; // We skipped validation amountOfTokens <= maxBet and amountOfTokens <= userTokenBalance // Pack betDiceValue into TvmCell // to pass as payload with token transfer const payload = (await provider.packIntoCell({ data: { _bet_dice_value: (betDiceValue - 1).toString(10), }, structure: [ {name: '_bet_dice_value', type: 'uint8'}, ], })).boc; // Send transfer method from user's wallet // pay attention - recipient is the owner of target // TokenWallet contract, not the TokenWallet it self. // TokenWallet address will be calculated under the hood. const firstTx = await userTokenWalletContract.methods.transfer({ amount: amountOfTokens, recipient: tokenDiceContract.address, deployWalletValue: '100000000', //0.1 Venom remainingGasTo: userAddress, notify: true, payload: payload }).send({ amount: "1500000000", bounce: true, from: userAddress }).catch((e) => { if (e.code ===3) { // rejected by a user return Promise.resolve(null); } else { // The message has expired or some other // perform any necessary error handling return Promise.reject(e); } }); if (firstTx) { // At this point, the external message we sent is already // included in the block, and we just need to wait for all // transactions in the chain to be finished // Update user's UI to notify "the transaction has already // been added to the blockchain, waiting for finalization" // Wait until all transactions are finished // and watch for the Game event. let playTx; const subscriber = new provider.Subscriber(); await subscriber.trace(firstTx).tap(tx_in_tree => { if (tx_in_tree.account.equals(TokenDiceContractAddress)) { playTx = tx_in_tree; } }).finished(); // Decode events by using abi // we are looking for event Game(address player, uint8 bet, uint8 result, uint128 prize); let events = await tokenDiceContract.decodeTransactionEvents({transaction: playTx}) if (events.length !== 1 || events[0].event !== 'Game') { throw new Error('Something is wrong'); } if (events[0].data.result === events[0].data.bet) { // User won, update UI const amountOfPrizeBeauty = `${new BigNumber(events[0].data.prize).shiftedBy(-1 * decimals).toFixed(2, BigNumber.ROUND_DOWN)} ${symbol}`; } else { // User lose, update UI. } }



That's how we write the front end for our dice game contract.

You can check out a sample implementation in React here.


In the next article, we'll analyze various improvements to our frontend.