@vara-eth/api
v0.3.2
Published
Typescript library for interacting with Vara.Eth network
Readme
Vara.Eth TypeScript API
TypeScript client library for Vara.Eth - a decentralized compute network that extends Ethereum with high-performance parallel execution, near-zero gas fees, and instant finalization without requiring asset bridging.
Table of Contents
- Installation
- Quick Start
- High-Level API
- Ethereum Side Operations
- Vara.Eth Side Operations
- Additional Resources
- License
Installation
npm install @vara-eth/apiPrerequisites
Install required peer dependencies:
npm install viem@npm:@vara-eth/[email protected] [email protected]Note:
@vara-eth/viemis a temporary fork of viem that adds support for EIP-7594, which is not yet available in the upstream package. Once EIP-7594 support lands in the official viem release, this library will switch back to the original package.
Quick Start
import { createVaraEthApi, WsVaraEthProvider } from '@vara-eth/api';
import { walletClientToSigner } from '@vara-eth/api/signer';
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
// Initialize Ethereum clients
const publicClient = createPublicClient({ transport: http('https://eth-rpc-url') });
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({ account, transport: http('https://eth-rpc-url') });
// Convert WalletClient to ITransactionSigner
const signer = walletClientToSigner(walletClient);
// Create fully initialized VaraEthApi in one call
const api = await createVaraEthApi(
new WsVaraEthProvider('ws://localhost:9944'),
publicClient,
routerAddress,
signer,
);
// Access Router and WVARA clients via api.eth
const router = api.eth.router;
const wvara = api.eth.wvara;High-Level API
Signer Interfaces
The library uses two signer interfaces to express the exact capabilities required at each use site:
// For message signing only (injected transactions, Metamask Snap adapters)
interface IMessageSigner {
signMessage(message: Uint8Array | string): Promise<Hash>;
getAddress(): Promise<Address>;
}
// For on-chain contract operations (extends IMessageSigner)
interface ITransactionSigner extends IMessageSigner {
sendTransaction(tx: TransactionRequest): Promise<Hash>;
}Viem WalletClient Adapter:
Convert viem's WalletClient to ITransactionSigner:
import { walletClientToSigner } from '@vara-eth/api/signer';
const signer = walletClientToSigner(walletClient);DynamicSigner — for reactive environments (e.g. React):
DynamicSigner resolves the active signer lazily at call time. This lets you swap wallets without recreating the VaraEthApi instance:
import { DynamicSigner } from '@vara-eth/api/signer';
const signer = new DynamicSigner(() => getCurrentSigner()); // getter called on every operation
const api = await createVaraEthApi(provider, publicClient, routerAddress, signer);createVaraEthApi
Factory function that constructs EthereumClient internally, waits for initialization, and returns a ready-to-use VaraEthApi. Use this instead of constructing EthereumClient and VaraEthApi separately.
import { createVaraEthApi, WsVaraEthProvider } from '@vara-eth/api';
const api = await createVaraEthApi(
new WsVaraEthProvider('ws://localhost:9944'), // or HttpVaraEthProvider
publicClient,
routerAddress,
signer, // optional ITransactionSigner
);
// EthereumClient is accessible via api.eth
const router = api.eth.router;
const wvara = api.eth.wvara;VaraEthApi
Main API class for interacting with the Vara.Eth network. Provides methods for querying program state and performing read-only operations. Use createVaraEthApi() to instantiate it.
// Query methods
await api.query.program.getIds(); // List all program IDs
await api.query.program.readState(hash); // Read program state
await api.query.program.codeId(programId); // Get program's code ID
// Call methods (read-only)
await api.call.program.calculateReplyForHandle(source, programId, payload);
// Create and send injected transactions
const injected = await api.createInjectedTransaction({
destination: programId,
payload: encodedPayload,
value: 0n,
});
await injected.send();
// Access EthereumClient for contract operations
api.eth.router;
api.eth.wvara;EthereumClient
Wrapper around viem's PublicClient and a signer that provides a unified interface for contract interactions. Automatically initializes Router and WrappedVara contract clients.
Note: Most users should use
createVaraEthApi()instead of constructingEthereumClientdirectly. Access it viaapi.ethwhen needed.
const signer = walletClientToSigner(walletClient);
const ethereumClient = new EthereumClient(publicClient, routerAddress, signer);
// Wait for initialization to complete
await ethereumClient.waitForInitialization();
// Access underlying clients
ethereumClient.publicClient;
ethereumClient.signer; // ITransactionSigner interface
// Get account address (async)
const address = await ethereumClient.getAccountAddress();
// Access contract clients
ethereumClient.router; // RouterClient instance
ethereumClient.wvara; // WrappedVaraClient instance
// Update signer if needed
ethereumClient.setSigner(newSigner);RouterClient
Interface for interacting with the Router contract - the main entry point for code validation and program creation on Ethereum.
Source: Router.sol
// Access via api.eth (recommended)
const router = api.eth.router;
await router.createProgram(codeId); // Create program from validated code
await router.createProgramWithAbiInterface(codeId, abiAddress); // Create with Solidity ABIMirrorClient
Interface for interacting with Mirror contracts - deployed programs on Ethereum. Each program has its own Mirror contract.
Source: Mirror.sol
const mirror = getMirrorClient({ address: programId, signer, publicClient });
await mirror.sendMessage(payload, value); // Send message to program
await mirror.executableBalanceTopUp(amount); // Top up program's balance
await mirror.stateHash(); // Get current state hashWrappedVaraClient
Interface for managing WVARA tokens (ERC20 wrapper for VARA) used for gas payments.
Source: WrappedVara.sol
// Access via api.eth (recommended)
const wvara = api.eth.wvara;
await wvara.approve(spender, amount); // Approve spending
await wvara.balanceOf(address); // Check balance
await wvara.allowance(owner, spender); // Check allowanceUploading Program Code
Before creating a program, you must upload and validate your WASM code using the vara-eth CLI.
Getting the CLI
Download from releases (recommended):
- Visit Gear repository releases
- Download the
ethexe-clibinary for your platform
Build from source:
cargo build -p ethexe-cli -rUploading Your Program
Insert your private key:
./target/release/ethexe key insert $SENDER_PRIVATE_KEYUpload your compiled WASM:
./target/release/ethexe --cfg none tx \
--ethereum-rpc "$ETH_RPC" \ # Ethereum node RPC
--ethereum-router "$ROUTER_ADDRESS" \ # Router contract address
--sender "$SENDER_ADDRESS" \ # Your account address
upload -l path/to/program.opt.wasmThe CLI will submit code via EIP-4844 blob transactions, request validation, and return a codeId once validators confirm it. Use this codeId with the API:
const codeId = '0x...'; // From CLI output
const tx = await api.eth.router.createProgram(codeId);Ethereum Side Operations
1. Program Creation
Create programs from validated code:
Note: Code must be uploaded and validated before creating programs. Use the Vara.Eth CLI to upload and validate WASM code. The CLI will provide you with a
codeIdafter successful validation.
// Create program from validated code
const codeId = '0x...'; // Code ID from vara-eth CLI
const tx = await api.eth.router.createProgram(codeId);
await tx.sendAndWaitForReceipt();
// Get the program ID
const programId = await tx.getProgramId();
console.log('Program created:', programId);
// Get Mirror contract for program interaction
const mirror = getMirrorClient({ address: programId, signer, publicClient });Creating Program with Solidity ABI Interface
const codeId = '0x...'; // Code ID from vara-eth CLI
// Deploy Solidity ABI contract
const deployHash = await walletClient.deployContract({
abi: counterAbi,
bytecode: counterBytecode,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: deployHash });
const abiAddress = receipt.contractAddress;
// Create program with ABI interface
const tx = await api.eth.router.createProgramWithAbiInterface(codeId, abiAddress);
await tx.sendAndWaitForReceipt();2. Sending Messages to Programs
Send messages and wait for replies:
Note: Programs are typically built with Sails framework. Use sails-js library to encode payloads and decode replies.
// Encode payload using sails-js
const sails = await Sails.new(); // Initialize from IDL
const payload = sails.services.Counter.functions.Increment.encodePayload();
// Send message
const tx = await mirror.sendMessage(payload, 0n);
await tx.send();
// Get message details
const message = await tx.getMessage();
console.log('Message ID:', message.id);
// Setup listener and wait for reply
const { waitForReply } = await tx.setupReplyListener();
const { payload: replyPayload, replyCode, value } = await waitForReply;
// Decode reply using sails-js
const result = sails.services.Counter.functions.Increment.decodeResult(replyPayload);
console.log('Result:', result);3. Managing Executable Balance
Programs require wVARA balance to execute. Top up using wVARA tokens:
// Check WVARA balance
const balance = await api.eth.wvara.balanceOf(await api.eth.getAccountAddress());
console.log('WVARA balance:', balance);
// Approve program to spend wVARA
const approveTx = await api.eth.wvara.approve(programId, BigInt(10 * 1e12));
await approveTx.sendAndWaitForReceipt();
const approvalData = await approveTx.getApprovalLog();
console.log('Approved amount:', approvalData.value);
// Top up program's executable balance
const topUpTx = await mirror.executableBalanceTopUp(BigInt(10 * 1e12));
const { status } = await topUpTx.sendAndWaitForReceipt();
console.log('Top-up status:', status);4. Checking Program State
Query program information from Router and Mirror contracts:
// Check code validation status
const codeState = await api.eth.router.codeState(codeId);
console.log('Code state:', codeState); // 'Validated' | 'Rejected' | 'Unknown'
// Get program's code ID
const programCodeId = await api.eth.router.programCodeId(programId);
// Get program's state hash
const stateHash = await mirror.stateHash();
// Read full program state from Vara.Eth
const state = await api.query.program.readState(stateHash);
console.log('Program active:', 'Active' in state.program);
// Get program nonce
const nonce = await mirror.nonce();5. Working with TxManager
Contract write methods return a TxManager instance that handles transaction lifecycle:
const tx = await api.eth.router.createProgram(codeId);
// Send transaction and get response
const response = await tx.send();
console.log('Transaction hash:', response.hash);
// Send and wait for receipt
const receipt = await tx.sendAndWaitForReceipt();
console.log('Status:', receipt.status);
console.log('Block number:', receipt.blockNumber);
// Estimate gas before sending
const gasEstimate = await tx.estimateGas();
console.log('Estimated gas:', gasEstimate);
// Access transaction request
const txRequest = tx.getTx();
console.log('Gas limit:', txRequest.gasLimit);
// Find specific events in receipt
await tx.send();
const event = await tx.findEvent('ProgramCreated');
console.log('Event args:', event.args);
// Use transaction-specific helper functions
const programId = await tx.getProgramId(); // Available on createProgram transactionsTxManager Helper Functions:
Each transaction type can have specific helper methods:
createProgram:getProgramId()- extracts program ID from eventrequestCodeValidation:waitForCodeGotValidated()- waits for validation completionapprove:getApprovalLog()- gets approval event datasendMessage:getMessage(),setupReplyListener()- message handling
Vara.Eth Side Operations
1. Instantiating VaraEthApi
Use createVaraEthApi() with an HTTP or WebSocket provider:
import { createVaraEthApi, HttpVaraEthProvider, WsVaraEthProvider } from '@vara-eth/api';
// HTTP Provider (for queries and calls)
const api = await createVaraEthApi(new HttpVaraEthProvider('http://localhost:9944'), publicClient, routerAddress, signer);
// WebSocket Provider (for subscriptions and real-time updates)
const api = await createVaraEthApi(new WsVaraEthProvider('ws://localhost:9944'), publicClient, routerAddress, signer);
// Don't forget to disconnect when done
await api.provider.disconnect();HTTP vs WebSocket Providers:
- HttpVaraEthProvider: Best for one-time queries and calls. Simpler, no persistent connection.
- WsVaraEthProvider: Required for subscriptions and real-time event listening. Maintains persistent connection.
2. Validator Pool
By default, injected transactions are sent to a single provider endpoint. A validator pool maintains a direct WebSocket connection to each known validator node, allowing the library to route each transaction to the validator that is scheduled to produce the next block. This eliminates one network hop and reduces confirmation latency.
When to use a pool:
- You know the RPC endpoints of the active validator set
- You want transactions delivered directly to the producing validator rather than relying on peer-to-peer forwarding
Creating a pool:
import { createVaraEthApi, VaraEthValidatorWsPool } from '@vara-eth/api';
const pool = new VaraEthValidatorWsPool([
{ address: '0xValidator1...', url: 'wss://validator-1.example.com' },
{ address: '0xValidator2...', url: 'wss://validator-2.example.com' },
{ address: '0xValidator3...', url: 'wss://validator-3.example.com' },
]);
const api = await createVaraEthApi(pool, publicClient, routerAddress, signer);Each entry maps a validator's Ethereum address to its WebSocket RPC URL. Addresses are compared case-insensitively.
Managing pool membership at runtime:
// Add a new validator (connects immediately)
await pool.addValidator('0xNewValidator...', 'wss://validator-4.example.com');
// Remove a validator (disconnects its WebSocket)
await pool.removeValidator('0xValidator1...');
// Inspect current pool members
console.log(pool.validatorAddresses);
// Check whether a specific address is in the pool
const inPool = pool.hasValidator('0xValidator2...');How routing works with injected transactions:
When you call setRecipient(), setSlotValidator() (or the deprecated but still supported setNextValidator()), or setDefaultValidator() on an InjectedTx, the library:
- Computes the target validator address (either the one you specified, the slot-scheduled validator, or the zero address)
- Sets that address as the transaction
recipientfield - If the provider is a pool and the target address is in the pool, routes the send/subscribe calls through that validator's dedicated WebSocket connection
- If the address is not in the pool (e.g. a new validator added on-chain that hasn't been added to the pool yet), the transaction is sent via the currently active pool connection — the receiving node will forward it to the intended validator
Targeting a specific validator:
const injected = await api.createInjectedTransaction({ destination: programId, payload });
// Target the validator scheduled to produce the imminent block
await injected.setSlotValidator();
// Or target a specific validator by address
await injected.setRecipient('0xValidator1...');
await injected.send();setSlotValidator() derives the assigned validator from the current slot (floor(timestamp / blockDuration) % validators.length), where timestamp is projected two blocks ahead to account for network propagation.
Sending without targeting a specific validator:
const injected = await api.createInjectedTransaction({ destination: programId, payload });
// Set zero address — any validator can process this transaction
injected.setDefaultValidator();
await injected.send();Use setDefaultValidator() when validator targeting is not important. The transaction will be processed by whichever validator produces the next available slot.
3. Injected Transactions
Injected transactions are Vara.Eth-native transactions sent directly to the network, bypassing Ethereum. They provide faster execution and lower costs for operations that don't require Ethereum settlement.
What are Injected Transactions?
Unlike regular messages sent through Mirror contracts on Ethereum, injected transactions are:
- Sent directly to Vara.Eth validators
- Signed with Ethereum private key but submitted off-chain
- Cheaper and faster (no Ethereum gas costs)
- Reference an Ethereum block for security
Creating and Sending Injected Transactions:
// Create injected transaction using API
const injected = await api.createInjectedTransaction({
destination: programId, // Program to send message to
payload: '0x1c436f756e74657224496e6372656d656e74', // Encoded message payload
value: 0n, // Optional value to send
// These are auto-populated if not provided:
// recipient: validator address (auto-selected)
// referenceBlock: recent Ethereum block hash (auto-fetched)
// salt: random salt for uniqueness
});
// Send transaction
const result = await injected.send();
// Wait for full transaction promise (includes reply)
const promise = await injected.sendAndWaitForPromise();
// Validate the promise signature
await promise.validateSignature(); // Throws if signature is invalidNote: The
Injectedclass has been renamed toInjectedTx. For backward compatibility,Injectedis still available as an alias.
Configuring Transaction Properties:
// Create injected transaction with all properties
const injected = await api.createInjectedTransaction({
destination: programId,
payload: encodedPayload,
value: 1000n,
referenceBlock: blockHash, // Specific Ethereum block
salt: '0x030405', // Custom salt
recipient: validatorAddress, // Specific validator
});
// Modify transaction using fluent API
injected
.setValue(2000n) // Update value
.setSalt('0x060708'); // Update salt
// Access transaction properties
const messageId = injected.messageId; // Vara.Eth message ID
// Update transaction fields
await injected.setReferenceBlock(); // Fetch latest Ethereum block
await injected.setRecipient(); // Auto-select next validator
// or specify a validator
await injected.setRecipient(validatorAddress);
// Send transaction
await injected.send();4. Querying Program Data
Query program information from Vara.Eth network:
// List all program IDs
const programIds = await api.query.program.getIds();
console.log('Total programs:', programIds.length);
// Get program's code ID
const codeId = await api.query.program.codeId(programId);
// Read program state by state hash
const stateHash = await mirror.stateHash(); // Get from Mirror contract
const state = await api.query.program.readState(stateHash);
if ('Active' in state.program) {
console.log('Program is active');
console.log('Balance:', state.balance);
}5. Reading Program State via calculateReplyForHandle
Perform read-only queries on program state without sending transactions:
Note: Use sails-js to encode query payloads and decode results.
// Encode query payload using sails-js
const queryPayload = sails.services.Counter.queries.GetValue.encodePayload();
// Calculate what the program would reply (read-only)
const reply = await api.call.program.calculateReplyForHandle(
await api.eth.getAccountAddress(), // Source address
programId, // Program to query
queryPayload, // Encoded query
);
// Decode result using sails-js
const value = sails.services.Counter.queries.GetValue.decodeResult(reply.payload);
console.log('Current counter value:', value);This method is useful for:
- Reading program state without modifying it
- Testing message payloads before sending
- Querying computed values from programs
Additional Resources
- Vara.Eth Whitepaper
- Sails Framework - Build Vara.Eth programs
- Sails-JS - Encode/decode payloads
- Viem Documentation - Ethereum client library
License
GPL 3.0 - see LICENSE
