Interacting with the blockchain with viem
viem a TypeScript interface for Ethereum that provides low-level stateless primitives for interacting with Ethereum.
You can use viem to interact with smart contracts deployed on Lisk. Viem provides first-class support for chains implemented on the OP stack, see viem.sh > OP stack.
Install
To install viem run the following command:
npm install --save viem
Setup
Before you can start using viem, you need to setup a Client with a desired Transport and Chain.
Public Client
In order to read data from Lisk, you need to create a Public client.
By extending the public client with publicActionsL2
or publicActionsL1
, you have access to additional methods for Lisk and other chains built on top of the OP stack.
See Layer 2 extensions for more information.
- Lisk
- Lisk Sepolia
import { createPublicClient, http } from 'viem'
import { lisk, ethereum } from 'viem/chains'
import { publicActionsL2, publicActionsL1 } from 'viem/op-stack'
export const publicClientL2 = createPublicClient({
chain: lisk,
transport: http()
}).extend(publicActionsL2())
export const publicClientL1 = createPublicClient({
chain: ethereum,
transport: http()
}).extend(publicActionsL1())
import { createPublicClient, http } from 'viem'
import { liskSepolia, sepolia } from 'viem/chains'
import { publicActionsL2, publicActionsL1 } from 'viem/op-stack'
export const publicClientL2 = createPublicClient({
chain: liskSepolia,
transport: http()
}).extend(publicActionsL2())
export const publicClientL1 = createPublicClient({
chain: sepolia,
transport: http()
}).extend(publicActionsL1())
Wallet Client
In order to write data to Lisk, you need to create a Wallet client (createWalletClient
) and specify an Account
to use.
By extending the wallet client with walletActionsL1
or walletActionsL2
, you have access to additional methods for Lisk and other chains built on top of the OP stack.
See Layer 2 extensions for more information.
- JSON-RPC Account
- Local Account
- Lisk
- Lisk Sepolia
import { createWalletClient, custom } from 'viem'
import { ethereum, lisk } from 'viem/chains'
import { walletActionsL1, walletActionsL2 } from 'viem/op-stack'
// Retrieve Account from an EIP-1193 Provider.
const [account] = await window.ethereum.request({
method: 'eth_requestAccounts'
})
export const walletClientL1 = createWalletClient({
account,
chain: ethereum,
transport: custom(window.ethereum)
}).extend(walletActionsL1());
export const walletClientL2 = createWalletClient({
account,
chain: lisk,
transport: custom(window.ethereum)
}).extend(walletActionsL2());
import { createWalletClient, custom } from 'viem'
import { sepolia, liskSepolia } from 'viem/chains'
import { walletActionsL1, walletActionsL2 } from 'viem/op-stack'
// Retrieve Account from an EIP-1193 Provider.
const [account] = await window.ethereum.request({
method: 'eth_requestAccounts'
})
export const walletClientL1 = createWalletClient({
account,
chain: sepolia,
transport: custom(window.ethereum)
}).extend(walletActionsL1());
export const walletClientL2 = createWalletClient({
account,
chain: liskSepolia,
transport: custom(window.ethereum)
}).extend(walletActionsL2());
Replace <PRIVATE_KEY>
with the private key of the account you want to use.
The 0x
prefix indicates that the following string is a hexadecimal number.
- Lisk
- Lisk Sepolia
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { ethereum, lisk } from 'viem/chains'
import { walletActionsL1, walletActionsL2 } from 'viem/op-stack'
// Wallet client for Ethereum
export const walletClientL1 = createWalletClient({
account: privateKeyToAccount('0x<PRIVATE_KEY>'),
chain: ethereum,
transport: http()
}).extend(walletActionsL1());
// Wallet client for Lisk
export const walletClientL2 = createWalletClient({
account: privateKeyToAccount('0x<PRIVATE_KEY>'),
chain: lisk,
transport: http()
}).extend(walletActionsL2());
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia, liskSepolia } from 'viem/chains'
import { walletActionsL1, walletActionsL2 } from 'viem/op-stack'
// Wallet client for Ethereum
export const walletClientL1 = createWalletClient({
account: privateKeyToAccount('0x<PRIVATE_KEY>'),
chain: sepolia,
transport: http()
}).extend(walletActionsL1());
// Wallet client for Lisk
export const walletClientL2 = createWalletClient({
account: privateKeyToAccount('0x<PRIVATE_KEY>'),
chain: liskSepolia,
transport: http()
}).extend(walletActionsL2());
In addition to making a JSON-RPC request (eth_requestAccounts
) to get an Account, viem provides various helper methods for creating an Account
, including: privateKeyToAccount
, mnemonicToAccount
, and hdKeyToAccount
.
Reading data from the blockchain
Create a public client and use it to read and access data from Lisk using Public Actions and OP stack public actions.
Public Actions are client methods that map one-to-one with a "public" Ethereum RPC method (eth_blockNumber
, eth_getBalance
, etc.).
For example, you can use the getBlockNumber
action to get the latest block:
import { parseEther } from 'viem'
import { publicClientL2 } from './public-client.js'
const blockNumber = await publicClientL2.getBlockNumber();
export const l1Gas = await publicClientL2.estimateL1Gas({
account: '0x3C46A11471f285E36EE8d089473ce98269D1b081',
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
value: parseEther('0.1')
})
console.log(blockNumber);
console.log(l1Gas);
Writing data to the blockchain
Create a wallet client and use it to read and access data from Lisk using Wallet Actions and OP stack wallet actions.
Wallet Actions are actions that map one-to-one with a "wallet" or "signable" Ethereum RPC method (eth_requestAccounts
, eth_sendTransaction
, etc).
For example, you can use the sendTransaction
action to post a transaction:
import { walletClientL2 } from './wallet-client.js'
const hash = await walletClientL2.sendTransaction({
to: '0x...',
value: 1000000000000000000n
})
console.log(hash);
Interacting with smart contracts
You can use viem to interact with a smart contract on Lisk by creating a Contract
instance using getContract
and passing it the contract ABI, contract address, and and Public and/or Wallet Client:
- contract-example.js
- client.js
- abi.js
import { getContract } from 'viem'
import { wagmiAbi } from './abi.js'
import { publicClient, walletClient } from './client.js'
// 1. Create contract instance
const contract = getContract({
address: 'CONTRACT_ADDRESS',
abi: wagmiAbi,
// 1a. Insert a single client
//client: publicClient,
// 1b. Or public and/or wallet clients
client: { public: publicClient, wallet: walletClient }
})
// 2. Call contract methods, fetch events, listen to events, etc.
const result = await contract.read.totalSupply()
const logs = await contract.getEvents.Transfer()
const unwatch = contract.watchEvent.Transfer(
{ from: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e' },
{ onLogs(logs) { console.log(logs) } }
)
- Lisk
- Lisk Sepolia
import { createPublicClient, createWalletClient, http, custom } from 'viem'
import { lisk } from 'viem/chains'
import { EthereumProvider } from '@walletconnect/ethereum-provider'
import { publicActionsL2 } from 'viem/op-stack'
export const publicClient = createPublicClient({
chain: lisk,
transport: http(),
}).extend(publicActionsL2());
// eg: Metamask
export const walletClient = createWalletClient({
chain: lisk,
transport: custom(window.ethereum),
})
// eg: WalletConnect
const provider = await EthereumProvider.init({
projectId: "abcd1234",
showQrModal: true,
chains: [1],
})
export const walletClientWC = createWalletClient({
chain: lisk,
transport: custom(provider),
})
import { createPublicClient, createWalletClient, http, custom } from 'viem'
import { liskSepolia } from 'viem/chains'
import { EthereumProvider } from '@walletconnect/ethereum-provider'
import { publicActionsL2 } from 'viem/op-stack'
export const publicClient = createPublicClient({
chain: liskSepolia,
transport: http(),
}).extend(publicActionsL2());
// eg: Metamask
export const walletClient = createWalletClient({
chain: liskSepolia,
transport: custom(window.ethereum),
})
// eg: WalletConnect
const provider = await EthereumProvider.init({
projectId: "abcd1234",
showQrModal: true,
chains: [1],
})
export const walletClientWC = createWalletClient({
chain: liskSepolia,
transport: custom(provider),
})
The ABI of a contract can be found on the respective contract page in BlockScout.
export const wagmiAbi = [
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "message",
"type": "string"
}
],
"name": "NewHello",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "blacklist",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "counter",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_message",
"type": "string"
}
],
"name": "createHello",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "maxLength",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "message",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minLength",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string[]",
"name": "_newBlackList",
"type": "string[]"
}
],
"name": "setBlacklist",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_newMin",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "_newMax",
"type": "uint32"
}
],
"name": "setMinMaxMessageLength",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
];
CONTRACT_ADDRESS
is the address of the deployed contract.