Back to homepage

How to send blockchain transactions from a backend

Published

Sending a transaction from a web3 application is a kinda simple process when we're using Metamask or any other wallet. We connect to the app and sign the transaction with just a click. But behind the scenes, a lot is going on and, whenever you need to sign and send a transaction manually from a backed, you get to understand all the steps involved in the process.

In this article, I'm going to explain how to sign and send a transaction from a backend using web3.js. This is a very common scenario from web3 apps that want to mint NFTs for their users, bridges, or any other app that has an application wallet.

As an example, I'll do an ERC20 token transfer, but you can replace the contract method for anything you want.

Note: use npm init -y && npm i web3 dotenv to install the required dependencies.

Requirements to create a transfer transaction

As usually when interacting with smart contracts, we need some parameters:

  • Node RPC endpoint: used to send our transaction to a blockchain node so it's propagated and picked up from the trxpool. You can get on for free from Chainstack
  • Wallet address and private key: required to sign the transactions from our backend and send them. Remember to keep the key safe as an environment variable and not include it in your code!
  • Smart contract ABI: similar to when we need to interact with a smart contract from a front-end, we need the ABI json file.
  • Smart contract address: the address of the smart contract in the blockchain. You can read how to deploy a smart contract here.

With all these parameters we need to:

  1. Load all parameters from environment variables.
  2. Create a web3 provider using the RPC endpoint.
  3. Import the wallet to the provider using its private key. Important!
  4. Create a smart contract reference using its address, ABI, and the web3 provider.
const Web3 = require('web3');

//load env file into environment variables
require('dotenv').config();

// load contract ABI from file
const TOKEN_ABI_JSON = require('./MyToken.json');

const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
const ORIGIN_CONTRACT_ADDRESS = process.env.TOKEN_CONTRACT_ADDRESS;
const WALLET_ADDRESS = process.env.WALLET_ADDRESS;
const WALLET_KEY = process.env.PRIV_KEY;

const web3Provider = new Web3(process.env.RPC_ENDPOINT);

// import wallet in the provider
web3Provider.eth.accounts.wallet.add(WALLET_KEY);

const tokenContract = new web3Provider.eth.Contract(
  TOKEN_ABI_JSON.abi,
  TOKEN_CONTRACT_ADDRESS
);

// transfer transaction variables
const amount = '1000000';
const recipientAddress = '0x21321321331321311232123';

// method to do the transfer
await transferTokens(web3Provider, tokenContract, amount, recipientAddress);

Send transaction method

To actually send the transaction, I created a separate function that receives the web3 provider, the smart contract reference, the amount, and the recipient address.

const transferTokens = async (provider, contract, amount, address) => {
  try {
    console.log(`Transfering ${amount} tokens to ${address}  πŸ’ΈπŸ’ΈπŸ’ΈπŸ’ΈπŸ’Έ`);

    // 1 create smart contract transaction
    const trx = contract.methods.transfer(address, amount);
    // 2 calculate gas fee
    const gas = await trx.estimateGas({ from: BRIDGE_WALLET });
    console.log('gas :>> ', gas);
    // 3 calculate gas price
    const gasPrice = await provider.eth.getGasPrice();
    console.log('gasPrice :>> ', gasPrice);
    // 4 encode transaction data
    const data = trx.encodeABI();
    console.log('data :>> ', data);
    // 5 get transaction number for wallet
    const nonce = await provider.eth.getTransactionCount(WALLET_ADDRESS);
    console.log('nonce :>> ', nonce);
    // 6 build transaction object with all the data
    const trxData = {
      // trx is sent from the wallet
      from: WALLET_ADDRESS,
      // trx destination is the ERC20 token contract
      to: ORIGIN_TOKEN_CONTRACT_ADDRESS,
      /** data contains the amount an recepient address params for transfer contract method */
      data,
      gas,
      gasPrice,
      nonce,
    };

    console.log('Transaction ready to be sent');
    /** 7 send transaction, it'll be automatically signed
    because the provider has already the wallet **/
    const receipt = await provider.eth.sendTransaction(trxData);
    console.log(`Transaction sent, hash is ${receipt.transactionHash}`);

    return true;
  } catch (error) {
    console.error('Error in transferTokens >', error);
    return false;
  }
};

Let's review how we're building the transaction and sending it from the backend step-by-step:

  1. create smart contract transaction by using the contract.methods.methodName(). This can be used for any smart contract method πŸ˜‰
  2. calculate gas fee using trx.estimateGas().
  3. calculate gas price using provider.eth.getGasPrice().
  4. encode transaction data using trx.encodeABI().
  5. get transaction number for the wallet with provider.eth.getTransactionCount(WALLET_ADDRESS).
  6. build transaction object with all the data.
  7. send transaction data with provider.eth.sendTransaction(trxData). The transaction will be automatically signed because the from property in the transaction data object, matches with the wallet address we imported into the provider.
  8. Print transaction hash

As you can see, these are multiple steps and, if you think about it, these are exactly the ones that Metamask does for us behind the scenes. Metamask already has our wallet address so it can calculate the gas, the transaction number (or nonce), encode all the transaction data, and send it via the RPC endpoints it has configured (provided by Infura by default).

I hope this helps you understand how blockchain transactions are submitted and, if you have any questions, feel free to contact me on Twitter and I'll be happy to help πŸ€™

TAGS

If you enjoyed this article consider sharing it on social media or buying me a coffee ✌️

Buy Me A Coffee