Jesse A. Green / notebook

Union -- User Guide

Generated May 2026 from unionlabs/union at 6f56186

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Union is the hyper-efficient, zero-knowledge Inter-Blockchain Communication (IBC) infrastructure for general message passing, asset transfers, NFTs, and DeFi across heterogeneous blockchains. It connects Cosmos-SDK chains, EVM networks, Sui, Starknet, Movement/Aptos, and more through a trust-minimized ZK-proof-based consensus bridge and an intelligent relay network.

Whether you want to bridge assets through the web UI, build programmatic cross-chain transfers with the TypeScript SDK, run a Union validator, operate a Voyager relay node, or decode on-chain data with the u CLI -- this guide covers it all.


Table of Contents


1. Quick Start

The fastest way to experience Union is to bridge assets through the web application:

  1. Visit app.union.build
  2. Connect your wallet (MetaMask, Keplr, Leap, or a Sui wallet)
  3. Select a source chain, destination chain, and token
  4. Enter the amount, confirm the transaction, and wait for cross-chain settlement

Tip

For testnet experimentation, use the Union faucet (drip service) to get free testnet tokens before bridging. The testnet runs on union-testnet-10 and connects to Sepolia, Holesky, and Arbitrum Sepolia.

For programmatic access, install the TypeScript SDK:

npm install @unionlabs/sdk @unionlabs/sdk-evm
# or for Cosmos chains:
npm install @unionlabs/sdk @unionlabs/sdk-cosmos
# or for Sui:
npm install @unionlabs/sdk @unionlabs/sdk-sui

For infrastructure operators, continue to Installation for uniond, voyager, and unionvisor setup.


2. Installation

TypeScript SDK

# Core SDK (required for all chain types)
npm install @unionlabs/sdk

# Chain-specific packages
npm install @unionlabs/sdk-evm      # EVM chains (Ethereum, Arbitrum, Base, etc.)
npm install @unionlabs/sdk-cosmos    # Cosmos-SDK chains (Union, Babylon, Osmosis, etc.)
npm install @unionlabs/sdk-sui       # Sui

The SDK uses Effect-TS as its foundation. Peer dependencies include effect, viem (for EVM), @cosmjs/proto-signing and @cosmjs/stargate (for Cosmos), and @mysten/sui (for Sui).

Important

The SDK requires Node.js 18+ and TypeScript 5.0+. Effect-TS is a core dependency -- you must install effect alongside the Union SDK packages.

uniond (Chain Binary)

uniond is a Cosmos-SDK binary written in Go. Build from source:

git clone https://github.com/unionlabs/union.git
cd union/uniond
go build -o uniond ./cmd/uniond

Alternatively, download a pre-built binary from the official releases or use the Nix build system:

nix build .#uniond

Voyager (IBC Relayer)

Voyager is a Rust binary. Build from source:

git clone https://github.com/unionlabs/union.git
cd union
cargo build --release -p voyager

The resulting binary is at target/release/voyager.

unionvisor (Process Supervisor)

cd union
cargo build --release -p unionvisor

u CLI Tool

cd union
cargo build --release -p u

drip (Faucet)

cd union
cargo build --release -p drip

galoisd (ZK Proving Service)

cd union/galoisd
go build -o galoisd ./cmd/galoisd

Warning

Building from source requires a Rust toolchain (stable), Go 1.21+, and protocol buffer tooling. The Nix flake in the repository provides a reproducible build environment with all dependencies pre-configured. If you encounter build issues, use nix develop to enter the development shell.


3. Configuration

TypeScript SDK Configuration

The SDK is configured through the Effect-TS dependency injection pattern. Each operation requires specific service layers:

Service Layer Purpose Chain Type
Evm.PublicClient Read-only EVM chain access EVM
Evm.WalletClient Transaction signing for EVM EVM
Cosmos.Client Read-only Cosmos chain access Cosmos
Cosmos.SigningClient Transaction signing for Cosmos Cosmos
Sui.PublicClient Read-only Sui chain access Sui
Sui.WalletClient Transaction signing for Sui Sui
ChainRegistry.Default Chain metadata resolution All

Environment variables used by SDK examples:

Variable Purpose
PRIVATE_KEY EVM private key (hex-prefixed)
KEY Alternative EVM private key
MNEMONIC / MEMO Cosmos wallet mnemonic
SUI_MNEMONIC Sui wallet mnemonic
RECIPIENT Default recipient address
ADDRESS Default sender/query address

Voyager Configuration

Voyager reads its configuration from a JSON or JSONC file specified with -c:

voyager -c /path/to/config.json start

Generate a default configuration:

voyager config default

Print the JSON Schema for validation:

voyager config schema

uniond Configuration

Standard Cosmos-SDK configuration via config.toml and app.toml in the node home directory (default ~/.union):

uniond init my-node --chain-id union-1

Key configuration files:

  • ~/.union/config/config.toml -- CometBFT consensus, P2P, RPC settings
  • ~/.union/config/app.toml -- application-level settings (gas, pruning, API)
  • ~/.union/config/genesis.json -- chain genesis state

The address prefix for Union is union.

unionvisor Configuration

unionvisor uses a bundle directory structure:

bundle/
├── genesis.json
├── meta.json
└── versions/
    ├── v0.8.0/
    │   └── uniond
    └── v0.9.0/
        └── uniond

The meta.json defines the binary name, fallback version, and versions directory path.

Environment variables:

Variable Purpose
UNIONVISOR_ROOT Root directory for unionvisor state
UNIONVISOR_BUNDLE Path to the bundle directory
UNIONVISOR_POLL_INTERVAL Upgrade poll interval

drip (Faucet) Configuration

Drip uses a JSON config file:

{
  "log_format": "text",
  "secret": "captcha-secret",
  "bypass_secret": "bypass-key",
  "max_request_polls": 7,
  "chains": [
    {
      "id": "union-devnet-1",
      "bech32_prefix": "union",
      "memo": "drip drop greetings from union faucet",
      "ws_url": "ws://localhost:26657/websocket",
      "grpc_url": "http://localhost:9090",
      "gas_config": {
        "gas_price": "1.0",
        "gas_denom": "au",
        "gas_multiplier": "1.1",
        "max_gas": 40000000
      },
      "signer": "0x...",
      "coins": [
        { "denom": "au", "amount": 13370 }
      ]
    }
  ]
}

4. Core Usage -- Web UI (app.union.build)

The app2 web application at app.union.build is a SvelteKit-based interface for bridging assets across all supported chains.

Connecting Wallets

The app supports multiple wallet types simultaneously:

Wallet Type Supported Wallets Chain Type
EVM MetaMask, WalletConnect, injected Ethereum, Arbitrum, Base, BSC, etc.
Cosmos Keplr, Leap Union, Babylon, Osmosis, Sei, Xion
Sui Sui Wallet Sui

Click the wallet icon in the header to manage connected wallets. Multiple wallets across different chain types can be connected at once.

Bridging Assets

  1. Navigate to the Transfer page
  2. Select the source chain and asset from the dropdown
  3. Select the destination chain
  4. Enter the transfer amount
  5. The quote amount displays the expected received amount on the destination
  6. Confirm and sign the transaction in your wallet

Transfer Tracking

After submitting a transfer, track its progress through the Explorer section:

  • /explorer/transfers -- view all transfers
  • /explorer/packets/{packet_hash} -- individual packet details
  • /explorer/channels -- browse active IBC channels
  • /explorer/clients -- view light client states

Additional Features

  • Faucet (/faucet) -- request testnet tokens
  • Liquid Staking (/liquid-staking) -- stake Union tokens
  • Dashboard (/dashboard) -- view balances, missions, achievements, and leaderboard
  • Stake (/stake) -- manage validator delegations

5. Core Usage -- TypeScript SDK

The TypeScript SDK provides programmatic cross-chain transfer capabilities through the @unionlabs/sdk family of packages. All operations are built on Effect-TS for type-safe, composable, and error-tracked workflows.

EVM-to-EVM Transfer

import { Evm, EvmZkgmClient } from "@unionlabs/sdk-evm"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { UniversalChainId } from "@unionlabs/sdk/schema/chain"
import { ChannelId } from "@unionlabs/sdk/schema/channel"
import * as TokenOrder from "@unionlabs/sdk/TokenOrder"
import * as ZkgmClient from "@unionlabs/sdk/ZkgmClient"
import * as ZkgmClientRequest from "@unionlabs/sdk/ZkgmClientRequest"
import * as ZkgmClientResponse from "@unionlabs/sdk/ZkgmClientResponse"
import * as ZkgmIncomingMessage from "@unionlabs/sdk/ZkgmIncomingMessage"
import { Effect, Logger } from "effect"
import { http } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { holesky } from "viem/chains"

const program = Effect.gen(function*() {
  const source = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.17000"),
  )
  const destination = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.11155111"),
  )

  const tokenOrder = yield* TokenOrder.make({
    source,
    destination,
    sender: "0x06627714f3F17a701f7074a12C02847a5D2Ca487",
    receiver: "0x50A22f95bcB21E7bFb63c7A8544AC0683dCeA302",
    baseToken: "0x685ce6742351ae9b618f383883d6d1e0c5a31b4b",
    baseAmount: 10n,
    quoteToken: "0x80fdbf104ec58a527ec40f7b03f88c404ef4ba63",
    quoteAmount: 10n,
    kind: "escrow",
    metadata: undefined,
    version: 2,
  })

  const request = ZkgmClientRequest.make({
    source,
    destination,
    channelId: ChannelId.make(2),
    ucs03Address: "0x5fbe74a283f7954f10aa04c2edf55578811aeb03",
    kind: "simulateAndExecute",
    instruction: tokenOrder,
  })

  const zkgmClient = yield* ZkgmClient.ZkgmClient
  const response: ZkgmClientResponse.ZkgmClientResponse =
    yield* zkgmClient.execute(request)

  yield* Effect.log("Submission Hash", response.txHash)

  const completion = yield* response.waitFor(
    ZkgmIncomingMessage.LifecycleEvent.$is(
      "EvmTransactionReceiptComplete",
    ),
  )
  yield* Effect.log("Completion", completion)
}).pipe(
  Effect.provide(EvmZkgmClient.layerWithoutWallet),
  Effect.provide(Evm.WalletClient.Live({
    account: privateKeyToAccount(process.env.KEY as any),
    chain: holesky,
    transport: http("https://rpc.17000.ethereum.chain.kitchen"),
  })),
  Effect.provide(Evm.PublicClient.Live({
    chain: holesky,
    transport: http("https://rpc.17000.ethereum.chain.kitchen"),
  })),
  Effect.provide(ChainRegistry.Default),
  Effect.provide(Logger.replace(
    Logger.defaultLogger,
    Logger.prettyLoggerDefault,
  )),
)

Effect.runPromise(program).then(console.log).catch(console.error)

Cosmos-to-EVM Transfer

import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"
import { TokenOrder, ZkgmClient, ZkgmClientRequest, ZkgmClientResponse } from "@unionlabs/sdk"
import { Cosmos, CosmosZkgmClient } from "@unionlabs/sdk-cosmos"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { UniversalChainId } from "@unionlabs/sdk/schema/chain"
import { ChannelId } from "@unionlabs/sdk/schema/channel"
import { Effect, Logger } from "effect"

const program = Effect.gen(function*() {
  const source = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("union.union-testnet-10"),
  )
  const destination = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.11155111"),
  )

  const tokenOrder = yield* TokenOrder.make({
    source,
    destination,
    sender: "union122ny3mep2l7nhtafpwav2y9e5jrslhek76hsjl",
    receiver: "0x50A22f95bcB21E7bFb63c7A8544AC0683dCeA302",
    baseToken: "au",
    baseAmount: 10n,
    quoteToken: "0xba5eD44733953d79717F6269357C77718C8Ba5ed",
    quoteAmount: 10n,
    kind: "solve",
    metadata: "0x...",
    version: 2,
  })

  const request = ZkgmClientRequest.make({
    source,
    destination,
    channelId: ChannelId.make(1),
    ucs03Address:
      "union1336jj8ertl8h7rdvnz4dh5rqahd09cy0x43guhsxx6xyrztx292qpe64fh",
    instruction: tokenOrder,
  })

  const zkgmClient = yield* ZkgmClient.ZkgmClient
  const response: ZkgmClientResponse.ZkgmClientResponse =
    yield* zkgmClient.execute(request)
  yield* Effect.log("TX Hash:", response.txHash)
}).pipe(
  Effect.provide(CosmosZkgmClient.layerWithoutSigningClient),
  Effect.provide(Cosmos.SigningClient.Live(
    "union122ny3mep2l7nhtafpwav2y9e5jrslhek76hsjl",
    "https://rpc.union-testnet-10.union.chain.kitchen",
    await DirectSecp256k1HdWallet.fromMnemonic(
      "your mnemonic here",
      { prefix: "union" },
    ),
    { gasPrice: GasPrice.fromString("100000au") },
  )),
  Effect.provide(Cosmos.Client.Live(
    "https://rpc.union-testnet-10.union.chain.kitchen",
  )),
  Effect.provide(ChainRegistry.Default),
  Effect.provide(Logger.replace(
    Logger.defaultLogger,
    Logger.prettyLoggerDefault,
  )),
)

Effect.runPromise(program).then(console.log).catch(console.error)

Cosmos-to-Cosmos Transfer

import { Effect } from "effect"
import { toHex } from "viem"
import {
  createCosmosToCosmosFungibleAssetOrder,
} from "@unionlabs/sdk/ucs03/fungible-asset-order"
import {
  CosmWasmClientSource,
  CosmWasmClientDestination,
  createCosmWasmClient,
} from "@unionlabs/sdk/cosmos/client"
import {
  CosmosChannelSource,
  CosmosChannelDestination,
} from "@unionlabs/sdk/cosmos/channel"
import { AddressCosmosZkgm } from "@unionlabs/sdk/schema/address"
import { UniversalChainId, ChannelId } from "@unionlabs/sdk/schema"

const program = Effect.gen(function*() {
  const sourceClient = yield* createCosmWasmClient(
    "https://rpc.bbn-test-5.babylon.chain.kitchen",
  )
  const destClient = yield* createCosmWasmClient(
    "https://rpc.osmo-test-5.osmosis.chain.kitchen",
  )

  return yield* createCosmosToCosmosFungibleAssetOrder({
    sender: AddressCosmosZkgm.make(toHex("bbn1...")),
    receiver: AddressCosmosZkgm.make(toHex("osmo1...")),
    baseToken: "ubbn",
    baseAmount: 1000000n,
    quoteAmount: 1000000n,
    sourceChainId: UniversalChainId.make("babylon.bbn-test-5"),
    sourceChannelId: ChannelId.make(2),
  }).pipe(
    Effect.provideService(CosmWasmClientSource, { client: sourceClient }),
    Effect.provideService(CosmWasmClientDestination, { client: destClient }),
    Effect.provideService(CosmosChannelDestination, {
      ucs03address: "osmo1336jj8ertl8h7rdvnz4dh5rqahd09cy0x43guhsxx6xyrztx292qs2uecc",
      channelId: 1,
    }),
    Effect.provideService(CosmosChannelSource, {
      ucs03address: "bbn1336jj8ertl8h7rdvnz4dh5rqahd09cy0x43guhsxx6xyrztx292q77945h",
      channelId: 5,
    }),
  )
})

EVM-to-Cosmos Transfer

import { Effect } from "effect"
import { createPublicClient, http, parseEther, toHex } from "viem"
import { holesky } from "viem/chains"
import {
  createEvmToCosmosFungibleAssetOrder,
} from "@unionlabs/sdk/ucs03/fungible-asset-order"
import { ViemPublicClientSource } from "@unionlabs/sdk/evm/client"
import { CosmWasmClientDestination, createCosmWasmClient } from "@unionlabs/sdk/cosmos/client"
import { CosmosChannelDestination } from "@unionlabs/sdk/cosmos/channel"
import { AddressEvmZkgm, AddressCosmosZkgm } from "@unionlabs/sdk/schema/address"
import { TokenRawDenom, UniversalChainId, ChannelId } from "@unionlabs/sdk/schema"

const program = Effect.gen(function*() {
  const cosmosClient = yield* createCosmWasmClient(
    "https://rpc.bbn-test-5.babylon.chain.kitchen",
  )

  return yield* createEvmToCosmosFungibleAssetOrder({
    sender: AddressEvmZkgm.make("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"),
    receiver: AddressCosmosZkgm.make(toHex("bbn122ny3mep2l7nhtafpwav2y9e5jrslhekrn8frh")),
    baseToken: TokenRawDenom.make("0xb476983cc7853797fc5adc4bcad39b277bc79656"),
    baseAmount: parseEther("100"),
    quoteAmount: 1000000n,
    sourceChainId: UniversalChainId.make("ethereum.17000"),
    sourceChannelId: ChannelId.make(2),
  }).pipe(
    Effect.provideService(ViemPublicClientSource, {
      client: createPublicClient({ chain: holesky, transport: http() }),
    }),
    Effect.provideService(CosmWasmClientDestination, { client: cosmosClient }),
    Effect.provideService(CosmosChannelDestination, {
      ucs03address: "bbn1336jj8ertl8h7rdvnz4dh5rqahd09cy0x43guhsxx6xyrztx292q77945h",
      channelId: 3,
    }),
  )
})

Sui Transfer

