Integrate

Frontend Integration

Connect web frontends to Vara.eth programs using TypeScript and JavaScript SDKs.

Frontend Integration

Vara.eth programs are designed to feel like standard Ethereum smart contracts from a frontend perspective. You connect a wallet, call methods, listen to events — all through familiar tools. The key difference is under the hood: your calls go to a Mirror contract on L1, which routes them to off-chain WASM execution with instant pre-confirmed results.

SDK Overview

Three libraries power frontend integration:

LibraryPurposeInstall
@vara-eth/apiCore Vara.eth client — EthereumClient, VaraEthApi, injected transactions, state queriesnpm install @vara-eth/api
sails-jsTyped client generated from your program's IDL — handles payload encoding and decodingnpm install sails-js

@vara-eth/api is the primary SDK. It provides two interaction paths:

  • Ethereum-sideEthereumClient with RouterClient, MirrorClient, and WrappedVaraClient for on-chain operations
  • Vara.eth-sideVaraEthApi with injected transactions and calculateReplyForHandle for direct validator communication (pre-confirmations, instant reads)

Use sails-js alongside @vara-eth/api to encode/decode payloads from your program's IDL. See the full RPC API reference for detailed SDK documentation.

Method Coverage by Flow

This section maps the full product flow to concrete methods, so frontend teams can see what is handled in UI and what is typically handled by ops/backend.

Flow StepPrimary Method(s)Typical Owner
Upload codeethexe tx upload ... --watchDevOps / release pipeline
Create programrouter.createProgram(...) or router.createProgramWithAbiInterface(...)Backend / deploy scripts
Top up executable balancewvara.approve(...) + mirror.executableBalanceTopUp(...)Backend / keeper
Send user action (L1 path)mirror.sendMessage(...) (via sails-js typed call)Frontend
Send fast action (injected path)api.createInjectedTransaction(...) + sendAndWaitForPromise()Frontend (advanced UX)
Read canonical statemirror.stateHash() + api.query.program.readState(...)Frontend / backend
Read computed value (no write)api.call.program.calculateReplyForHandle(...)Frontend / backend

Frontend Scope

Frontend usually owns interaction and reads. Upload/create/top-up are usually operational flows (backend scripts, CI, or manual CLI) and should not be hardcoded into browser UI.

Project Setup

Install Dependencies

npm install @vara-eth/api viem@^2.39.0 [email protected] sails-js

Generate Typed Client

From your program's IDL file, generate TypeScript bindings:

npx sails-js-cli generate ./path/to/your_program.idl --out ./src/generated

This creates a typed client class with methods matching your program's services and routes. Every method call is automatically encoded to the correct SCALE format. For more details, see sails-js client generation.

Environment Configuration

config.ts
export const config = {
  // Ethereum RPC (Hoodi testnet)
  ethRpcHttp: "https://hoodi-reth-rpc.gear-tech.io",
  ethRpcWs: "wss://hoodi-reth-rpc.gear-tech.io/ws",

  // Vara.eth RPC (for pre-confirmations and state queries)
  varaEthWs: "wss://vara-eth-validator-1.gear-tech.io:9944",

  // Contract addresses
  routerAddress: "0xBC888a8B050B9B76a985d91c815d2c4f2131a58A",
  programId: "0x...", // Your program's Mirror address
};

Addresses Change by Network

Keep addresses in one source of truth: Contract Addresses.

Connecting to a Program

import { ethers } from "ethers";
import { MyProgram } from "./generated";

// Connect wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

// Create typed program instance
const program = new MyProgram(signer, config.programId);

// Call methods with full type safety
const result = await program.myService.myMethod(arg1, arg2);

With ethers.js + ABI (Alternative)

If the program was deployed with createProgramWithAbiInterface(...), you can interact through the standard Ethereum ABI:

import { ethers } from "ethers";
import abi from "./MyProgramAbi.json";

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

const mirror = new ethers.Contract(config.programId, abi, signer);

// Call like a normal Ethereum contract
const tx = await mirror.myMethod(arg1, arg2);
await tx.wait();

Etherscan Compatibility

The ABI approach works natively with Etherscan's "Write as Proxy" interface, making debugging and manual testing straightforward.

MetaMask Integration

Vara.eth uses standard Ethereum transactions, so MetaMask works out of the box. Users sign transactions to the Mirror contract on L1 — no custom wallet adapters or network switching required.

Connecting MetaMask

async function connectWallet() {
  if (!window.ethereum) {
    throw new Error("MetaMask not found");
  }

  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const address = await signer.getAddress();

  return { provider, signer, address };
}

Adding Hoodi Testnet

async function addHoodiNetwork() {
  await window.ethereum.request({
    method: "wallet_addEthereumChain",
    params: [
      {
        chainId: "0x88bb0", // 560048 in hex
        chainName: "Ethereum Hoodi Testnet",
        rpcUrls: ["https://hoodi-reth-rpc.gear-tech.io"],
        nativeCurrency: {
          name: "ETH",
          symbol: "ETH",
          decimals: 18,
        },
      },
    ],
  });
}

Type Generation from IDL

