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.
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 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,
});
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.
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,
});
};
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.
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:
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.
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:
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:
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: March 6, 2025