Skip to content

Send Funds Holesky → Sepolia

This guide walks you through writing our example “Send Funds Holesky -> Sepolia”. The goal of this guide it to show you how to create a token order and submit it to the ucs03-zkgm interface.

Relevant imports will be included with each step. Outside of libraries provided by Union, this guide uses viem and Effect.

This first section is a walk through of creating the program section of the send funds example.

Begin by using Effect to create a program function.

import { Effect, Logger } from "effect"
const program = Effect.gen(function*() {})

Using the ChainRegistry from the Union TS SDK, you can declare the source and destination chains used in this example. In this case, the source is Holesky (ethereum.17000) and the destination is Sepolia (ethereum.11155111).

import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { Effect, Logger } from "effect"
const program = Effect.gen(function*() {
const source = yield* ChainRegistry.byUniversalId(
UniversalChainId.make("ethereum.17000"),
)
const destination = yield* ChainRegistry.byUniversalId(
UniversalChainId.make("ethereum.11155111"),
)
})

Now you can define the token order that will be responsible for the transfer. This example uses the TokenOrderV2 TS interface.

To construct the token order, you will need the token contract/denom on both the source chain (baseToken) and the destination chain (quoteToken). In this case, we are using the relevant contract addresses for LINK.

We also need to determine the Kind for the TokenOrder. In this case, we use esccrow. Though kind can be initialize, escrow, unescrow, or solve. To understand which kind of token order to use, refer to the ucs03 EVM Token Order Examples docs.

import * as TokenOrder from "@unionlabs/sdk/TokenOrder"
import { Effect, Logger } from "effect"
const program = Effect.gen(function*() {
// ... snip ...
const tokenOrder = yield* TokenOrder.make({
source,
destination,
sender: "0x06627714f3F17a701f7074a12C02847a5D2Ca487",
receiver: "0x50A22f95bcB21E7bFb63c7A8544AC0683dCeA302",
// LINK on Holesky
baseToken: "0x685ce6742351ae9b618f383883d6d1e0c5a31b4b",
baseAmount: 10n,
// Holesky LINK on Sepolia
quoteToken: "0x80fdbf104ec58a527ec40f7b03f88c404ef4ba63",
quoteAmount: 10n,
kind: "escrow",
metadata: undefined,
version: 2,
})
yield* Effect.log("Token Order V2", tokenOrder)
})

Finally, the token order you’ve constructed can be used as an instruction to create a ZkgmClientRequest.

import * as ZkgmClientRequest from "@unionlabs/sdk/ZkgmClientRequest"
import { Effect, Logger } from "effect"
const program = Effect.gen(function*() {
// ... snip ...
const request = ZkgmClientRequest.make({
source,
destination,
channelId: ChannelId.make(2),
ucs03Address: "0x5fbe74a283f7954f10aa04c2edf55578811aeb03",
instruction: tokenOrder,
})
})

Now that you’ve created a full zkgm request, you can execute it and wait on the response to close out the program.

import { ChannelId } from "@unionlabs/sdk/schema/channel"
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"
const program = Effect.gen(function*() {
// ... snip ...
const zkgmClient = yield* ZkgmClient.ZkgmClient
// NOTE: 1. switch chain is assumed
// NOTE: 2. write in progress
const response: ZkgmClientResponse.ZkgmClientResponse = yield* zkgmClient.execute(request)
// NOTE: 3. write complete (with tx hash)
yield* Effect.log("Submission Hash", response.txHash)
const completion = yield* response.waitFor(
ZkgmIncomingMessage.LifecycleEvent.$is("EvmTransactionReceiptComplete"),
)
// NOTE: 4. tx complete
yield* Effect.log("Completion", completion)
})

With the program ready, we can now use Effect to execute our transfer and listen for a response.

Below, several items of note are provided to the program:

  • Evm.WalletClient connected to a Holesky RPC
    • Contains a manually created key pair
  • Evm.PublicClient connected to a Holesky RPC
import { Evm, EvmZkgmClient } from "@unionlabs/sdk-evm"
import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry"
import { Effect, Logger } from "effect"
import { http } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { holesky } from "viem/chains"
const program = Effect.gen(function*() {
// ... program ...
}).pipe(
Effect.provide(EvmZkgmClient.layerWithoutWallet),
Effect.provide(Evm.WalletClient.Live({
account: privateKeyToAccount(
(process.env.KEY as any) ?? "0x...",
),
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)

You have now created and executed a transfer from an EVM chain using USC03-ZKGM with the Union TS SDK 🎉.

For ease of use, you can refer to the complete example “Send Funds Holesky -> Sepolia”.

Happy building!