viem
Introduction
Section titled “Introduction”viem is a lightweight TypeScript library designed for interacting with Ethereum-compatible blockchains. This comprehensive guide will walk you through using viem to interact with and deploy smart contracts to Polkadot Hub.
Prerequisites
Section titled “Prerequisites”Before getting started, ensure you have the following installed:
- Node.js: v22.13.1 or later, check the Node.js installation guide.
- npm: v6.13.4 or later (comes bundled with Node.js).
- Solidity: This guide uses Solidity
^0.8.9for smart contract development.
Project Structure
Section titled “Project Structure”This project organizes contracts, scripts, and compiled artifacts for easy development and deployment.
viem-project/├── package.json├── tsconfig.json├── src/│ ├── chainConfig.ts│ ├── createClient.ts│ ├── createWallet.ts│ ├── compile.ts│ ├── deploy.ts│ └── interact.ts├── contracts/│ └── Storage.sol├── abis/│ └── Storage.json└── artifacts/ └── Storage.binSet Up the Project
Section titled “Set Up the Project”First, create a new folder and initialize your project:
mkdir viem-projectcd viem-projectnpm init -yInstall Dependencies
Section titled “Install Dependencies”Install viem along with other necessary dependencies, including solc, which enables compiling smart contracts' EVM bytecode.
# Install viem and solcnpm install viem solc@0.8.38
# Install TypeScript and development dependenciesnpm install --save-dev typescript ts-node @types/nodeInitialize Project
Section titled “Initialize Project”Initialize a TypeScript project by running the following command:
npx tsc --initAdd the following scripts to your package.json file to enable running TypeScript files:
{ "scripts": { "client": "ts-node src/createClient.ts", "compile": "ts-node src/compile.ts", "deploy": "ts-node src/deploy.ts", "interact": "ts-node src/interact.ts" },}Create a directory for your TypeScript source files:
mkdir srcSet Up the Chain Configuration
Section titled “Set Up the Chain Configuration”The first step is to set up the chain configuration. Create a new file at src/chainConfig.ts:
import { http } from 'viem';
export const TRANSPORT = http('INSERT_RPC_URL');
// Configure the Polkadot Hub chainexport const POLKADOT_HUB = { id: INSERT_CHAIN_ID, name: 'INSERT_CHAIN_NAME', network: 'INSERT_NETWORK_NAME', nativeCurrency: { decimals: INSERT_CHAIN_DECIMALS, name: 'INSERT_CURRENCY_NAME', symbol: 'INSERT_CURRENCY_SYMBOL', }, rpcUrls: { default: { http: ['INSERT_RPC_URL'], }, },} as const;Ensure to replace INSERT_RPC_URL, INSERT_CHAIN_ID, INSERT_CHAIN_NAME, INSERT_NETWORK_NAME, INSERT_CHAIN_DECIMALS, INSERT_CURRENCY_NAME, and INSERT_CURRENCY_SYMBOL with the proper values. Check the Connect to Polkadot page for more information on the possible values.
Set Up the viem Client
Section titled “Set Up the viem Client”To interact with the chain, you need to create a client that is used solely for reading data. To accomplish this, create a new file at src/createClient.ts:
import { createPublicClient, createWalletClient, http } from 'viem';
const transport = http('INSERT_RPC_URL');
// Configure the Polkadot Hub chainconst assetHub = { id: INSERT_CHAIN_ID, name: 'INSERT_CHAIN_NAME', network: 'INSERT_NETWORK_NAME', nativeCurrency: { decimals: INSERT_CHAIN_DECIMALS, name: 'INSERT_CURRENCY_NAME', symbol: 'INSERT_CURRENCY_SYMBOL', }, rpcUrls: { default: { http: ['INSERT_RPC_URL'], }, },} as const;
// Create a public client for reading dataexport const publicClient = createPublicClient({ chain: assetHub, transport,});After setting up the Public Client, you can begin querying the blockchain. Here's an example of fetching the latest block number:
Fetch Last Block code
import { createPublicClient, http } from 'viem';
const transport = http('https://eth-rpc-testnet.polkadot.io');
// Configure the Polkadot Hub chainconst polkadotHubTestnet = { id: 420420417, name: 'Polkadot Hub TestNet', network: 'polkadot-hub-testnet', nativeCurrency: { decimals: 18, name: 'PAS', symbol: 'PAS', }, rpcUrls: { default: { http: ['https://eth-rpc-testnet.polkadot.io'], }, },} as const;
// Create a public client for reading dataexport const publicClient = createPublicClient({ chain: polkadotHubTestnet, transport,});
const main = async () => { try { const block = await publicClient.getBlock(); console.log('Last block: ' + block.number.toString()); } catch (error: unknown) { console.error('Error connecting to Polkadot Hub TestNet: ' + error); }};
main();Set Up a Wallet
Section titled “Set Up a Wallet”In case you need to sign transactions, you will need to instantiate a Wallet Client object within your project. To do so, create src/createWallet.ts:
import { privateKeyToAccount } from 'viem/accounts';import { createWalletClient, http } from 'viem';
const transport = http('INSERT_RPC_URL');
// Configure the Polkadot Hub chainconst assetHub = { id: INSERT_CHAIN_ID, name: 'INSERT_CHAIN_NAME', network: 'INSERT_NETWORK_NAME', nativeCurrency: { decimals: INSERT_CHAIN_DECIMALS, name: 'INSERT_CURRENCY_NAME', symbol: 'INSERT_CURRENCY_SYMBOL', }, rpcUrls: { default: { http: ['INSERT_RPC_URL'], }, public: { http: ['INSERT_RPC_URL'], }, },} as const;
// Create a wallet client for writing dataexport const createWallet = (privateKey: `0x${string}`) => { const account = privateKeyToAccount(privateKey); return createWalletClient({ account, chain: assetHub, transport, });};Sample Smart Contract
Section titled “Sample Smart Contract”This example demonstrates compiling a Storage.sol Solidity contract for deployment to Polkadot Hub. The contract's functionality stores a number and permits users to update it with a new value.
mkdir contracts artifactsYou can use the following contract to interact with the blockchain. Paste the following contract in contracts/Storage.sol:
//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 Contract
Section titled “Compile the Contract”Create a new file at src/compile.ts for handling contract compilation:
import solc from 'solc';import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';import { basename, join } from 'path';
const ensureDir = (dirPath: string): void => { if (!existsSync(dirPath)) { mkdirSync(dirPath, { recursive: true }); }};
const compileContract = ( solidityFilePath: string, abiDir: string, artifactsDir: string): void => { try { // Read the Solidity file const source: string = readFileSync(solidityFilePath, 'utf8'); const fileName: string = basename(solidityFilePath);
// Construct the input object for the Solidity compiler const input = { language: 'Solidity', sources: { [fileName]: { content: source, }, }, settings: { outputSelection: { '*': { '*': ['abi', 'evm.bytecode'], }, }, }, };
console.log(`Compiling contract: ${fileName}...`);
// Compile the contract const output = JSON.parse(solc.compile(JSON.stringify(input)));
// Check for errors if (output.errors) { const errors = output.errors.filter((error: any) => error.severity === 'error'); if (errors.length > 0) { console.error('Compilation errors:'); errors.forEach((err: any) => console.error(err.formattedMessage)); return; } // Show warnings const warnings = output.errors.filter((error: any) => error.severity === 'warning'); warnings.forEach((warn: any) => console.warn(warn.formattedMessage)); }
// Ensure output directories exist ensureDir(abiDir); ensureDir(artifactsDir);
// Process compiled contracts for (const [sourceFile, contracts] of Object.entries(output.contracts)) { for (const [contractName, contract] of Object.entries(contracts as any)) { console.log(`Compiled contract: ${contractName}`);
// Write the ABI const abiPath = join(abiDir, `${contractName}.json`); writeFileSync(abiPath, JSON.stringify((contract as any).abi, null, 2)); console.log(`ABI saved to ${abiPath}`);
// Write the bytecode const bytecodePath = join(artifactsDir, `${contractName}.bin`); writeFileSync(bytecodePath, (contract as any).evm.bytecode.object); console.log(`Bytecode saved to ${bytecodePath}`); } } } catch (error) { console.error('Error compiling contracts:', error); }};
const solidityFilePath: string = './contracts/Storage.sol';const abiDir: string = './abis';const artifactsDir: string = './artifacts';
compileContract(solidityFilePath, abiDir, artifactsDir);To compile your contract:
npm run compileAfter executing this script, you will see the compilation results including the generated Storage.json (containing the contract's ABI) and Storage.bin (containing the compiled bytecode).
Deploy the Contract
Section titled “Deploy the Contract”Create a new file at src/deploy.ts for handling contract deployment:
import { existsSync, readFileSync } from 'fs';import { dirname, join } from 'path';import { fileURLToPath } from 'url';import { createWallet } from './createWallet.ts';import { publicClient } from './createClient.ts';
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);const ABIS_DIR = join(__dirname, '../abis');const ARTIFACTS_DIR = join(__dirname, '../artifacts');
const deployContract = async ( contractName: string, privateKey: `0x${string}`) => { try { console.log(`Deploying ${contractName}...`);
const abiPath = join(ABIS_DIR, `${contractName}.json`); const bytecodePath = join(ARTIFACTS_DIR, `${contractName}.bin`);
if (!existsSync(abiPath) || !existsSync(bytecodePath)) { throw new Error( `Missing artifacts for ${contractName}. Try running "npm run compile" first.` ); }
// Read contract artifacts const abi = JSON.parse( readFileSync(abiPath, 'utf8') ); const bytecode = `0x${readFileSync(bytecodePath, 'utf8').trim()}` as `0x${string}`;
// Create wallet const wallet = createWallet(privateKey);
// Deploy contract const hash = await wallet.deployContract({ abi, bytecode, args: [], // Add constructor arguments if needed });
// Wait for deployment const receipt = await publicClient.waitForTransactionReceipt({ hash }); const contractAddress = receipt.contractAddress;
console.log(`Contract deployed at: ${contractAddress}`); return contractAddress; } catch (error) { console.error('Deployment failed:', error); throw error; }};
const privateKey = 'INSERT_PRIVATE_KEY';deployContract('Storage', privateKey);Ensure to replace INSERT_PRIVATE_KEY with the proper value. For further details on private key exportation, refer to the article How to export an account's private key.
To deploy, run the following command:
npm run deployIf everything is successful, you will see the address of your deployed contract displayed in the terminal. This address is unique to your contract on the network you defined in the chain configuration, and you'll need it for any future interactions with your contract.
Interact with the Contract
Section titled “Interact with the Contract”Create a new file at src/interact.ts for interacting with your deployed contract:
import { readFileSync } from 'fs';import { dirname, join } from 'path';import { fileURLToPath } from 'url';import { publicClient } from './createClient.ts';import { createWallet } from './createWallet.ts';
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);const ABI_PATH = join(__dirname, '../abis/Storage.json');
const STORAGE_ABI = JSON.parse(readFileSync(ABI_PATH, 'utf8'));
const interactWithStorage = async ( contractAddress: `0x${string}`, privateKey: `0x${string}`) => { try { const wallet = createWallet(privateKey); const currentNumber = await publicClient.readContract({ address: contractAddress, abi: STORAGE_ABI, functionName: 'storedNumber', args: [], }); console.log(`Stored number: ${currentNumber}`);
const newNumber = BigInt(42); const { request } = await publicClient.simulateContract({ address: contractAddress, abi: STORAGE_ABI, functionName: 'setNumber', args: [newNumber], account: wallet.account, });
const hash = await wallet.writeContract(request); await publicClient.waitForTransactionReceipt({ hash }); console.log(`Number updated to ${newNumber}`);
const updatedNumber = await publicClient.readContract({ address: contractAddress, abi: STORAGE_ABI, functionName: 'storedNumber', args: [], }); console.log('Updated stored number:', updatedNumber); } catch (error) { console.error('Interaction failed:', error); }};
const PRIVATE_KEY = 'INSERT_PRIVATE_KEY';const CONTRACT_ADDRESS = 'INSERT_CONTRACT_ADDRESS';
interactWithStorage(CONTRACT_ADDRESS, PRIVATE_KEY);Ensure to replace INSERT_PRIVATE_KEY and INSERT_CONTRACT_ADDRESS with the proper values.
To interact with the contract:
npm run interactFollowing a successful interaction, you will see the stored value before and after the transaction. The output will show the initial stored number (0 if you haven't modified it yet), confirm when the transaction to set the number to 42 is complete, and then display the updated stored number value. This demonstrates both reading from and writing to your smart contract.
Where to Go Next
Section titled “Where to Go Next”Now that you have the foundation for using viem with Polkadot Hub, consider exploring:
-
Advanced viem Features
Explore viem's documentation:
-
Test Frameworks
Integrate viem with the following frameworks for comprehensive testing:
-
Event Handling
Learn how to subscribe to and process contract events:
-
Building dApps
Combine viem the following technologies to create full-stack applications: