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
- 2. Installation
- 3. Configuration
- 4. Core Usage -- Web UI (app.union.build)
- 5. Core Usage -- TypeScript SDK
- 6. Core Usage -- uniond (Chain Binary)
- 7. Core Usage -- Voyager (IBC Relayer)
- 8. Core Usage -- unionvisor (Process Supervisor)
- 9. Core Usage -- u CLI Tool
- 10. Core Usage -- drip (Faucet)
- 11. Core Usage -- galoisd (ZK Proving)
- 12. Core Usage -- Ceremony (MPC)
- 13. Supported Chains and Channels
- 14. Common Tasks and Workflow Recipes
- 15. Advanced Usage
- 16. Troubleshooting
- 17. FAQ
1. Quick Start
The fastest way to experience Union is to bridge assets through the web application:
- Visit app.union.build
- Connect your wallet (MetaMask, Keplr, Leap, or a Sui wallet)
- Select a source chain, destination chain, and token
- 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-10and 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
effectalongside 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 developto 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
- Navigate to the Transfer page
- Select the source chain and asset from the dropdown
- Select the destination chain
- Enter the transfer amount
- The quote amount displays the expected received amount on the destination
- 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 explorerPOST /-- 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
- Visit the ceremony web app
- Connect your wallet (for identity verification)
- Download and run the MPC client binary
- Follow the on-screen instructions to generate and submit your contribution
- 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 channeltokenorder-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:
- Connect MetaMask (Ethereum Mainnet)
- Connect Keplr (Union)
- Select Ethereum as source, Union as destination
- Select ETH, enter amount
- 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.tapErrorCauseto 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:
- Verify plugins are loaded:
voyager -c config.json plugin list - Check interest filters:
voyager -c config.json plugin interest <plugin-name> - 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:
- Check peer count:
uniond status | jq '.NodeInfo.n_peers' - Add more persistent peers to
config.toml - Check disk space and I/O performance
- 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:
- Check the transfer status in the Explorer at app.union.build
- If using a private relayer, check
voyager queue stats - 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:
- Update your wallet extension to the latest version
- For Keplr/Leap: the app should automatically suggest adding the Union chain
- 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:
- Verify Voyager is running with
start(not justindex) - Check plugin list for completeness
- 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.kitchenhttps://rpc.17000.ethereum.chain.kitchenhttps://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?
- Check the transaction hash on the source chain's explorer
- Look up the packet in Union's Explorer at app.union.build
- If using a private relayer, check
voyager queue query-failed - 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.