Use the Network Testnet · v1.10 · all tests passing
Estimated read time: 9 minutes

Interact with a Contract

Reads versus writes

Calls to a contract fall into two camps: reads that return state without changing it, and writes that send a signed transaction to mutate state. Reads are free, return instantly, and require no wallet. Writes cost VLD for gas, take one block to confirm (~2 seconds on testnet), and need a signature. Almost every confusing thing about contract interaction reduces to "I sent a write where a read would have done" — start by being sure which one you need.

Reading state

A read call lands on a JSON-RPC endpoint, hits a node's local copy of the chain, and returns the function's view of state at the latest block. The wallet is not involved; no signing happens. From the SDK:

import { ValdiumClient, Contract } from '@valdium/sdk'; import erc20 from '@valdium/sdk/abis/erc20.js'; const client = new ValdiumClient('https://testnet.valdium.xyz/rpc'); const usdv = new Contract('0x5aC14fD3A2c…0C', erc20, client); const balance = await usdv.balanceOf('0xAbCd…1234'); const symbol = await usdv.symbol(); console.log(symbol, balance.toString());

From the CLI:

valdium contract call \ --address 0x5aC14fD3A2c0e8ee48B07C8A3D1Ebf27b0EeDF0C \ --abi /path/to/erc20.json \ balanceOf 0xAbCd…1234

Every Valdium node serves these reads; the SDK and CLI default to the public testnet RPC if no other endpoint is configured.

Writing state — the wallet path

A write call is a transaction whose data field encodes the function selector and arguments. The wallet builds the calldata, fills in the gas and nonce, asks the user, signs with Dilithium3, and submits.

const usdv = new Contract( '0x5aC14fD3A2c0e8ee48B07C8A3D1Ebf27b0EeDF0C', erc20, signer // ← from window.valdium, not a read-only client ); const tx = await usdv.transfer( '0xRecipient…', '1000000' // 1 USDv if decimals = 6 ); const receipt = await tx.wait(); console.log('included in block', receipt.blockNumber);

The wallet popup will show: the contract address, the decoded function name and arguments (if the contract source is verified), the VLD value being attached, and a gas estimate. If decoding fails the popup says so explicitly — that's the signal to read the source on the explorer before you click.

Writing state — the CLI path

For scripted flows where a popup is the wrong UX, the CLI signs locally with an unlocked keystore:

valdium contract send \ --address 0x5aC14fD3A2c… \ --abi /path/to/erc20.json \ --from my-wallet \ transfer 0xRecipient… 1000000

The first run prompts for the keystore passphrase and holds the unlock for the duration of the CLI session. For long-lived scripts (cron, CI) a hardware signer is the right answer; for one-shot interactive calls the passphrase-unlocked flow is fine.

Calldata, ABIs, and selectors

The calldata of a transaction is a 4-byte function selector followed by the ABI-encoded arguments. The selector is the first four bytes of BLAKE3("functionName(argType1,argType2)") — note the BLAKE3, not blake3; this is one of the wire-level points where Valdium diverges from Ethereum. The SDK handles the encoding; if you ever have to inspect raw calldata, the first 8 hex chars after 0x are the selector and the rest is the argument payload.

For unverified contracts the explorer cannot decode the calldata for you, so the wallet shows the raw selector and a hex blob in the prompt. You should not sign a write to an unverified contract unless you have read the source through another channel.

Estimating gas

Before sending a write, you can ask any node what the call would cost without paying:

const estimate = await usdv.estimateGas.transfer( '0xRecipient…', '1000000' ); console.log(estimate.toString(), 'gas units');

The node executes the call against the pending state and returns the gas it consumed. The wallet runs the same estimate before showing the popup, and the popup multiplies by the current baseFee + maxPriorityFee to display a VLD figure. See Fee Market for the a base-fee + priority-tip-style pricing model Valdium inherits.

Confirmations and receipts

A submitted transaction returns a hash immediately. The chain produces a 2-second block; the next block (sometimes the one after) includes the tx. tx.wait() blocks until the receipt is available; the receipt tells you whether the call succeeded from the contract's perspective:

const receipt = await tx.wait(); if (receipt.status === 1) { // success } else { // contract reverted — read receipt.logs for clues }

A status of 0 means the contract throw'd or ran out of gas. You still paid for the gas you used up to the revert point; the state change was rolled back. The revert reason, if the contract included one, is in the revertReason field of the receipt.

Reading event logs

Contracts emit events; the receipt's logs array carries them. Each log has an address, an indexed topics array, and a non-indexed data blob. The SDK decodes them against the ABI:

for (const log of receipt.logs) { const parsed = usdv.interface.parseLog(log); if (parsed && parsed.name === 'Transfer') { console.log(parsed.args.from, '→', parsed.args.to, parsed.args.value); } }

Querying historical events is the same call against a node, scoped to a block range:

const events = await usdv.queryFilter( usdv.filters.Transfer(myAddress), fromBlock, toBlock );

Failure modes worth knowing

"Insufficient funds for gas." The account has zero or near-zero VLD. Get some from the faucet before retrying.

"Nonce too low." The wallet sent a tx with a nonce already used by a previous one. Most wallets auto-bump; the CLI requires --nonce for advanced flows.

"Reverted without reason." The contract used a bare throw with no message. Read the source. If you cannot read the source, do not retry.

"Pending forever." The submitted gas price was below the current base fee; the tx sits in the mempool until either the fee rises to meet it (rare) or you replace it with a higher-fee transaction at the same nonce.