import { getFullnodeUrl } from "@mysten/sui/client"
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"
import { Sui, SuiZkgmClient } from "@unionlabs/sdk-sui"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { UniversalChainId } from "@unionlabs/sdk/schema/chain"
import { ChannelId } from "@unionlabs/sdk/schema/channel"
import * as TokenOrder from "@unionlabs/sdk/TokenOrder"
import * as ZkgmClient from "@unionlabs/sdk/ZkgmClient"
import * as ZkgmClientRequest from "@unionlabs/sdk/ZkgmClientRequest"
import * as ZkgmClientResponse from "@unionlabs/sdk/ZkgmClientResponse"
import { Effect, Logger } from "effect"
import * as Layer from "effect/Layer"

const keypair = Ed25519Keypair.deriveKeypair(process.env.SUI_MNEMONIC ?? "...")

const program = Effect.gen(function*() {
  const source = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.17000"),
  )
  const destination = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("union.union-1"),
  )

  const wallet = yield* Sui.WalletClient
  const sender = wallet.signer.toSuiAddress()

  const tokenOrder = yield* TokenOrder.make({
    source,
    destination,
    sender,
    receiver: "union1wycy8g8v5sff6gsjl9yhjs43q98xpl05p3gn2s",
    baseToken: "0x2::sui::SUI",
    baseAmount: 10000000n,
    quoteToken: "union1y05e0p2jcvhjzf7kcqsrqx93d4g3u93hc2hykaq8hrvkqrp5ltrssagzyd",
    quoteAmount: 10000000n,
    kind: "solve",
    metadata: "0x...",
    version: 2,
  })

  const request = ZkgmClientRequest.make({
    source,
    destination,
    channelId: ChannelId.make(5),
    ucs03Address: "0x3078...",
    instruction: tokenOrder,
    transport: {
      sui: {
        vaultId: "0xc3ac8618f622fc70ea30eaec5b45d504e239af668033d07e396be44d45f8f45d",
        ibcStoreId: "0xdc5f20df5f143a06772c073e9c30dacd30e31f6788885cf478d0fd40f92766c4",
        coins: [{
          typeArg: "0x2::sui::SUI",
          objectId: "0x266d00c4b329111255339c041cc57a1b616cfeddafdae47df8f814002578e95b",
          baseAmount: BigInt(3),
        }],
      },
    },
  })

  const zkgmClient = yield* ZkgmClient.ZkgmClient
  const response: ZkgmClientResponse.ZkgmClientResponse =
    yield* zkgmClient.execute(request)
  yield* Effect.log("Submission Hash", response.txHash)
}).pipe(
  Effect.provide(
    Layer.mergeAll(
      SuiZkgmClient.layerWithoutWallet,
      ChainRegistry.Default,
      Logger.replace(Logger.defaultLogger, Logger.prettyLoggerDefault),
    ).pipe(
      Layer.provideMerge(Sui.PublicClient.Live({
        url: getFullnodeUrl("testnet"),
      })),
      Layer.provideMerge(Sui.WalletClient.Live({
        url: getFullnodeUrl("testnet"),
        signer: keypair,
      })),
    ),
  ),
)

Effect.runPromise(program).catch(console.error)

Cosmos Contract Call (UCS05)

Execute a cross-chain contract call from Babylon to an Ethereum contract:

import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"
import { Call, Ucs05, ZkgmClientRequest, ZkgmClientResponse } from "@unionlabs/sdk"
import { Cosmos, CosmosZkgmClient } from "@unionlabs/sdk-cosmos"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { UniversalChainId } from "@unionlabs/sdk/schema/chain"
import { ChannelId } from "@unionlabs/sdk/schema/channel"
import { Effect, Logger } from "effect"

const signer = await DirectSecp256k1HdWallet.fromMnemonic(
  process.env.MEMO ?? "your mnemonic",
  { prefix: "bbn" },
)

const program = Effect.gen(function*() {
  const source = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("babylon.bbn-1"),
  )
  const destination = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.1"),
  )

  const call = Call.make({
    sender: Ucs05.CosmosDisplay.make({
      address: "bbn122ny3mep2l7nhtafpwav2y9e5jrslhekrn8frh",
    }),
    eureka: false,
    contractAddress: Ucs05.EvmDisplay.make({
      address: "0x921e5b5091f431f84f14423ec487783a853bc4b0",
    }),
    contractCalldata: "0xDEADBEEF",
  })

  const request = ZkgmClientRequest.make({
    source,
    destination,
    channelId: ChannelId.make(3),
    ucs03Address:
      "bbn1336jj8ertl8h7rdvnz4dh5rqahd09cy0x43guhsxx6xyrztx292q77945h",
    instruction: call,
  })

  const client = yield* CosmosZkgmClient.make.pipe(
    Effect.provide(Cosmos.SigningClient.Live(
      "bbn122ny3mep2l7nhtafpwav2y9e5jrslhekrn8frh",
      "https://rpc.bbn-1.babylon.chain.kitchen",
      signer,
      { gasPrice: GasPrice.fromString("0.0007ubbn") },
    )),
    Effect.provide(Cosmos.Client.Live(
      "https://rpc.bbn-1.babylon.chain.kitchen",
    )),
  )

  const response: ZkgmClientResponse.ZkgmClientResponse =
    yield* client.execute(request)
  yield* Effect.log("TX Hash:", response.txHash)
}).pipe(
  Effect.provide(ChainRegistry.Default),
  Effect.provide(Logger.replace(
    Logger.defaultLogger,
    Logger.prettyLoggerDefault,
  )),
)

Effect.runPromise(program).then(console.log).catch(console.error)

SDK Instruction Types

The SDK supports several instruction types for ZKGM (UCS03):

Instruction Purpose
Instruction.FungibleAssetOrder Transfer fungible tokens cross-chain
Instruction.Batch Bundle multiple instructions into one packet
Instruction.Forward Route a transfer through an intermediate chain
Instruction.Multiplex Call a smart contract on the destination chain
import { Instruction } from "@unionlabs/sdk/ucs03"
import { toHex } from "viem"

// Batch two transfers in a single packet
const batch = new Instruction.Batch({
  operand: [
    new Instruction.FungibleAssetOrder({
      operand: [
        toHex("union1jk9psyhvgkrt2cumz8eytll2244m2nnz4yt2g2"),
        "0xBe68fC2d8249eb60bfCf0e71D5A0d2F2e292c4eD",
        toHex("muno"), 4n, "muno", "muno", 18, 0n,
        "0x74d5b8eacfeb0dadaaf66403f40e304b3ef968b3", 4n,
      ],
    }),
    new Instruction.FungibleAssetOrder({
      operand: [/* second transfer params */],
    }),
  ],
})

// Forward through an intermediate chain
const forward = new Instruction.Forward({
  operand: [
    0n,          // path
    0n,          // timeoutHeight
    1234567890n, // timeoutTimestamp
    new Instruction.FungibleAssetOrder({ operand: [/* ... */] }),
  ],
})

// Multiplex: call a remote smart contract
const multiplex = new Instruction.Multiplex({
  operand: [
    "0xBe68fC2d8249eb60bfCf0e71D5A0d2F2e292c4eD",
    true,
    "0x74d5b8eacfeb0dadaaf66403f40e304b3ef968b3",
    toHex("some smart contract data"),
  ],
})

6. Core Usage -- uniond (Chain Binary)

uniond is the Cosmos-SDK chain binary for the Union blockchain. It handles node operation, validator management, transactions, and queries.

Initializing a Node

uniond init my-node --chain-id union-1

This creates the node home directory at ~/.union with default configuration.

Starting the Node

uniond start

Key Management

# Create a new key
uniond keys add my-wallet

# List all keys
uniond keys list

# Show key address
uniond keys show my-wallet

# Recover a key from mnemonic
uniond keys add my-wallet --recover

Querying the Chain

# Check node status
uniond status

# Query account balance
uniond query bank balances union1...

# Query a specific transaction
uniond query tx <TXHASH>

# Query transactions by events
uniond query txs --events 'message.action=/cosmos.bank.v1beta1.MsgSend'

# Query block at height
uniond query block <HEIGHT>

Submitting Transactions

# Send tokens
uniond tx bank send <from-key> union1<recipient> 1000000au \
  --chain-id union-1 \
  --gas auto \
  --gas-adjustment 1.3

# Sign a transaction
uniond tx sign unsigned_tx.json --from my-wallet

# Broadcast a signed transaction
uniond tx broadcast signed_tx.json

# Simulate a transaction (dry run)
uniond tx simulate signed_tx.json

Validator Operations

# Create a Union validator
uniond create-union-validator \
  --moniker "My Validator" \
  --identity "<keybase-id>" \
  --commission-rate 0.10 \
  --commission-max-rate 0.20 \
  --commission-max-change-rate 0.01 \
  --min-self-delegation 1

# Prove BLS key possession
uniond prove-possession <bls-key>

Genesis Commands

# Validate genesis file
uniond genesis validate-genesis

# Add genesis account
uniond genesis add-genesis-account union1... 1000000000au

# Generate gentx
uniond genesis gentx my-wallet 1000000000au --chain-id union-1

Node Configuration and Maintenance

# Update app configuration
uniond config set <key> <value>

