voidchain-web3
v0.6.2
Published
TypeScript Web3 client for VoidChain session-based JSON-RPC APIs.
Maintainers
Readme
voidchain-web3
TypeScript client for VoidChain's session-based JSON-RPC API.
The library uses VoidChain chain_* methods for login, queries, transfers, deployment, and contract execution. Contract calldata and return values are encoded and decoded with viem, so contract calls feel close to viem while still using VoidChain's current session model.
中文操作手册见 docs/操作文档.md。
Install
npm install voidchain-web3Publish to npm / 发布到 npm
发布到 npm 前需要先编译,因为 npm 用户安装后实际使用的是编译后的
dist 文件,不是 src 源码。
npm.cmd test
npm.cmd run typecheck
npm.cmd run build
npm.cmd pack --dry-run发布前还要确认 package.json 里的版本号是 npm 上没有发布过的新版本:
npm.cmd version patch
npm.cmd publish如果已经手动修改过版本号,就不需要再执行 npm.cmd version patch,完成检查和构建后直接发布即可。
Clients
New projects should usually split public reads and wallet/session operations:
import {
createVoidChainPublicClient,
createVoidChainWalletClient,
} from 'voidchain-web3'
const publicClient = createVoidChainPublicClient({
endpoint: 'https://<voidchain-endpoint>',
sliceno: 0,
})
const walletClient = createVoidChainWalletClient({
endpoint: 'https://<voidchain-endpoint>',
sliceno: 0,
})Existing code can keep using the combined client:
import { createVoidChainClient } from 'voidchain-web3'
const client = createVoidChainClient({
endpoint: 'https://<voidchain-endpoint>',
sliceno: 0,
})createVoidChainClient() contains both Public Client and Wallet Client methods.
Register And Login
const registered = await walletClient.register({
account: '<new-account>',
password: '<password>',
})
const account = await walletClient.login({
account: '<new-account>',
password: '<password>',
})
console.log(registered.address)
console.log(account.address)register() creates an account but does not log it in. Transaction methods and mnemonic export require an active login session.
Public Queries
const blockNumber = await publicClient.getBlockNumber({ sliceno: 0 })
const block = await publicClient.getBlockByNumber({
number: Number(blockNumber),
sliceno: 0,
})
const addressInfo = await publicClient.getAddressInfo({
address: account.address,
blockTag: 'latest',
sliceno: 0,
})
const nonce = await publicClient.getTransactionCount({
address: account.address,
sliceno: 0,
})
console.log(block?.number)
console.log(addressInfo.Balance)
console.log(nonce)Public pubChainQuery calls use JSON-RPC id: 1, so they are not affected by a logged-in session.
const chainId = await publicClient.getChainId() // number, queried from the node
const balance = await publicClient.getBalance({
address: account.address,
blockTag: 'latest',
sliceno: 0,
}) // bigint in native base units (VoidChain native coin has 9 decimals)
const detail = await publicClient.getBlock({ number: 100, sliceno: 0 })
console.log(detail.parentHash, detail.stateRoot, detail.Txns)
const blocks = await publicClient.scanBlocks({ begin: 100, sliceno: 0 })
console.log(blocks[0]?.Senders, blocks[0]?.Data)getBlock() now uses native queryBlock and returns full block details, including
the node's Txns field. It is no longer an alias of getBlockByNumber(), which
still returns a summary from queryBlocks. Native query failures are propagated.
scanBlocks() performs one scanBlock request (up to 100 blocks per the 8.1
API), returns an array, and does not automatically poll or decode Base64 Data.
Both height inputs must be nonnegative safe integers. The default HTTP transport
keeps safe JSON integers as number and parses larger integers as bigint, so
nanosecond timestamps and large native balances are not rounded. A custom
transport is responsible for preserving numbers before returning its response.
Watch the latest height without duplicate callbacks:
const unwatchHeight = publicClient.watchBlockNumber({
emitOnBegin: true,
pollingIntervalMs: 3000,
sliceno: 0,
onBlockNumber(blockNumber) {
console.log(blockNumber)
},
})For an indexer, resume from a stored inclusive height and persist nextBlock
only after the batch has been processed:
const unwatchBlocks = publicClient.watchBlocks({
fromBlock: Number(await loadCursor()),
pollingIntervalMs: 3000,
sliceno: 0,
async onBlocks(blocks, { nextBlock }) {
await saveBlocks(blocks)
await saveCursor(nextBlock)
},
onError(error) {
console.error(error)
},
})
unwatchHeight()
unwatchBlocks()watchBlocks() removes duplicate heights and emits only the contiguous range
starting at fromBlock. If fromBlock is omitted, it starts after the current
latest block. The callback's nextBlock is the inclusive height for resuming
after a restart.
Transfer And Wait
const txHash = await walletClient.sendTransfer({
from: account.address,
to: '0x<receiver-address>',
value: 1000000000n,
sliceno: 0,
})
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
sliceno: 0,
pollingIntervalMs: 3000,
timeoutMs: null,
})
if (receipt.status === 2) {
console.log('success')
}timeoutMs: null means wait without a local timeout. Pending transactions are only queried again; the SDK never resends the original write transaction while waiting.
Legacy Cross-Space Transfer
The legacy cross-space flow uses two session-based writes. First call
transsend() to debit the source space and create the outgoing record. After
that transaction is confirmed, call transrecv() with the same sender, source
nonce, asset, and value to credit the destination space.
const sendHash = await walletClient.transsend({
value: 18n,
sliceno: 0,
destsliceno: 2,
})
const sendReceipt = await publicClient.waitForTransactionReceipt({
hash: sendHash,
sliceno: 0,
timeoutMs: null,
})
if (sendReceipt.nonce === undefined) {
throw new Error('VoidChain receipt did not include the source nonce')
}
const receiveHash = await walletClient.transrecv({
sender: account.address,
nonce: sendReceipt.nonce,
value: 18n,
sliceno: 2,
sousliceno: 0,
})Omit address for native TC, or pass the asset contract address for a token.
transsend.sliceno is the source space; transrecv.sliceno is the destination
space. sousliceno is the source space and keeps the VoidChain protocol's
documented spelling. Both methods require an active VoidChain session and
return a transaction hash. This SDK does not submit transrecv() automatically.
Raw Contract Call
Use call() when the DApp or wallet plugin already has raw calldata. The SDK returns raw Hex and does not need the ABI.
import { decodeFunctionResult, encodeFunctionData } from 'voidchain-web3'
const abi = [
{
type: 'function',
name: 'balanceOf',
stateMutability: 'view',
inputs: [
{ name: 'account', type: 'address' },
{ name: 'id', type: 'uint256' },
],
outputs: [{ name: '', type: 'uint256' }],
},
] as const
const data = encodeFunctionData({
abi,
functionName: 'balanceOf',
args: ['0x<owner-address>', 1n],
})
const returnData = await walletClient.call({
from: account.address,
to: '0x<contract-address>',
data,
sliceno: 0,
})
const balance = decodeFunctionResult({
abi,
functionName: 'balanceOf',
data: returnData,
})walletClient.call() and combined client.call() wrap VoidChain
chain_queryTransaction and require a session and from. They return
content.ret_data with a 0x prefix. Empty return data becomes "0x".
For a public raw call without a login session or ABI:
const raw = await publicClient.call({
to: '0x1111111111111111111111111111111111111111',
data: '0x12345678', // replace with the target function's encoded calldata
sliceno: 0,
})publicClient.call() uses eth_callSlice and returns raw Hex. from is
optional; value accepts bigint, number, or Hex (use bigint for large
amounts). Optional gas, gasPrice, and nonce use Hex. No historical
block parameter is exposed because the 8.1 call API does not document one.
ABI Contract Helpers
const balance = await publicClient.readContract({
address: '0x<contract-address>',
abi,
functionName: 'balanceOf',
args: ['0x<owner-address>'],
sliceno: 0,
})
const name = await walletClient.readContract({
address: '0x<contract-address>',
abi,
functionName: 'name',
args: [],
from: account.address,
sliceno: 0,
})
const txHash = await walletClient.writeContract({
address: '0x<contract-address>',
abi,
functionName: 'transfer',
args: ['0x<receiver-address>', 1000n],
from: account.address,
sliceno: 0,
})publicClient.readContract() uses eth_callSlice and does not require a
session. walletClient.readContract() uses VoidChain chain_queryTransaction
and requires a login session. Contract helpers use viem ABI tools for calldata
and return-value encoding.
Contract Events
Use getContractEvents() when you want decoded contract events instead of raw
logs.
import {
createVoidChainPublicClient,
parseAbiItem,
} from 'voidchain-web3'
const erc20Abi = [
parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
]
const transfers = await publicClient.getContractEvents({
address: '0x<erc20-contract>',
abi: erc20Abi,
eventName: 'Transfer',
args: {
from: '0x<sender-address>',
},
fromBlock: 1000n,
toBlock: 'latest',
sliceno: 0,
})
for (const event of transfers) {
console.log(event.eventName)
console.log(event.args)
console.log(event.transactionHash)
}getContractEvents() builds event topics from the ABI, calls
eth_getLogsSlice, and decodes each log with viem. It returns an array because
a block range can contain many matching events.
Use watchContractEvent() to poll new blocks and receive decoded events:
const unwatch = publicClient.watchContractEvent({
address: '0x<erc20-contract>',
abi: erc20Abi,
eventName: 'Transfer',
fromBlock: 'latest',
pollingIntervalMs: 3000,
sliceno: 0,
onLogs(logs) {
console.log(logs)
},
onError(error) {
console.error(error)
},
})
// Stop watching later.
unwatch()The watcher keeps lastScannedBlock in memory. Persist it in your app or
backend if you need to resume after a page refresh or process restart.
Common viem ABI helpers are re-exported from voidchain-web3:
import {
decodeAbiParameters,
decodeErrorResult,
decodeEventLog,
decodeFunctionData,
decodeFunctionResult,
encodeAbiParameters,
encodeEventTopics,
encodeFunctionData,
getAbiItem,
parseAbi,
parseAbiParameters,
parseAbiItem,
} from 'voidchain-web3'
import type {
Abi,
AbiEvent,
AbiFunction,
AbiParameter,
} from 'voidchain-web3'When the ABI is declared as const (or inline), contract function names,
arguments, return values, event names, filter arguments, and decoded event
arguments are inferred by TypeScript. readContract() accepts only view and
pure functions; writeContract() accepts only payable and nonpayable
functions. A dynamically typed Abi retains the broader fallback types.
Deploy Contract
const txHash = await walletClient.deployContract({
from: account.address,
abi,
bytecode: '0x60a0604052...',
args: [],
sliceno: 0,
})
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
sliceno: 0,
})
console.log(receipt.contractAddress)Simulate Deploy Contract
Use encodeDeployData() to locally encode bytecode + constructor args, then pass the encoded deployment data to simulateDeployContract().
import {
createVoidChainWalletClient,
encodeDeployData,
} from 'voidchain-web3'
const walletClient = createVoidChainWalletClient({
endpoint: 'https://<voidchain-endpoint>',
sliceno: 0,
})
const account = await walletClient.login({
account: '<account>',
password: '<password>',
})
const data = encodeDeployData({
abi,
bytecode: '0x60a0604052...',
args: [],
})
const simulation = await walletClient.simulateDeployContract({
from: account.address,
data,
value: 0n,
sliceno: 0,
})
console.log(simulation.returnData)
console.log(simulation.request)encodeDeployData() is local encoding only: it does not access the chain, does not sign, and does not require a session. simulateDeployContract() asks a VoidChain node to execute against current chain state, but it does not deploy the contract, send a transaction, or mutate state. Deployment simulation intentionally omits to because the contract address does not exist yet.
You can also estimate deployment gas with eth_estimateGas by passing from + data + value and omitting to:
const gas = await publicClient.eth.estimateGas({
from: account.address,
data,
value: '0x0',
})Mnemonic APIs
VoidChain account mnemonic import/export and local mnemonic derivation are separate features.
Node Import And Export
const exported = await walletClient.exportMnemonic()
const mnemonicBase64 = exported.mnemonicBase64
const mnemonic = exported.mnemonicexportMnemonic() calls VoidChain chain_exportKeystore with opcode=Account&subcode=exportMnem. Base64 is encoding, not encryption. Do not print, log, commit, or expose mnemonic values.
const restored = await walletClient.importMnemonic({
account: '<account>',
password: '<password>',
mnemonicBase64: '<base64-encoded-mnemonic>',
})Local Derivation
import {
DEFAULT_ETHEREUM_PATH,
createMnemonic,
findDerivationPath,
isValidMnemonic,
mnemonicToAccount,
mnemonicToAddress,
mnemonicToPrivateKey,
} from 'voidchain-web3'
const mnemonic = createMnemonic()
if (!isValidMnemonic(mnemonic)) {
throw new Error('Invalid mnemonic')
}
const account = mnemonicToAccount(mnemonic)
console.log(account.path) // m/44'/60'/0'/0/0
console.log(account.address)The default derivation path is DEFAULT_ETHEREUM_PATH, which is MetaMask's common Ethereum path:
m/44'/60'/0'/0/0You can derive another account by index:
const secondAddress = mnemonicToAddress(mnemonic, {
addressIndex: 1,
})Or use an explicit path. When path is provided, it takes priority over accountIndex and addressIndex.
const account = mnemonicToAccount(mnemonic, {
path: "m/44'/60'/0'/0/1",
})mnemonicToPrivateKey() is available for export, debugging, and migration flows:
const privateKey = mnemonicToPrivateKey(mnemonic)Avoid passing private keys through business code or exposing them to DApps. Current VoidChain write transactions still use the session-based sendTransaction() flow, not local private-key signing.
findDerivationPath() can search common MetaMask-style account and address indexes:
const found = findDerivationPath(mnemonic, '0x<address>', {
accountIndexEnd: 9,
addressIndexEnd: 99,
})
console.log(found?.path)Transport Options
const publicClient = createVoidChainPublicClient({
endpoint: 'https://<voidchain-endpoint>',
sliceno: 0,
transportOptions: {
timeoutMs: 10000,
retryCount: 3,
retryDelayMs: 150,
},
})Public queries use retry/backoff. State-changing operations such as register, login, transfers, deployment, and write-contract calls are not automatically retried.
Ethereum-Compatible RPC
Ethereum-compatible methods are available under client.eth or publicClient.eth.
const blockNumber = await publicClient.eth.blockNumberSlice({ sliceno: 0 })
const logs = await publicClient.eth.getLogsSlice({
address: '0x<contract-address>',
fromBlock: '0x0',
toBlock: 'latest',
topics: [],
sliceno: 0,
})VoidChain 8.1 slice methods are exposed with the same suffix:
eth.blockNumberSlice() -> eth_blockNumberSlice
eth.getBlockByNumberSlice() -> eth_getBlockByNumberSlice
eth.getTransactionCountSlice() -> eth_getTransactionCountSlice
eth.sendRawTransactionSlice() -> eth_sendRawTransactionSlice
eth.callSlice() -> eth_callSlice
eth.getBalanceSlice() -> eth_getBalanceSlice
eth.getLogs() -> eth_getLogs
eth.getLogsSlice() -> eth_getLogsSliceOlder aliases such as eth.getSliceBalance() and eth.getSliceTransactionCount() are still available for compatibility.
Wallet Plugin Note
voidchain-web3 does not expose an EIP-1193 request() provider. A browser wallet plugin should implement provider method dispatch, origin permissions, confirmation UI, and page/service-worker communication itself.
A plugin can map:
eth_call -> walletClient.call()
eth_sendTransaction -> walletClient.sendTransaction()The plugin should never expose session ids, passwords, or mnemonics to DApps.
Errors
The client exports typed errors:
VoidChainRpcErrorVoidChainResponseErrorVoidChainContractErrorVoidChainSessionErrorVoidChainTimeoutErrorVoidChainAbortError
VoidChain ret !== "0" responses throw VoidChainResponseError. Contract execution errors returned in content.err throw VoidChainContractError.
Local Signing
Local signing uses viem accounts and does not require a VoidChain session.
import {
VOIDCHAIN_TESTNET_CHAIN_ID,
mnemonicToAccount,
personalSign,
signAuthorization,
signTypedDataV4,
} from 'voidchain-web3'
const account = mnemonicToAccount('<mnemonic>')
const messageSignature = await personalSign({
account,
message: 'hello voidchain',
})
const typedSignature = await signTypedDataV4({
account,
domain: {
name: 'VoidChain App',
version: '1',
chainId: VOIDCHAIN_TESTNET_CHAIN_ID,
},
types: {
Message: [{ name: 'contents', type: 'string' }],
},
primaryType: 'Message',
message: {
contents: 'Hello',
},
})
const authorization = await signAuthorization({
account,
contractAddress: '0x<delegate-contract>',
nonce: 0,
})VoidChain chain IDs:
testnet: 36500
mainnet: 36000