How to create, sign, and send transactions manually with Ethers
Published
Table of contents
Sending a transaction to a blockchain when we have a wallet like Metamask or TrustWallet seems like a very basic operation; with just a couple of clicks we can interact with a smart contract or transfer some ETH. However, these wallets abstract a lot things for us and, in some cases we need to send transactions manually. For example if we have a backend job or a program and we don't have access to a browser wallet. In those cases, we need to build a transaction, encode it, sign it, and finally send it.
In this article, we're going to see how to do all these things using ethers, one of the most used Javascript libraries.
Requirements
In order to sign and send transactions programatically to a blockchain we need a few things:
- Account private key: the private key of the account that will sign the transaction
- Contract address: the address of the contract we want to interact with
- Contract ABI: the Application Binary Interface. You can get it from the block explorer if the contract has been verified.
- RPC endpoint: You can get an RPC endpoint for different blockchain providers like Chainstack or Infura
Apart from this, we'll create a code snippet which we'll run with Node, so Node.js and NPM are required as well.
Project setup and configuration
Initialise a new project by creating a new folder and start a new NPM project. Just run the following command: mkdir manual-transaction && cd manual-transaction && npm init -y
.
Next we'll install ethers and dotenv with npm i -D ethers dotenv typescript ts-node @types/node
.
After that, create a file named .env
. In this file, we'll include the account private key and the RPC endpoint as follows, just replace the values with your own:
ACCOUNT_KEY=abc2322adad46775
RPC_ENDPOINT=https://your-rpc-endpoint
That's all we need to setup the project, now let's write the code 🤓
Sending a transaction with ethers
Create an index.js file in which we'll write all our code. In it, the first thing we're going to do is import dotenv
. This package will read all the variables we define in the .env
file, and load them as environment variables. We'll add some validations to make sure that the variables are correcly loaded before continuing.
// load env file
import dotenv from 'dotenv';
dotenv.config();
const ACCOUNT_KEY = process.env.ACCOUNT_KEY;
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
if (!ACCOUNT_KEY) {
throw new Error('Account private key not provided in env file');
}
if (!RPC_ENDPOINT) {
throw new Error('RPC endpoint not provided in env file');
}
After that, let's import the hardhat
classes that we need and initialise a Provider
and our Contract
:
// Previous code above
import { Contract, ethers, Wallet } from 'ethers';
const CONTRACT_ADDRESS = '0xabc23342dfe243432'; // the address of the contract
const CONTRACT_ABI = []; // The contract ABI
// RPC loaded from env file previously
const provider = new ethers.providers.JsonRpcProvider(RPC_ENDPOINT);
// account key loaded from env file previously
const signer = new Wallet(ACCOUNT_KEY, provider);
const contractInstance = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);
For this example, let's say our smart contract has a method named updateMessage
that receives a string as a parameter (function updateMessage(string _message) public
). Here is how we will create the transaction, sign it, and send it:
// wrap code in async function
async function sendTransction() {
// create transaction
const unsignedTrx = await contractInstance.populateTransaction.updateMessage(
'This is my new message'
);
console.log('Transaction created');
// send transaction via signer so it's automatically signed
const trxResponse = await signer.sendTransaction(unsignedTrx);
console.log(`Transaction signed and sent: ${txResponse.hash}`);
// wait for block
await txResponse.wait(1);
console.log(
`Transaction has been mined at blocknumber: ${txResponse.blockNumber}, transaction hash: ${txResponse.hash}`
);
}
// trigger sendTransaction function
sendTransaction().then(() => console.log('done'));
Here is the full code:
// load env file
import dotenv from 'dotenv';
dotenv.config();
const ACCOUNT_KEY = process.env.ACCOUNT_KEY;
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
if (!ACCOUNT_KEY) {
throw new Error('Account private key not provided in env file');
}
if (!RPC_ENDPOINT) {
throw new Error('RPC endpoint not provided in env file');
}
import { Contract, ethers, Wallet } from 'ethers';
const CONTRACT_ADDRESS = '0xabc23342dfe243432'; // the address of the contract
const CONTRACT_ABI = []; // The contract ABI
// RPC loaded from env file previously
const provider = new ethers.providers.JsonRpcProvider(RPC_ENDPOINT);
// account key loaded from env file previously
const signer = new Wallet(ACCOUNT_KEY, provider);
const contractInstance = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);
// wrap code in async function
async function sendTransction() {
const unsignedTrx = await contractInstance.populateTransaction.updateMessage(
'This is my new message'
);
console.log('Transaction created');
const trxResponse = await signer.sendTransaction(unsignedTrx);
console.log(`Transaction sent: ${txResponse.hash}`);
// wait for block
await txResponse.wait(1);
console.log(
`Proposal has been mined at blocknumber: ${txResponse.blockNumber}, transaction hash: ${txResponse.hash}`
);
}
// trigger sendTransaction function
sendTransaction().then(() => console.log('done'));
TAGS