# Manage pruning
uniond pruning

# Create/restore snapshots
uniond snapshot export
uniond snapshot restore <snapshot-file>

7. Core Usage -- Voyager (IBC Relayer)

Voyager is Union's trust-minimized IBC relayer with a plugin architecture supporting ZK-proof-based consensus verification across heterogeneous chains.

Starting the Relayer

voyager -c config.json start

Configuration Management

# Print current config
voyager -c config.json config print

# Generate default config
voyager config default

# Print config JSON Schema
voyager config schema

Global Options

Flag Short Description
--config-file-path -c Path to config file (JSON or JSONC)
--log-format -l Log format: text (default) or json
--stack-size -- Thread stack size (default: 2MB)

Indexing Events

Start indexing blockchain events for relay processing:

# Index from latest block (infinite)
voyager -c config.json index <chain-id>

# Index from a specific height
voyager -c config.json index <chain-id> --from 1000000

# Index a range of blocks
voyager -c config.json index <chain-id> --from 1000000 --to 1001000

# Index a single block
voyager -c config.json index <chain-id> --exact 1000500

# Index and automatically enqueue
voyager -c config.json index <chain-id> --from 1000000 --enqueue

Queue Management

# View queue statistics
voyager -c config.json queue stats

# Enqueue a custom operation
voyager -c config.json queue enqueue '<JSON operation>'

# Query failed messages
voyager -c config.json queue query-failed

# Query a specific failed message by ID
voyager -c config.json queue query-failed-by-id <ID>

# Truncate queue tables
voyager -c config.json queue truncate --queue --done --failed

# Vacuum queue database
voyager -c config.json queue vacuum --queue --done --failed

Plugin System

# List loaded plugins
voyager -c config.json plugin list

# Query plugin interest filter
voyager -c config.json plugin interest <plugin-name>

# Get plugin info
voyager -c config.json plugin info <plugin-name>

# Call a plugin directly
voyager -c config.json plugin call <plugin-name> '<JSON args>'

RPC Commands

Query a running Voyager instance via JSON-RPC:

# Get Voyager info
voyager rpc --rpc-url http://localhost:7042 info

# Query client state
voyager rpc client-state <chain-id> <client-id>

# Query consensus state
voyager rpc consensus-state <chain-id> <client-id> <height>

# Get latest height
voyager rpc latest-height <chain-id>

# Get latest timestamp
voyager rpc latest-timestamp <chain-id>

# Query IBC state
voyager rpc ibc-state <chain-id> <height> <path>

# Get IBC proof
voyager rpc ibc-proof <chain-id> <height> <path>

Creating Clients

voyager -c config.json msg create-client <chain-id> <client-type>

Important

Voyager's plugin architecture means you must have the appropriate plugins loaded in your configuration for the chains you want to relay between. Each chain type (EVM, Cosmos, Sui, etc.) requires its own set of client, finality, proof, state, and event-source plugins.

Voyager Plugin Types

Plugin Category Purpose Examples
client-bootstrap Initialize light clients arbitrum, cometbls, ethereum, sui
client-update Update light client states arbitrum, ethereum, tendermint, sui
event-source Watch for IBC events cosmwasm, evm, movement, starknet, sui
finality Track block finality arbitrum, cometbls, ethereum, sui
proof Generate state proofs cosmwasm, evm-mpt, movement, sui
state Query on-chain state cosmwasm, evm, movement, sui
transaction Submit transactions aptos, cosmos, evm, sui
packet-batch Batch packet operations --
packet-filter Filter relevant packets --
packet-index Index packet metadata --
packet-timeout Handle packet timeouts --
zkgm-filter Filter ZKGM-specific events --

8. Core Usage -- unionvisor (Process Supervisor)

unionvisor is a process supervisor that manages uniond binary upgrades automatically. It monitors for on-chain upgrade proposals and swaps the binary at the correct block height.

Initialization

unionvisor --root /path/to/unionvisor init \
  -b /path/to/bundle \
  --moniker "my-node" \
  --seeds "seed1@ip:port,seed2@ip:port"
Flag Description
-b, --bundle Path to the bundle directory
--seeds Comma-separated seed node addresses
--moniker Node display name
--allow-dirty Allow initialization in a non-empty directory

Running the Supervisor

unionvisor --root /path/to/unionvisor run \
  -b /path/to/bundle \
  -p 5000
Flag Description
-b, --bundle Path to bundle directory
-p, --poll-interval Upgrade poll interval in milliseconds

Setting a Specific Version

unionvisor --root /path/to/unionvisor set-uniond-version \
  -b /path/to/bundle

Calling uniond Through unionvisor

unionvisor --root /path/to/unionvisor call \
  -b /path/to/bundle \
  -- status

This passes all arguments after -- directly to the managed uniond binary.

Bundle Structure

The bundle contains versioned binaries and genesis configuration:

bundle/
├── genesis.json          # Chain genesis file
├── meta.json             # Bundle metadata
└── versions/
    ├── genesis/          # Fallback / initial version
    │   └── uniond
    ├── v1.1.0/
    │   └── uniond
    └── v1.2.0/
        └── uniond

The meta.json format:

{
  "binary_name": "uniond",
  "fallback_version": "genesis",
  "versions_directory": "versions"
}

9. Core Usage -- u CLI Tool

The u CLI is Union's Swiss Army knife for developers. It provides codec operations, ZKGM utilities, deployment info, vanity address generation, and chain-specific tooling.

Command Overview

u [OPTIONS] <COMMAND>

Commands:
  arbitrum (arb)        Arbitrum-specific tools (v1/v2)
  parlia                BSC/Parlia chain tools
  codec (c)             Codec operations (light-client decoding)
  cometbft (comet, cmt) CometBFT RPC tools
  zkgm (z, ucs03)       ZKGM/UCS03 utilities
  deployments (d)       Print deployment configurations
  path                  Path utilities
  packet                Packet inspection
  vanity (v)            Vanity address generation
  hex (h)               Hex encode/decode

Global Options

Flag Short Description
--log-format -l Log format: text (default) or json

Hex Encoding/Decoding

# Encode to hex
echo "hello" | u hex

# Decode from hex
u hex --decode 0x68656c6c6f

# Encode without 0x prefix
u hex --no-prefix "hello"

ZKGM Utilities

# Predict a wrapped token address
u zkgm predict-wrapped-token <args>

# Encode/decode ZKGM instructions
u zkgm make encode <instruction-json>
u zkgm make decode <hex-data>

Deployment Info

# Print all deployments
u deployments print

# Print deployment for a specific chain
u deployments print ethereum.1

Codec Operations

# Decode light client data
u codec light-client <subcommand>

CometBFT RPC

# Query CometBFT RPC
u cometbft rpc <subcommand>

Vanity Address Generation

# Generate vanity wallet addresses
u vanity wallet <pattern>

# Generate vanity ICS23 keys
u vanity ics23 <pattern>

# Generate vanity CREATE3 addresses
u vanity create3 <pattern>

10. Core Usage -- drip (Faucet)

The drip faucet distributes testnet tokens via a GraphQL API.

Running the Faucet

drip -c config.json
Flag Short Default Description
--config -c required Path to config file
--batch-size -b 6000 Number of requests to process per batch
--max-per-day -m 50 Maximum requests per address per day

The faucet starts an HTTP server on 0.0.0.0:8000 with:

  • GET / -- GraphiQL interactive explorer
  • POST / -- GraphQL endpoint (queries and mutations)

Requesting Tokens via GraphQL

mutation {
  send(
    chainId: "union-testnet-10"
    denom: "au"
    toAddress: "union1..."
  ) {
    id
    txHash
  }
}

The faucet supports rate limiting per address (configurable via ratelimit_seconds in config) and optional CAPTCHA verification.

Checking Request Status

query {
  request(id: 42) {
    id
    chainId
    denom
    address
    time
    txHash
  }
}

Warning

The faucet uses a SQLite database (db.sqlite3) in the working directory. Ensure the process has write permissions to the current directory.


11. Core Usage -- galoisd (ZK Proving)

galoisd is the zero-knowledge proving service that generates BLS12-381 proofs for Union's consensus verification.

Commands

Command Description
serve Start the gRPC proving server
serve-bls12381 Start BLS12-381 specific server
example-prove Generate an example proof
example-verify Verify an example proof
export-vk Export the verification key
gen-contract Generate a Solidity verifier contract
query-stats Query proof generation statistics
query-stats-health Health check for statistics service

MPC Ceremony Commands

Command Description
mpc-phase1-init Initialize MPC Phase 1
mpc-phase2-init Initialize MPC Phase 2
mpc-phase2-contrib Submit Phase 2 contribution
mpc-phase2-extract Extract Phase 2 parameters
mpc-phase2-verify Verify Phase 2 contribution

Starting the Proving Server

galoisd serve --listen 0.0.0.0:9090

The server exposes a /health endpoint for monitoring.


12. Core Usage -- Ceremony (MPC)