The IDL file is the source of truth for your program's interface. It describes services, methods, parameters, return types, and events.

Generating TypeScript Types

npx sails-js-cli generate \
  ./target/wasm32-gear/release/my_program.idl \
  --out ./src/generated

Generating Solidity ABI

cargo sails sol --idl-path ./target/wasm32-gear/release/my_program.idl

What Gets Generated

The sails-js generator produces:

  • Program class — main entry point with typed methods for each service
  • Type definitions — TypeScript interfaces matching your Rust structs and enums
  • Event types — typed event listeners for program emissions
  • Encoding/decoding — automatic SCALE serialization, invisible to the developer

Handling Pre-Confirmations in UX

Pre-confirmations give your dApp sub-second feedback. See Events & State Reading for the full explanation.

Optimistic UI Pattern

async function handleSwap(tokenA: string, tokenB: string, amount: bigint) {
  // 1. Show pending state in UI
  setStatus("pending");

  // 2. Send an Ethereum-side transaction through Mirror
  const tx = await program.dex.swap(tokenA, tokenB, amount);

  // 3. Wait for Ethereum confirmation and then refresh state
  await tx.wait();
  const updated = await program.dex.balanceOfCurrentUser();
  setBalance(updated);
  setStatus("confirmed");
}

Injected Pre-Confirmation Pattern (@vara-eth/api)

For sub-second feedback, use Vara.eth-side injected transactions:

import { VaraEthApi, WsVaraEthProvider, EthereumClient } from "@vara-eth/api";

const ethereumClient = new EthereumClient(publicClient, config.routerAddress, signerAdapter);
await ethereumClient.waitForInitialization();

const api = new VaraEthApi(new WsVaraEthProvider(config.varaEthWs), ethereumClient);

const injected = await api.createInjectedTransaction({
  destination: config.programId,
  payload: encodedPayload, // encode with sails-js
  value: 0n,
});

const promise = await injected.sendAndWaitForPromise();
await promise.validateSignature(); // cryptographic check

setStatus("pre-confirmed");
const replyPayload = promise.payload;
// decode replyPayload with sails-js

Trust Levels

Trust LevelWhen to UseExample
Full trustUI updates, display dataShow swap result, game state
CautionFinancial decisionsDisplay pending balance with indicator
Wait for finalityHigh-value settlementsLarge transfers, withdrawals

Listening to Events

Mirror Events (Standard Ethereum)

const provider = new ethers.WebSocketProvider(config.ethRpcWs);
const mirror = new ethers.Contract(config.programId, mirrorAbi, provider);

mirror.on("StateChanged", (stateHash, event) => {
  console.log("New state:", stateHash);
});

mirror.on("Message", (id, destination, payload, value, event) => {
  console.log("Outgoing message from program");
});

Custom Program Events (via Sails)

// With sails-js typed events
program.on("SwapExecuted", (event) => {
  console.log(`Swapped ${event.amountIn} for ${event.amountOut}`);
  updateTradeHistory(event);
});

Reading State

Via RPC (Free, Instant)

// Typed state query via sails-js
const balance = await program.token.balanceOf(userAddress);
const totalSupply = await program.token.totalSupply();
// These are free — no L1 transaction needed

Error Handling

try {
  const tx = await program.myService.myMethod(args);
  const result = await tx.wait();
} catch (error) {
  if (error.code === "INSUFFICIENT_FUNDS") {
    showError("Insufficient ETH for transaction fee");
  } else if (error.message.includes("ExecutableBalance")) {
    showError("Program temporarily unavailable. Please try again later.");
  } else if (error.message.includes("not initialized")) {
    showError("Program is not yet active");
  } else {
    showError("Transaction failed: " + error.message);
  }
}
ErrorCauseSolution
User rejected transactionMetaMask popup declinedShow retry prompt
Insufficient ETHNot enough ETH for L1 gasPrompt user to fund wallet
Executable balance zeroProgram has no wVARADeveloper must top up program
Program not initializedFirst message not sentInitialize the program first
Encoding errorWrong payload formatCheck IDL matches your program version

Complete Example: React dApp

import { useState } from "react";
import { ethers } from "ethers";
import { MyProgram } from "./generated";

function App() {
  const [program, setProgram] = useState<MyProgram | null>(null);
  const [counter, setCounter] = useState<number>(0);
  const [status, setStatus] = useState<string>("disconnected");

  async function connect() {
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const p = new MyProgram(signer, PROGRAM_ADDRESS);
    setProgram(p);
    setStatus("connected");

    // Load initial state
    const value = await p.counter.get();
    setCounter(Number(value));
  }

  async function increment() {
    if (!program) return;
    setStatus("sending...");

    const tx = await program.counter.increment();

    // Pre-confirmed result — sub-second
    setCounter((prev) => prev + 1);
    setStatus("pre-confirmed");

    // Wait for L1 finality
    await tx.waitForFinality();
    setStatus("finalized");
  }

  return (
    <div>
      <button onClick={connect}>Connect Wallet</button>
      <p>Counter: {counter}</p>
      <button onClick={increment} disabled={!program}>
        Increment
      </button>
      <p>Status: {status}</p>
    </div>
  );
}

On this page