viem¶
Introduction¶
viem is a lightweight TypeScript library designed for interacting with EVM-compatible blockchains. This comprehensive guide will walk you through using viem to interact with and deploy smart contracts to Asset Hub.
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.9
for smart contract development
Set Up the Project¶
First, create a new folder and initialize your project:
Install Dependencies¶
Install viem along with other necessary dependencies, including @parity/revive, which enables to compile smart contracts to PolkaVM bytecode:
# Install viem and Revive
npm install viem @parity/revive
# Install TypeScript and development dependencies
npm install --save-dev typescript ts-node @types/node
Init Project¶
Init a TypeScript project by running the following command:
Add 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:
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 Asset Hub chain
export const ASSET_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 Asset Hub page for more information on the possible values.
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 Asset Hub chain
const 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 data
export 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://westend-asset-hub-eth-rpc.polkadot.io');
// Configure the Asset Hub chain
const assetHub = {
id: 420420421,
name: 'Westend Asset Hub',
network: 'westend-asset-hub',
nativeCurrency: {
decimals: 18,
name: 'WND',
symbol: 'WND',
},
rpcUrls: {
default: {
http: ['https://westend-asset-hub-eth-rpc.polkadot.io'],
},
},
} as const;
// Create a public client for reading data
export const publicClient = createPublicClient({
chain: assetHub,
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 Asset Hub: ' + error);
}
};
main();
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 Asset Hub chain
const 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 data
export const createWallet = (privateKey: `0x${string}`) => {
const account = privateKeyToAccount(privateKey);
return createWalletClient({
account,
chain: assetHub,
transport,
});
};
Note
The wallet you import with your private key must have sufficient funds to pay for transaction fees when deploying contracts or interacting with them. Make sure to fund your wallet with the appropriate native tokens for the network you're connecting to.
Sample 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.
You 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¶
Create a new file at src/compile.ts
for handling contract compilation:
import { compile } from '@parity/revive';
import { readFileSync, writeFileSync } from 'fs';
import { basename, join } from 'path';
const compileContract = async (
solidityFilePath: string,
outputDir: string
): Promise<void> => {
try {
// Read the Solidity file
const source: string = readFileSync(solidityFilePath, 'utf8');
// Construct the input object for the compiler
const input: Record<string, { content: string }> = {
[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
if (
contract.evm &&
contract.evm.bytecode &&
contract.evm.bytecode.object
) {
const bytecodePath = join(outputDir, `${name}.polkavm`);
writeFileSync(
bytecodePath,
Buffer.from(contract.evm.bytecode.object, 'hex')
);
console.log(`Bytecode saved to ${bytecodePath}`);
} else {
console.warn(`No bytecode found for contract: ${name}`);
}
}
}
} catch (error) {
console.error('Error compiling contracts:', error);
}
};
const solidityFilePath: string = './contracts/Storage.sol';
const outputDir: string = './artifacts/';
compileContract(solidityFilePath, outputDir);
To compile your contract:
After executing this script, you will see the compilation results including the generated Storage.json
(containing the contract's ABI) and Storage.polkavm
(containing the compiled bytecode) files in the artifacts
folder. These files contain all the necessary information for deploying and interacting with your smart contract on Asset Hub.
Deploy the Contract¶
Create a new file at src/deploy.ts
for handling contract deployment:
import { readFileSync } from 'fs';
import { join } from 'path';
import { createWallet } from './createWallet';
import { publicClient } from './createClient';
const deployContract = async (
contractName: string,
privateKey: `0x${string}`
) => {
try {
console.log(`Deploying ${contractName}...`);
// Read contract artifacts
const abi = JSON.parse(
readFileSync(
join(__dirname, '../artifacts', `${contractName}.json`),
'utf8'
)
);
const bytecode = `0x${readFileSync(
join(__dirname, '../artifacts', `${contractName}.polkavm`)
).toString('hex')}` 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.
Warning
Never commit or share your private key. Exposed keys can lead to immediate theft of all associated funds. Use environment variables instead.
To deploy, run the following command:
If 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¶
Create a new file at src/interact.ts
for interacting with your deployed contract:
import { publicClient } from './createClient';
import { createWallet } from './createWallet';
import { readFileSync } from 'fs';
const STORAGE_ABI = JSON.parse(
readFileSync('./artifacts/Storage.json', '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:
Following 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.
Complete Project Structure¶
After completing this guide, your project directory should have the following structure. This overview helps you verify that you've created all the necessary files in their correct locations for your viem integration with Asset Hub:
viem-project/
├── package.json
├── tsconfig.json
├── src/
│ ├── chainConfig.ts
│ ├── createClient.ts
│ ├── createWallet.ts
│ ├── compile.ts
│ ├── deploy.ts
│ └── interact.ts
├── contracts/
│ └── Storage.sol
└── artifacts/
├── Storage.json
└── Storage.polkavm
Where to Go Next¶
Now that you have the foundation for using viem with Asset Hub, consider exploring:
-
External Advanced viem Features
Explore viem's documentation:
-
External Test Frameworks
Integrate viem with the following frameworks for comprehensive testing:
-
External Event Handling
Learn how to subscribe to and process contract events:
-
External Building dApps
Combine viem the following technologies to create full-stack applications:
| Created: April 9, 2025