The ceremony application is a SvelteKit web UI for participating in Union's Multi-Party Computation (MPC) trusted setup ceremony. This ceremony generates the cryptographic parameters used by the ZK proving system.

Components

  • ceremony -- SvelteKit web app for ceremony participation
  • mpc/client -- Rust CLI client for contributing to the ceremony
  • mpc/coordinator -- Rust coordination server
  • mpc/edge -- Supabase edge functions for ceremony API

Participating

  1. Visit the ceremony web app
  2. Connect your wallet (for identity verification)
  3. Download and run the MPC client binary
  4. Follow the on-screen instructions to generate and submit your contribution
  5. View your contribution hash in the contributions list

13. Supported Chains and Channels

Mainnet Chains and Channels

Chain Chain ID Channels
Union union.union-1 1 (Ethereum), 2 (Osmosis), 3 (Base), 4 (Arbitrum), 7 (BSC), 8 (Corn), 9 (Sei), 10 (Berachain), 11 (Bob), 12 (Xion), 13 (Babylon)
Ethereum ethereum.1 1 (Babylon), 2 (Union), 3 (Osmosis), 4 (Sei), 5 (Base), 6 (Berachain), 7 (Arbitrum), 8 (BSC), 9 (Corn), 10 (Bob)
Arbitrum arbitrum.42161 1 (Union), 2 (Ethereum)
Base base.8453 1 (Union), 2 (Sei), 3 (Ethereum)
BSC bsc.56 1 (Union), 2 (Ethereum)
Berachain berachain.80094 1 (Union), 2 (Ethereum)
Bob bob.60808 1 (Union), 2 (Ethereum)
Corn corn.21000000 1 (Union), 2 (Ethereum)
Osmosis osmosis.osmosis-1 1 (Union), 2 (Ethereum)
Sei sei.pacific-1 1 (Union), 2 (Ethereum), 3 (Base)
Babylon babylon.bbn-1 3 (Ethereum)
Xion xion.xion-mainnet-1 1 (Union)

Testnet Chains

Chain Chain ID
Union Testnet union.union-testnet-10
Ethereum Sepolia ethereum.11155111
Ethereum Holesky ethereum.17000
Arbitrum Sepolia arbitrum.421614
Bob Testnet bob.808813
Corn Testnet corn.21000001
Movement Bardock movement.bardock-testnet

Channel Tags

Channels are tagged with metadata:

  • canonical -- official, production-grade channel
  • tokenorder-v2 -- supports TokenOrder v2 instruction format

SLA Expectations

Each channel direction has an SLA (Service Level Agreement) for transfer completion:

Direction Typical SLA
Cosmos-to-EVM ~20 minutes
EVM-to-Cosmos ~3 hours (depends on finality)
EVM-to-EVM (via L1) ~45 minutes

14. Common Tasks and Workflow Recipes

Recipe 1: Bridge ETH from Ethereum to Union

Use the web UI at app.union.build:

  1. Connect MetaMask (Ethereum Mainnet)
  2. Connect Keplr (Union)
  3. Select Ethereum as source, Union as destination
  4. Select ETH, enter amount
  5. Approve and sign the transaction

Recipe 2: Bridge Tokens Programmatically (EVM-to-EVM)

import { Evm, EvmZkgmClient } from "@unionlabs/sdk-evm"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { UniversalChainId } from "@unionlabs/sdk/schema/chain"
import { ChannelId } from "@unionlabs/sdk/schema/channel"
import * as TokenOrder from "@unionlabs/sdk/TokenOrder"
import * as ZkgmClient from "@unionlabs/sdk/ZkgmClient"
import * as ZkgmClientRequest from "@unionlabs/sdk/ZkgmClientRequest"
import { Effect } from "effect"
import { http, parseEther } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { holesky } from "viem/chains"

const transfer = Effect.gen(function*() {
  const source = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.17000"),
  )
  const destination = yield* ChainRegistry.byUniversalId(
    UniversalChainId.make("ethereum.11155111"),
  )
  const tokenOrder = yield* TokenOrder.make({
    source, destination,
    sender: "0xYourAddress",
    receiver: "0xRecipientAddress",
    baseToken: "0xTokenOnHolesky",
    baseAmount: parseEther("1"),
    quoteToken: "0xTokenOnSepolia",
    quoteAmount: parseEther("1"),
    kind: "escrow",
    metadata: undefined,
    version: 2,
  })
  const request = ZkgmClientRequest.make({
    source, destination,
    channelId: ChannelId.make(2),
    ucs03Address: "0x5fbe74a283f7954f10aa04c2edf55578811aeb03",
    kind: "simulateAndExecute",
    instruction: tokenOrder,
  })
  const zkgmClient = yield* ZkgmClient.ZkgmClient
  return yield* zkgmClient.execute(request)
}).pipe(
  Effect.provide(EvmZkgmClient.layerWithoutWallet),
  Effect.provide(Evm.WalletClient.Live({
    account: privateKeyToAccount("0xYourPrivateKey" as any),
    chain: holesky,
    transport: http(),
  })),
  Effect.provide(Evm.PublicClient.Live({ chain: holesky, transport: http() })),
  Effect.provide(ChainRegistry.Default),
)

Recipe 3: Read Token Metadata on Aptos/Movement

import { AptosConfig, Network } from "@aptos-labs/ts-sdk"
import { Effect } from "effect"
import { AptosPublicClient, createAptosPublicClient } from "@unionlabs/sdk/aptos/client"
import { readFaDecimals, readFaName, readFaSymbol } from "@unionlabs/sdk/aptos/fa"

Effect.runPromiseExit(
  Effect.gen(function*() {
    const config = new AptosConfig({
      fullnode: "https://aptos.testnet.bardock.movementlabs.xyz/v1",
      network: Network.CUSTOM,
    })
    const publicClient = yield* createAptosPublicClient(config)
    const tokenAddress = "0x19842c145c835df3bb4daa4fb3914bb9bdbd4ff4bcf53b53a014eb54d39e875e"

    const name = yield* readFaName(tokenAddress).pipe(
      Effect.provideService(AptosPublicClient, { client: publicClient }),
    )
    const decimals = yield* readFaDecimals(tokenAddress).pipe(
      Effect.provideService(AptosPublicClient, { client: publicClient }),
    )
    const symbol = yield* readFaSymbol(tokenAddress).pipe(
      Effect.provideService(AptosPublicClient, { client: publicClient }),
    )
    yield* Effect.log("Token:", name, symbol, decimals)
  }),
)

Recipe 4: Read CW20 Token Balance on Cosmos

import { Effect } from "effect"
import { CosmWasmClientContext, createCosmWasmClient } from "@unionlabs/sdk/cosmos/client"
import { readCw20Balance, readCw20TokenInfo } from "@unionlabs/sdk/cosmos/cw20"

Effect.runPromiseExit(
  Effect.gen(function*() {
    const client = yield* createCosmWasmClient(
      "https://rpc.elgafar-1.stargaze-apis.com",
    )
    const tokenAddr = "stars1qrde534d4jwk44dn7w7gu9e2rayutr7kqx8lfjhsk3rd7z9rzxhq2gh3lr"
    const walletAddr = "stars1qcvavxpxw3t8d9j7mwaeq9wgytkf5vwputv5x4"

    const info = yield* readCw20TokenInfo(tokenAddr).pipe(
      Effect.provideService(CosmWasmClientContext, { client }),
    )
    const balance = yield* readCw20Balance(tokenAddr, walletAddr).pipe(
      Effect.provideService(CosmWasmClientContext, { client }),
    )
    yield* Effect.log(`${info.name}: ${balance} ${info.symbol}`)
  }),
)

Recipe 5: Execute a CosmWasm Contract

import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { Effect } from "effect"
import { createSigningCosmWasmClient } from "@unionlabs/sdk/cosmos/client"
import { executeContract } from "@unionlabs/sdk/cosmos/contract"

Effect.runPromiseExit(
  Effect.gen(function*() {
    const wallet = yield* Effect.tryPromise(() =>
      DirectSecp256k1HdWallet.fromMnemonic("your mnemonic", { prefix: "stars" }),
    )
    const [account] = yield* Effect.tryPromise(() => wallet.getAccounts())
    const client = yield* createSigningCosmWasmClient(
      "https://rpc.elgafar-1.stargaze-apis.com",
      wallet,
    )
    return yield* executeContract(
      client,
      account.address,
      "stars1contract...",
      { transfer: { recipient: "stars1recipient...", amount: "1000000" } },
    )
  }),
)

Recipe 6: Set Up a Union Validator

# 1. Initialize the node
uniond init my-validator --chain-id union-1

# 2. Replace genesis.json with the official one
# Download from the Union network repository

# 3. Set persistent peers and seeds in config.toml
# Edit ~/.union/config/config.toml

# 4. Start the node and wait for sync
uniond start

# 5. Create wallet
uniond keys add validator-wallet

# 6. Fund wallet with UNO tokens

