Ethers.js¶
Introduction¶
Ethers.js is a lightweight library that enables interaction with Ethereum Virtual Machine (EVM)-compatible blockchains through JavaScript. Ethers is widely used as a toolkit to establish connections and read and write blockchain data. This article demonstrates using Ethers.js to interact and deploy smart contracts to Asset Hub.
Set Up the Project¶
To start working with Ethers.js, create a new folder and initialize your project by running the following commands in your terminal:
Install Dependencies¶
Next, run the following command to install the Ethers.js library:
Set Up the Ethers.js Provider¶
To interact with the Asset Hub, you must set up an Ethers.js provider. This provider connects to a blockchain node, allowing you to query blockchain data and interact with smart contracts. In the root of your project, create a file named connectToProvider.js
and add the following code:
const { JsonRpcProvider } = require('ethers');
const createProvider = (rpcUrl, chainId, chainName) => {
const provider = new JsonRpcProvider(rpcUrl, {
chainId: chainId,
name: chainName,
});
return provider;
};
const PROVIDER_RPC = {
rpc: 'INSERT_RPC_URL',
chainId: 'INSERT_CHAIN_ID',
name: 'INSERT_CHAIN_NAME',
};
createProvider(PROVIDER_RPC.rpc, PROVIDER_RPC.chainId, PROVIDER_RPC.name);
Note
Replace INSERT_RPC_URL
, INSERT_CHAIN_ID
, and INSERT_CHAIN_NAME
with the appropriate values. For example, to connect to Westend Asset Hub's Ethereum RPC instance, you can use the following parameters:
With the Provider
set up, you can start querying the blockchain. For instance, to fetch the latest block number:
Fetch Last Block code
const { JsonRpcProvider } = require('ethers');
const createProvider = (rpcUrl, chainId, chainName) => {
const provider = new JsonRpcProvider(rpcUrl, {
chainId: chainId,
name: chainName,
});
return provider;
};
const PROVIDER_RPC = {
rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
chainId: 420420421,
name: 'westend-asset-hub',
};
const main = async () => {
try {
const provider = createProvider(
PROVIDER_RPC.rpc,
PROVIDER_RPC.chainId,
PROVIDER_RPC.name
);
const latestBlock = await provider.getBlockNumber();
console.log(`Latest block: ${latestBlock}`);
} catch (error) {
console.error('Error connecting to Asset Hub: ' + error.message);
}
};
main();
Compile Contracts¶
The revive
compiler transforms Solidity smart contracts into PolkaVM
bytecode for deployment on Asset Hub. Revive's Ethereum RPC interface allows you to use familiar tools like Ethers.js and MetaMask to interact with contracts.
Install the Revive Library¶
The @parity/revive
library will compile your Solidity code for deployment on Asset Hub. Run the following command in your terminal to install the library:
Sample Storage.sol
Smart Contract¶
This example demonstrates compiling a Storage.sol
Solidity contract for deployment to Asset Hub. The contract's functionality stores a number and permits users to update it with a new value.
//SPDX-License-Identifier: MIT
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.9;
contract Storage {
// Public state variable to store a number
uint256 public storedNumber;
/**
* Updates the stored number.
*
* The `public` modifier allows anyone to call this function.
*
* @param _newNumber - The new value to store.
*/
function setNumber(uint256 _newNumber) public {
storedNumber = _newNumber;
}
}
Compile the Smart Contract¶
To compile this contract, use the following script:
const { compile } = require('@parity/revive');
const { readFileSync, writeFileSync } = require('fs');
const { basename, join } = require('path');
const compileContract = async (solidityFilePath, outputDir) => {
try {
// Read the Solidity file
const source = readFileSync(solidityFilePath, 'utf8');
// Construct the input object for the compiler
const input = {
[basename(solidityFilePath)]: { content: source },
};
console.log(`Compiling contract: ${basename(solidityFilePath)}...`);
// Compile the contract
const out = await compile(input);
for (const contracts of Object.values(out.contracts)) {
for (const [name, contract] of Object.entries(contracts)) {
console.log(`Compiled contract: ${name}`);
// Write the ABI
const abiPath = join(outputDir, `${name}.json`);
writeFileSync(abiPath, JSON.stringify(contract.abi, null, 2));
console.log(`ABI saved to ${abiPath}`);
// Write the bytecode
const bytecodePath = join(outputDir, `${name}.polkavm`);
writeFileSync(
bytecodePath,
Buffer.from(contract.evm.bytecode.object, 'hex')
);
console.log(`Bytecode saved to ${bytecodePath}`);
}
}
} catch (error) {
console.error('Error compiling contracts:', error);
}
};
const solidityFilePath = './Storage.sol';
const outputDir = '.';
compileContract(solidityFilePath, outputDir);
Note
The script above is tailored to the Storage.sol
contract. It can be adjusted for other contracts by changing the file name or modifying the ABI and bytecode paths.
After executing the script, the Solidity contract will be compiled into the required polkavm
bytecode format. The ABI and bytecode will be saved into files with .json
and .polkavm
extensions, respectively. You can now proceed with deploying the contract to the Asset Hub network, as outlined in the next section.
Deploy the Compiled Contract¶
To deploy your compiled contract to Asset Hub, you'll need a wallet with a private key to sign the deployment transaction.
You can create a deploy.js
script in the root of your project to achieve this. The deployment script can be divided into key components:
-
First, set up the required imports and utilities:
-
Create a provider to connect to the Asset Hub network:
-
Set up functions to read contract artifacts:
deploy.js// Reads and parses the ABI file for a given contract const getAbi = (contractName) => { try { return JSON.parse( readFileSync(join(codegenDir, `${contractName}.json`), 'utf8') ); } catch (error) { console.error( `Could not find ABI for contract ${contractName}:`, error.message ); throw error; } }; // Reads the compiled bytecode for a given contract const getByteCode = (contractName) => { try { return `0x${readFileSync( join(codegenDir, `${contractName}.polkavm`) ).toString('hex')}`; } catch (error) { console.error( `Could not find bytecode for contract ${contractName}:`, error.message ); throw error;
-
Create the main deployment function:
deploy.js}; const deployContract = async (contractName, mnemonic, providerConfig) => { console.log(`Deploying ${contractName}...`); try { // Step 1: Set up provider and wallet const provider = createProvider( providerConfig.rpc, providerConfig.chainId, providerConfig.name ); const walletMnemonic = ethers.Wallet.fromPhrase(mnemonic); const wallet = walletMnemonic.connect(provider); // Step 2: Create and deploy the contract const factory = new ethers.ContractFactory( getAbi(contractName), getByteCode(contractName), wallet ); const contract = await factory.deploy(); await contract.waitForDeployment(); // Step 3: Save deployment information const address = await contract.getAddress(); console.log(`Contract ${contractName} deployed at: ${address}`); const addressesFile = join(codegenDir, 'contract-address.json'); const addresses = existsSync(addressesFile) ? JSON.parse(readFileSync(addressesFile, 'utf8')) : {}; addresses[contractName] = address; writeFileSync(addressesFile, JSON.stringify(addresses, null, 2), 'utf8'); } catch (error) { console.error(`Failed to deploy contract ${contractName}:`, error);
-
Configure and execute the deployment:
deploy.js}; const providerConfig = { rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io', chainId: 420420421, name: 'westend-asset-hub', }; const mnemonic = 'INSERT_MNEMONIC';
Note
Ensure to replace the
INSERT_MNEMONIC
placeholder with your actual mnemonic.
Here's the complete deployment script combining all the components above:
deploy.js
const { writeFileSync, existsSync, readFileSync } = require('fs');
const { join } = require('path');
const { ethers, JsonRpcProvider } = require('ethers');
const codegenDir = join(__dirname);
// Creates an Ethereum provider with specified RPC URL and chain details
const createProvider = (rpcUrl, chainId, chainName) => {
const provider = new JsonRpcProvider(rpcUrl, {
chainId: chainId,
name: chainName,
});
return provider;
};
// Reads and parses the ABI file for a given contract
const getAbi = (contractName) => {
try {
return JSON.parse(
readFileSync(join(codegenDir, `${contractName}.json`), 'utf8')
);
} catch (error) {
console.error(
`Could not find ABI for contract ${contractName}:`,
error.message
);
throw error;
}
};
// Reads the compiled bytecode for a given contract
const getByteCode = (contractName) => {
try {
return `0x${readFileSync(
join(codegenDir, `${contractName}.polkavm`)
).toString('hex')}`;
} catch (error) {
console.error(
`Could not find bytecode for contract ${contractName}:`,
error.message
);
throw error;
}
};
const deployContract = async (contractName, mnemonic, providerConfig) => {
console.log(`Deploying ${contractName}...`);
try {
// Step 1: Set up provider and wallet
const provider = createProvider(
providerConfig.rpc,
providerConfig.chainId,
providerConfig.name
);
const walletMnemonic = ethers.Wallet.fromPhrase(mnemonic);
const wallet = walletMnemonic.connect(provider);
// Step 2: Create and deploy the contract
const factory = new ethers.ContractFactory(
getAbi(contractName),
getByteCode(contractName),
wallet
);
const contract = await factory.deploy();
await contract.waitForDeployment();
// Step 3: Save deployment information
const address = await contract.getAddress();
console.log(`Contract ${contractName} deployed at: ${address}`);
const addressesFile = join(codegenDir, 'contract-address.json');
const addresses = existsSync(addressesFile)
? JSON.parse(readFileSync(addressesFile, 'utf8'))
: {};
addresses[contractName] = address;
writeFileSync(addressesFile, JSON.stringify(addresses, null, 2), 'utf8');
} catch (error) {
console.error(`Failed to deploy contract ${contractName}:`, error);
}
};
const providerConfig = {
rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
chainId: 420420421,
name: 'westend-asset-hub',
};
const mnemonic = 'INSERT_MNEMONIC';
deployContract('Storage', mnemonic, providerConfig);
After running this script, your contract will be deployed to Asset Hub, and its address will be saved in contract-address.json
within your project directory. You can use this address for future contract interactions.
Interact with the Contract¶
Once the contract is deployed, you can interact with it by calling its functions. For example, to set a number, read it and then modify that number by its double, you can create a file named checkStorage.js
in the root of your project and add the following code:
const { ethers } = require('ethers');
const { readFileSync } = require('fs');
const { join } = require('path');
const createProvider = (providerConfig) => {
return new ethers.JsonRpcProvider(providerConfig.rpc, {
chainId: providerConfig.chainId,
name: providerConfig.name,
});
};
const createWallet = (mnemonic, provider) => {
return ethers.Wallet.fromPhrase(mnemonic).connect(provider);
};
const loadContractAbi = (contractName, directory = __dirname) => {
const contractPath = join(directory, `${contractName}.json`);
const contractJson = JSON.parse(readFileSync(contractPath, 'utf8'));
return contractJson.abi || contractJson; // Depending on JSON structure
};
const createContract = (contractAddress, abi, wallet) => {
return new ethers.Contract(contractAddress, abi, wallet);
};
const interactWithStorageContract = async (
contractName,
contractAddress,
mnemonic,
providerConfig,
numberToSet
) => {
try {
console.log(`Setting new number in Storage contract: ${numberToSet}`);
// Create provider and wallet
const provider = createProvider(providerConfig);
const wallet = createWallet(mnemonic, provider);
// Load the contract ABI and create the contract instance
const abi = loadContractAbi(contractName);
const contract = createContract(contractAddress, abi, wallet);
// Send a transaction to set the stored number
const tx1 = await contract.setNumber(numberToSet);
await tx1.wait(); // Wait for the transaction to be mined
console.log(`Number successfully set to ${numberToSet}`);
// Retrieve the updated number
const storedNumber = await contract.storedNumber();
console.log(`Retrieved stored number:`, storedNumber.toString());
// Send a transaction to set the stored number
const tx2 = await contract.setNumber(numberToSet * 2);
await tx2.wait(); // Wait for the transaction to be mined
console.log(`Number successfully set to ${numberToSet * 2}`);
// Retrieve the updated number
const updatedNumber = await contract.storedNumber();
console.log(`Retrieved stored number:`, updatedNumber.toString());
} catch (error) {
console.error('Error interacting with Storage contract:', error.message);
}
};
const providerConfig = {
name: 'asset-hub-smart-contracts',
rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
chainId: 420420421,
};
const mnemonic = 'INSERT_MNEMONIC';
const contractName = 'Storage';
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const newNumber = 42;
interactWithStorageContract(
contractName,
contractAddress,
mnemonic,
providerConfig,
newNumber
);
Ensure you replace the INSERT_MNEMONIC
, INSERT_CONTRACT_ADDRESS
, and INSERT_ADDRESS_TO_CHECK
placeholders with actual values. Also, ensure the contract ABI file (Storage.json
) is correctly referenced.
Where to Go Next¶
Now that you have the foundational knowledge to use Ethers.js with Asset Hub, you can:
- Dive into Ethers.js utilities - discover additional Ethers.js features, such as wallet management, signing messages, etc
- Implement batch transactions - use Ethers.js to execute batch transactions for efficient multi-step contract interactions
- Build scalable applications - combine Ethers.js with frameworks like
Next.js
orNode.js
to create full-stack decentralized applications (dApps)
| Created: March 6, 2025