# 7. Create validator transaction
uniond create-union-validator \
  --moniker "My Validator" \
  --commission-rate 0.10 \
  --commission-max-rate 0.20 \
  --commission-max-change-rate 0.01 \
  --min-self-delegation 1 \
  --from validator-wallet \
  --chain-id union-1

Recipe 7: Set Up unionvisor for Automatic Upgrades

# 1. Prepare the bundle directory
mkdir -p /opt/union/bundle/versions/genesis
cp uniond /opt/union/bundle/versions/genesis/

# 2. Create meta.json
cat > /opt/union/bundle/meta.json << 'EOF'
{
  "binary_name": "uniond",
  "fallback_version": "genesis",
  "versions_directory": "versions"
}
EOF

# 3. Copy genesis.json into bundle
cp genesis.json /opt/union/bundle/

# 4. Initialize unionvisor
unionvisor --root /opt/union/unionvisor init \
  -b /opt/union/bundle \
  --moniker "my-node" \
  --seeds "seed1@ip:port"

# 5. Run the supervisor
unionvisor --root /opt/union/unionvisor run \
  -b /opt/union/bundle \
  -p 5000

Recipe 8: Run a Voyager Relay Node

# 1. Generate a default config
voyager config default > voyager-config.json

# 2. Edit the config to add:
#    - Chain endpoints (RPC, gRPC, WebSocket)
#    - Signing keys for transaction submission
#    - Plugin configuration for your target chains

# 3. Start indexing events for the chains you want to relay
voyager -c voyager-config.json index union.union-1 --from latest --enqueue
voyager -c voyager-config.json index ethereum.1 --from latest --enqueue

# 4. Start the relayer
voyager -c voyager-config.json start

Recipe 9: Monitor Relay Queue Health

# Check queue statistics
voyager -c config.json queue stats

# Check for failed messages
voyager -c config.json queue query-failed

# Investigate a specific failure
voyager -c config.json queue query-failed-by-id 42

# Clean up completed operations
voyager -c config.json queue truncate --done
voyager -c config.json queue vacuum --done

Recipe 10: Predict a Wrapped Token Address

Use the u CLI to predict the address of a wrapped token on the destination chain:

u zkgm predict-wrapped-token <base-token> <channel-id>

Or use the TypeScript SDK:

import { predictQuoteToken } from "@unionlabs/sdk/aptos/quote-token"

Recipe 11: Batch Multiple Transfers in One Packet

import { Instruction } from "@unionlabs/sdk/ucs03"
import { toHex } from "viem"

const batchInstruction = new Instruction.Batch({
  operand: [
    new Instruction.FungibleAssetOrder({
      operand: [/* transfer 1 params */],
    }),
    new Instruction.FungibleAssetOrder({
      operand: [/* transfer 2 params */],
    }),
  ],
})

Recipe 12: Forward a Transfer Through an Intermediate Chain

const forwardInstruction = new Instruction.Forward({
  operand: [
    0n,          // path (intermediate chain channel)
    0n,          // timeoutHeight
    1234567890n, // timeoutTimestamp
    new Instruction.FungibleAssetOrder({
      operand: [/* inner transfer params */],
    }),
  ],
})

Recipe 13: Call a Remote Smart Contract (Multiplex)

import { toHex } from "viem"

const multiplexInstruction = new Instruction.Multiplex({
  operand: [
    "0xTargetContractAddress",
    true,  // eureka flag
    "0xFallbackAddress",
    toHex("calldata for the smart contract"),
  ],
})

Recipe 14: Query Voyager RPC for Chain State

# Get the latest height of a chain
voyager rpc --rpc-url http://localhost:7042 latest-height union.union-1

# Query a client's state
voyager rpc client-state union.union-1 07-tendermint-0

# Get IBC proof for verification
voyager rpc ibc-proof ethereum.1 18000000 "connections/connection-0"

Recipe 15: Export Verification Key for On-Chain Verifier

# Export the ZK verification key
galoisd export-vk --output vk.json

# Generate a Solidity verifier contract from the VK
galoisd gen-contract --vk vk.json --output Verifier.sol

Recipe 16: Check Sui Coin Metadata

import { getFullnodeUrl } from "@mysten/sui/client"
import { Sui } from "@unionlabs/sdk-sui"
import { Effect, Logger } from "effect"

const program = Effect.gen(function*() {
  const meta = yield* Sui.readCoinMetadata("0x2::sui::SUI")
  yield* Effect.log("SUI metadata:", meta)

  const [name, symbol, decimals] = yield* Effect.all([
    Sui.getCoinName("0x2::sui::SUI"),
    Sui.readCoinSymbol("0x2::sui::SUI"),
    Sui.getCoinDecimals("0x2::sui::SUI"),
  ])
  yield* Effect.log("Granular:", { name, symbol, decimals })
}).pipe(
  Effect.provide(Sui.PublicClient.Live({
    url: getFullnodeUrl("mainnet"),
  })),
  Effect.provide(Logger.replace(
    Logger.defaultLogger,
    Logger.prettyLoggerDefault,
  )),
)

Recipe 17: Read Sui Coin Balances

import { Sui } from "@unionlabs/sdk-sui"
import { Effect } from "effect"

const balances = Effect.gen(function*() {
  const balance = yield* Sui.getCoinBalance(
    "0x2::sui::SUI",
    "0xYourSuiAddress",
  )
  yield* Effect.log("Balance:", balance)
})

Recipe 18: Use Token Order v2 with Solve Intent

The kind: "solve" option enables market-maker settlement:

const tokenOrder = yield* TokenOrder.make({
  source,
  destination,
  sender: "union1...",
  receiver: "0xRecipient",
  baseToken: "au",
  baseAmount: 10n,
  quoteToken: "0xWrappedTokenAddress",
  quoteAmount: 10n,
  kind: "solve",       // Market-maker fills the order
  metadata: "0x...",   // Solver-specific metadata
  version: 2,
})

Recipe 19: Use Token Order v2 with Escrow

import { parseEther } from "viem"

const tokenOrder = yield* TokenOrder.make({
  source,
  destination,
  sender: "0xSender",
  receiver: "0xReceiver",
  baseToken: "0xTokenAddress",
  baseAmount: parseEther("100"),
  quoteToken: "0xQuoteTokenOnDest",
  quoteAmount: parseEther("0.05"),
  kind: "escrow",       // Standard lock-and-mint
  metadata: undefined,
  version: 2,
})

Recipe 20: Decode Hex Data with u CLI

# Decode hex-encoded data
u hex --decode 0x68656c6c6f

# Encode string to hex
echo "hello world" | u hex

# Pipe hex data for processing
u hex --decode 0x... | u zkgm make decode

Recipe 21: Run a Multi-Node Testnet

# Start a multi-node testnet
uniond multi-node testnet \
  --v 4 \
  --output-dir ./testnet \
  --chain-id union-testnet-local

Recipe 22: Manage Node Keys

# List keys
uniond keys list

# Export a key (encrypted)
uniond keys export my-key

# Import a key
uniond keys import my-key keyfile.json

# Delete a key
uniond keys delete my-key

Recipe 23: Debug Node Issues

# Check node status
uniond status

# Debug address conversion
uniond debug addr union1...

# Debug raw bytes
uniond debug raw-bytes <hex>

# Debug pubkey
uniond debug pubkey <pubkey>

Recipe 24: Print Deployment Configuration

# Print all deployments
u deployments print

# Print deployment for a specific chain
u deployments print union.union-1

# Print for Ethereum mainnet
u deployments print ethereum.1

Recipe 25: Generate Vanity Addresses

# Generate a union address starting with "union1abc"
u vanity wallet union1abc

# Generate a CREATE3 address with a prefix
u vanity create3 0xDEAD

15. Advanced Usage

Custom Effect-TS Layers

The SDK's Effect-TS architecture enables sophisticated composition. Create custom layers that combine multiple chain clients:

import { Effect, Layer } from "effect"
import { Evm, EvmZkgmClient } from "@unionlabs/sdk-evm"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"

// Compose a layer with both EVM and registry capabilities
const multiChainLayer = Layer.mergeAll(
  EvmZkgmClient.layerWithoutWallet,
  ChainRegistry.Default,
)

// Use ManagedRuntime for reusable program execution
import { ManagedRuntime, Logger, LogLevel } from "effect"

const { runPromiseExit } = ManagedRuntime.make(
  Logger.minimumLogLevel(LogLevel.Error),
)

runPromiseExit(yourProgram).then(exit => {
  console.log(JSON.stringify(exit, null, 2))
})

Error Handling with Effect-TS

const safeTransfer = createEvmToEvmFungibleAssetOrder({
  // ... transfer params
}).pipe(
  Effect.tapErrorCause(cause =>
    Effect.logError("Transfer failed with cause", cause),
  ),
  Effect.retry({ times: 3 }),
  Effect.timeout("30 seconds"),
)

Tip

When developing custom integrations, use Effect.tapErrorCause to log full error details including the cause chain. This is invaluable for debugging cross-chain transfer failures.

Voyager Plugin Development

Voyager plugins implement a standardized interface. Each plugin is a standalone Rust binary that communicates with the Voyager core via JSON-RPC:

  • Interest filter -- declares which message types the plugin handles
  • Call handler -- processes incoming plugin calls
  • Info endpoint -- reports plugin metadata

See the Admin Guide for the complete plugin development reference.

UCS03 (ZKGM) Protocol Details

The ZKGM protocol (UCS03) is Union's cross-chain messaging standard. It supports:

Instruction Opcode Name Purpose
0x03 FungibleAssetOrder Token transfers
0x01 Forward Multi-hop routing
0x02 Multiplex Smart contract calls
0x00 Batch Multiple instructions in one packet

Each instruction is ABI-encoded and wrapped in a ZKGM packet that includes sender, receiver, token info, and the instruction payload.

Important

When constructing cross-chain transfers, always use UniversalChainId.make() with the format <ecosystem>.<chain-id> (e.g., "ethereum.1", "union.union-1", "osmosis.osmosis-1"). This format is standardized across the Union SDK.

Chain ID Format

Union uses a Universal Chain ID scheme across all its tooling:

Format Example Ecosystem
ethereum.<number> ethereum.1, ethereum.11155111 EVM (Ethereum, L2s)
arbitrum.<number> arbitrum.42161 Arbitrum
base.<number> base.8453 Base
bsc.<number> bsc.56 BNB Smart Chain
union.<name> union.union-1 Union
osmosis.<name> osmosis.osmosis-1 Osmosis
babylon.<name> babylon.bbn-1 Babylon
sei.<name> sei.pacific-1 Sei
berachain.<number> berachain.80094 Berachain
bob.<number> bob.60808 Bob
corn.<number> corn.21000000 Corn
xion.<name> xion.xion-mainnet-1 Xion

16. Troubleshooting

Issue 1: SDK -- "Effect is not defined" or Module Import Errors

Error: Cannot find module 'effect' or Effect is not defined

Cause: The effect peer dependency is not installed.

Solution:

npm install effect

Prevention: Always install effect alongside @unionlabs/sdk.


Issue 2: SDK -- BigInt Serialization Error

Error: TypeError: Do not know how to serialize a BigInt

Cause: JSON.stringify does not natively handle BigInt values.

Solution: Add BigInt serialization at the top of your script:

if (typeof BigInt.prototype.toJSON !== "function") {
  BigInt.prototype.toJSON = function() {
    return this.toString()
  }
}

Prevention: Include this polyfill in your application entry point.


Issue 3: SDK -- Service Layer Not Provided

Error: MissingService: ViemPublicClientSource (or similar)

Cause: An Effect-TS service dependency was not provided to the program.

Solution: Ensure all required service layers are provided via Effect.provide() or Effect.provideService(). Each transfer type requires specific layers:

  • EVM: Evm.PublicClient, Evm.WalletClient, ChainRegistry.Default
  • Cosmos: Cosmos.Client, Cosmos.SigningClient, ChainRegistry.Default
  • Sui: Sui.PublicClient, Sui.WalletClient, ChainRegistry.Default

Prevention: Follow the examples in this guide, ensuring every pipe() chain includes all necessary Effect.provide() calls.


Issue 4: Voyager -- "config file must be specified"

Error: Error: config file must be specified

Cause: The -c flag was omitted when running Voyager commands that require a config.

Solution:

voyager -c /path/to/config.json start

Prevention: Always pass -c <config-path> to Voyager commands (except config default and config schema).


Issue 5: Voyager -- Plugin Not Picking Up Events

Error: Voyager starts but no events are processed.

Cause: The event-source plugin's interest filter does not match the chain's events, or the index operation was not enqueued.

Solution:

  1. Verify plugins are loaded: voyager -c config.json plugin list
  2. Check interest filters: voyager -c config.json plugin interest <plugin-name>
  3. Enqueue an index operation: voyager -c config.json index <chain-id> --from latest -e

Prevention: Ensure your config includes event-source plugins for every chain you want to relay and that you have started indexing.


Issue 6: unionvisor -- "cannot read bundle/meta.json"

Error: cannot read bundle/meta.json

Cause: The unionvisor bundle directory is missing meta.json or the path is incorrect.

Solution: Verify the bundle structure contains meta.json:

ls /path/to/bundle/
# Should show: genesis.json  meta.json  versions/

Prevention: Always create the bundle with the correct structure before running unionvisor.


Issue 7: unionvisor -- "version was not found in bundle"

Error: version was not found in bundle: /path/to/versions/vX.Y.Z/uniond

Cause: The upgrade version binary is not present in the bundle's versions directory.

Solution:

mkdir -p /path/to/bundle/versions/vX.Y.Z
cp new-uniond-binary /path/to/bundle/versions/vX.Y.Z/uniond
chmod +x /path/to/bundle/versions/vX.Y.Z/uniond

Prevention: Pre-download upgrade binaries before the upgrade height is reached.


Issue 8: unionvisor -- "permission denied when executing version"

Error: permission denied when executing version in bundle

Cause: The uniond binary in the bundle lacks execute permissions.

Solution:

chmod +x /path/to/bundle/versions/*/uniond

Prevention: Always set execute permissions when adding binaries to the bundle.


Issue 9: drip -- "can't read config file"

Error: can't read config file or invalid config file

Cause: The config file path is wrong or the JSON is malformed.

Solution: Validate the config file:

python3 -c "import json; json.load(open('config.json'))"

Prevention: Use a JSON linter before starting drip.


Issue 10: uniond -- Node Sync Stuck

Error: Node stops syncing or falls behind peers.

Cause: Insufficient peers, network issues, or inadequate hardware resources.

Solution:

  1. Check peer count: uniond status | jq '.NodeInfo.n_peers'
  2. Add more persistent peers to config.toml
  3. Check disk space and I/O performance
  4. Consider using state sync for faster catch-up

Prevention: Maintain a minimum of 10 persistent peers and monitor disk usage.


Issue 11: SDK -- Transfer Times Out

Error: Transfer submitted but never completes on the destination chain.

Cause: The relayer (Voyager) may be down, the packet may be stuck in the queue, or the light client needs updating.

Solution:

  1. Check the transfer status in the Explorer at app.union.build
  2. If using a private relayer, check voyager queue stats
  3. Wait for the SLA period (up to 3 hours for EVM-to-Cosmos)

Prevention: Use the public relayer infrastructure for production transfers. Monitor relayer health for private deployments.


Issue 12: uniond -- "invalid chain-id"

Error: Transaction rejected with invalid chain ID.

Cause: The --chain-id flag does not match the connected node's chain ID.

Solution: Verify the node's chain ID:

uniond status | jq '.NodeInfo.network'

Use the returned value in your --chain-id flag.

Prevention: Always query the node status before submitting transactions.


Issue 13: Cosmos SDK -- Gas Estimation Failed

Error: out of gas in location: ... or insufficient fees

Cause: Gas price too low or gas limit too small.

Solution:

uniond tx bank send ... --gas auto --gas-adjustment 1.5 --gas-prices 0.025au

For the SDK:

{ gasPrice: GasPrice.fromString("100000au") }

Prevention: Use --gas auto with a gas adjustment of 1.3-1.5.


Issue 14: Web UI -- Wallet Connection Fails

Error: Wallet does not connect or shows "unsupported chain."

Cause: The wallet does not have the Union chain configured, or the browser extension is outdated.

Solution:

  1. Update your wallet extension to the latest version
  2. For Keplr/Leap: the app should automatically suggest adding the Union chain
  3. Clear browser cache and retry

Prevention: Keep wallet extensions updated.


Issue 15: SDK -- Chain Not Found in Registry

Error: ChainRegistry: chain not found for <chain-id>

Cause: The Universal Chain ID is malformed or the chain is not in the default registry.

Solution: Verify the chain ID format matches the Universal Chain ID scheme:

UniversalChainId.make("ethereum.1")     // correct
UniversalChainId.make("1")              // incorrect
UniversalChainId.make("mainnet")        // incorrect

Prevention: Always use the <ecosystem>.<chain-id> format.


Issue 16: Voyager -- Database Lock Error

Error: database is locked in Voyager queue operations.

Cause: Multiple Voyager instances accessing the same queue database.

Solution: Ensure only one Voyager instance uses a given queue database directory. Stop any duplicate processes.

Prevention: Use unique working directories for each Voyager instance.


Issue 17: Build -- Nix Build Fails

Error: Various build errors when using nix build.

Cause: Nix store corruption, outdated flake inputs, or missing system dependencies.

Solution:

nix flake update
nix build .#<package> --rebuild

Prevention: Pin flake inputs and use nix develop for a clean build environment.


Issue 18: galoisd -- Health Check Fails

Error: /health endpoint returns unhealthy or times out.

Cause: The proving service is overloaded or crashed.

Solution:

galoisd query-stats-health

Restart galoisd if the service is unresponsive.

Prevention: Monitor memory usage (ZK proving is resource-intensive) and set up automatic restarts.


Issue 19: SDK -- Incorrect Quote Token Address

Error: Transfer fails because the quote token does not exist on the destination chain.

Cause: The quoteToken address provided does not match the wrapped representation of the base token on the destination.

Solution: Use predictQuoteToken() or u zkgm predict-wrapped-token to determine the correct wrapped token address before constructing the transfer.

Prevention: Always predict the wrapped token address programmatically rather than hardcoding it.


Issue 20: Voyager -- Queue Growing Without Processing

Error: Queue stats show growing message count but no progress.

Cause: The start command was not issued, or the appropriate plugins are not loaded.

Solution:

  1. Verify Voyager is running with start (not just index)
  2. Check plugin list for completeness
  3. Review logs for plugin initialization errors

Prevention: Ensure all required plugins (event-source, client-update, transaction, proof, state, finality) are configured for each chain pair.


Warning

If you encounter stuck transfers or relayer issues, do not submit the same transfer multiple times. Check the packet status via the Explorer first. Duplicate submissions may lock additional funds.


Issue 21: unionvisor -- Upgrade Binary Mismatch

Error: Node crashes after upgrade with consensus errors.

Cause: The wrong binary version was placed in the bundle.

Solution: Verify the version matches the governance proposal:

uniond version
# Compare with expected upgrade version

Prevention: Download binaries from official release channels and verify checksums.


Issue 22: drip -- Rate Limit Exceeded

Error: Faucet request rejected.

Cause: Address has exceeded the ratelimit_seconds window.

Solution: Wait for the rate limit window to expire, then retry.

Prevention: Use a different testnet address if you need tokens immediately.


Issue 23: SDK -- Viem Client Configuration Mismatch

Error: Chain mismatch or RPC errors during EVM transfers.

Cause: The viem client's chain configuration does not match the target network.

Solution: Ensure the chain parameter matches your RPC endpoint:

import { holesky } from "viem/chains"

Evm.PublicClient.Live({
  chain: holesky,  // Must match the RPC network
  transport: http("https://rpc.17000.ethereum.chain.kitchen"),
})

Prevention: Always pair chain objects with their corresponding RPC URLs.


Issue 24: uniond -- Pruning Causes Query Failures

Error: store has been pruned when querying historical state.

Cause: Aggressive pruning settings removed the requested block data.

Solution: If you need historical queries, use an archive node or adjust pruning settings in app.toml:

pruning = "nothing"  # Keep all historical state (high disk usage)

Prevention: For validator nodes, use pruning = "default". For query nodes, use pruning = "nothing".


Issue 25: Voyager -- TLS/SSL Connection Errors to RPC

Error: TLS handshake failures when connecting to chain RPC endpoints.

Cause: Expired or invalid SSL certificates on the RPC endpoint, or system CA certificates are outdated.

Solution: Verify the RPC endpoint is accessible:

curl -v https://rpc.union-1.union.chain.kitchen/status

Update system CA certificates if needed.

Prevention: Use well-maintained public RPC endpoints and monitor their certificate expiry.


17. FAQ

What chains does Union support? Union currently supports Ethereum (mainnet + testnets), Arbitrum, Base, Berachain, Bob, BSC, Corn, Osmosis, Sei, Babylon, Xion, Union itself, Sui, Starknet, and Movement/Aptos. The full list of mainnet and testnet channels is in the Supported Chains section.

What is ZKGM? ZKGM (Zero-Knowledge General Message passing, also known as UCS03) is Union's cross-chain messaging protocol. It wraps transfer instructions, contract calls, batch operations, and multi-hop forwards into cryptographically verifiable packets that are relayed across chains.

What is the difference between "escrow" and "solve" transfer kinds? escrow is a standard lock-and-mint mechanism where tokens are locked on the source chain and minted on the destination. solve enables market-maker settlement where a solver fills the order on the destination chain and claims the locked tokens on the source.

What wallets work with the web UI? MetaMask and WalletConnect for EVM chains, Keplr and Leap for Cosmos chains, and Sui Wallet for Sui.

How long do cross-chain transfers take? Cosmos-to-EVM transfers typically complete in ~20 minutes. EVM-to-Cosmos transfers depend on Ethereum finality and take up to ~3 hours. EVM-to-EVM transfers through L1 intermediaries take ~45 minutes.

What is the au token? au is the native staking/gas token denomination on the Union chain (similar to uatom on Cosmos Hub or uosmo on Osmosis). The union prefix is used for bech32 addresses.

Do I need to run my own relayer? No. Union operates public Voyager relayer infrastructure. However, you can run your own for increased reliability or to support custom routes.

What is unionvisor? unionvisor is a process supervisor (similar to cosmovisor) that manages uniond binary upgrades. It monitors for on-chain upgrade proposals and automatically swaps the binary at the correct block height.

How do I get testnet tokens? Use the faucet at app.union.build/faucet or interact with the drip service's GraphQL API directly.

What is Effect-TS and why does the SDK use it? Effect-TS is a TypeScript library for building type-safe, composable programs with explicit dependency injection and error tracking. The Union SDK uses it to provide compile-time guarantees about which services (wallets, clients, registries) are available to each operation.

Can I bridge NFTs with Union? Union's IBC infrastructure supports general message passing, which includes NFTs. The ZKGM protocol can encode arbitrary instruction types beyond fungible asset orders.

What is the u CLI tool for? u is a developer utility for Union. It provides hex encoding/decoding, ZKGM instruction encoding, deployment info lookup, vanity address generation, and chain-specific debugging tools for Arbitrum, CometBFT, and Parlia.

How do I check if a transfer succeeded? Use the Explorer at app.union.build/explorer to search by packet hash, transaction hash, or sender address. Programmatically, use the response.waitFor() method in the SDK.

What are Union channels? Channels are bidirectional IBC communication paths between two chains. Each channel has an ID on each side. For example, Union channel 1 connects to Ethereum, and Ethereum channel 2 connects back to Union.

Can I use the SDK in a browser? The core SDK packages (@unionlabs/sdk, @unionlabs/sdk-evm, etc.) are TypeScript/JavaScript and can be used in browser environments. The web UI at app.union.build is itself built on these same packages.

What is the difference between TokenOrder v1 and v2? TokenOrder v2 (indicated by version: 2 in SDK calls and the tokenorder-v2 channel tag) is the current version. It supports enhanced features including solve/escrow modes, metadata fields, and improved token wrapping semantics. All current mainnet channels use v2.

How do I monitor my validator?

uniond status          # Node sync status
uniond query staking validator <operator-addr>   # Validator info

The web UI dashboard at app.union.build also provides validator monitoring.

What RPC endpoints are available? Union provides public RPC endpoints in the format https://rpc.<chain-id>.chain.kitchen. For example:

  • https://rpc.union-1.union.chain.kitchen
  • https://rpc.17000.ethereum.chain.kitchen
  • https://rpc.bbn-1.babylon.chain.kitchen

How does Union's ZK bridge work? Union uses a BLS12-381 based zero-knowledge proof system (powered by galoisd and Gnark). Validators sign blocks with BLS keys, and galoisd generates succinct proofs of consensus that can be verified on any chain. This enables trust-minimized bridging without trusted intermediaries.

What is the MPC ceremony for? The Multi-Party Computation ceremony generates the trusted setup parameters (structured reference string) for Union's ZK proving system. Each participant contributes randomness -- as long as at least one participant is honest, the parameters are secure.

Can I run multiple Voyager instances? Yes, but each instance must use a separate queue database directory. Multiple instances can relay different chain pairs in parallel.

What is the chain.kitchen domain? chain.kitchen hosts Union's public RPC infrastructure. It provides endpoints for all supported chains in the Union network.

How do I debug a failed cross-chain transfer?

  1. Check the transaction hash on the source chain's explorer
  2. Look up the packet in Union's Explorer at app.union.build
  3. If using a private relayer, check voyager queue query-failed
  4. Verify the channel is active and the light client is updated

What is UCS05? UCS05 is Union's cross-chain contract call standard. It extends ZKGM (UCS03) with the ability to call smart contracts on destination chains, enabling cross-chain DeFi composability.

Are there rate limits on the public RPC endpoints? Public RPC endpoints may have rate limits. For production applications, consider running your own full nodes or using dedicated RPC providers.

What is CometBLS? CometBLS is Union's custom consensus module that extends CometBFT with BLS12-381 signature aggregation. It enables compact ZK proofs of validator consensus, which is the foundation of Union's trust-minimized bridging.

What smart contract standards does Union use on EVM? Union deploys IBC handler contracts on EVM chains for packet processing. The ZKGM (UCS03) protocol is implemented in Solidity for EVM chains, Cairo for Starknet, Move for Aptos/Movement, and CosmWasm for Cosmos chains.