A viem-shaped client for Provable's cross-chain bridge service. The bridge
moves value between Aleo and other chains through third-party swap providers;
this package wraps its REST API (/bridge/* on the wallet-services API) as
typed actions on a client, plus an end-to-end swap action that also signs
the Aleo deposit. Aleo is always one side of the pair — the service does not
route, say, Ethereum to Solana.
import { createBridgeClient, httpBridge } from '@provablehq/veil-aleo-bridges'
const client = createBridgeClient({
transport: httpBridge('https://wallet.api.provable.com'),
})
const { quotes } = await client.getQuotes({
srcChain: 'ALEO',
srcAsset: 'ALEO_MAINNET',
destChain: 'SOLANA',
destAsset: 'SOL_SOLANA',
amountIn: '100',
recipientAddress: solAddress, // where the SOL lands
refundAddress: aleoAddress, // where a failed swap refunds
})pnpm add @provablehq/veil-aleo-bridges @provablehq/veil-coreThe read/track actions need only the bridge client. The swap action
additionally takes a @provablehq/veil-core WalletClient (any signer path — local key
via @provablehq/veil-aleo-sdk, or a wallet adapter) to sign the Aleo deposit.
Quotes fan out to the providers the service has enabled. Each quote names its provider, and the order you create is bound to that provider for its whole lifecycle. Three providers implement bridging today:
| Provider | Code | What it handles |
|---|---|---|
| NEAR Intents | NEAR_INTENTS |
Native ALEO to majors on other chains (SOL, ETH, USDT, USDC), and inbound to native ALEO. Skips quoting unless recipientAddress and refundAddress (or fromAddress) are present. |
| Halliday | HALLIDAY |
Inbound routes into Aleo's wrapped assets (ETH → ETH_ALEO, USDC → USDC_ALEO, BTC → WBTC_ALEO), and inbound to native ALEO. Also a fiat on-ramp. |
| Houdini Swap | HOUDINI |
CEX-routed swaps. Enabled per environment; returning no quotes in production at the time of writing. |
Which providers actually answer depends on two server-side switches — a registry entry and the provider adapter's own enablement (API keys, config) — so the set varies by environment and over time. Do not hardcode provider assumptions; read them from the quotes you get back.
Provider capabilities can also be gated by feature flags. getFlags() returns
the current values (for example near_supports_pub_priv_swaps); check them
before offering routes the flag gates.
A route is a (source asset, destination asset) pair some provider will quote. Routes appear and disappear — with provider enablement, liquidity, and flags — so treat any static description as a snapshot. The following patterns exist within the Aleo bridge as of July 2026.
| Pattern | Example pairs | Quoted by |
|---|---|---|
| Native ALEO → External Pairs | ALEO_MAINNET → SOL_SOLANA, ETH_MAINNET, USDT_TRON, USDC_SOLANA, … |
NEAR Intents |
| External Pairs → Native ALEO | SOL_SOLANA → ALEO_MAINNET, ETH_MAINNET → ALEO_MAINNET, … |
NEAR Intents, Halliday |
| External Pairs → Aleo Wrapped Assets | ETH_MAINNET → ETH_ALEO, USDC_ETH → USDC_ALEO, BTC_MAINNET → WBTC_ALEO |
Halliday |
Note the asymmetry: value can enter Aleo as wrapped assets or native ALEO, but leaves only as native ALEO — the wrapped assets have no outbound routes at this snapshot.
getRoutes() finds the routes from the asset catalog. This function returns
an array of asset pairs (src asset, destination asset) that share a supporting
provider and provide the metadata needed to get quotes.
// Data returned from getRoutes().
{
aleoAsset: { code: 'ALEO_MAINNET', chain: 'ALEO', chainName: 'Aleo', symbol: 'ALEO', … },
externalAsset: { code: 'USDC_BASE', chain: 'EVM:8453', chainName: 'Base', symbol: 'USDC', … },
providers: ['NEAR_INTENTS', 'HALLIDAY'],
}The routes returned by getRoutes() are candidate routes.
Call getQuotes() to confirm that a particular pair and direction
(Aleo → External or External → Aleo) is available to bridge, with
recipientAddress and refundAddress set. An empty array means no enabled
provider supports the route at the time of the quote.
// Everywhere USDC can move relative to Aleo:
const routes = await bridge.getRoutes({ symbol: 'USDC' })
const r = routes[0]
r.externalAsset.code // 'USDC_ETH'
r.externalAsset.chainName // 'Ethereum' (human-readable; chain id is 'EVM:1')
r.providers // ['HALLIDAY']
// Get a quote to see if the route exists.
const { quotes, meta } = await bridge.getQuotes({
srcChain: 'Ethereum',
srcAsset: 'USDC',
destChain: 'Aleo',
destAsset: 'USDC',
amountIn: '250',
recipientAddress: aleoAddress,
refundAddress: ethAddress,
})getQuotes returns one entry per provider willing to take the route, plus a
meta block. A live capture from the ALEO → SOL route, trimmed:
// Data returned from getQuotes().
{
quotes: [
{
provider: { id: 'ab26…', code: 'NEAR_INTENTS', displayName: 'NEAR Intents', … },
srcChain: 'ALEO', // resolved identifiers echoed back,
destChain: 'SOLANA', // even when you passed names/symbols
srcAsset: 'ALEO_MAINNET',
destAsset: 'SOL_SOLANA',
amountIn: '100', // decimal display units throughout
amountOut: '0.023951296', // estimated receive amount
minAmountOut: '0.023711783', // slippage floor
estimatedTimeSeconds: 900,
quoteId: '1089843a-…', // → createOrder's quoteId (some routes use quoteOptionId)
integrationType: 'CEX',
feeEstimate: { provider: { feeUsd: '0.020501', … }, appFeeBps: 5, … },
…
},
],
meta: {
count: 1,
quoteRequestId: '98b21e5f-…', // support handle — log it
// warnings / providerErrors appear here when providers skip or fail
},
}The fields that matter downstream: quoteId (or quoteOptionId) and
provider.id are what createOrder takes; the echoed srcAsset/destAsset
are the resolved codes; and destChainWalletValidationRegex validates the
recipient before committing.
Three conventions run through every call. Get these wrong and the API rejects
the request with a 400. Do not hardcode or guess the values — they all come
from getAssets():
- Chains are the API's identifiers, case-sensitive:
ALEO,SOLANA,BITCOIN,TRON, andEVM:<chainId>for EVM networks (EVM:1mainnet,EVM:8453Base,EVM:42161Arbitrum). Read them from the catalog'schainfield. For display,chainDisplayName('EVM:8453')→'Base'— a client-side map for now, until the API exposes its chain registry. - Assets are chain-qualified codes, never bare symbols:
ALEO_MAINNET,USDC_ALEO,ETH_BASE.ALEOalone is rejected. Read them from the catalog'scodefield. - Amounts are decimal strings in display units (
"1.5"ALEO, not microcredits), with at most the asset'sdecimalsof precision. Quotes and deposit instructions come back the same way. Theswapaction converts to atomic units internally when it builds the Aleo transfer; if you build a deposit yourself,parseDecimalAmount(amount, decimals)does the exact string-based conversion.
getQuotes and swap soften both rules for you: chains resolve from display
names locally, and asset symbols resolve against the catalog within their
chain (one extra getAssets fetch, only when a symbol is passed — exact
codes keep the single request). createOrder stays strict: echo the chosen
quote, which carries the resolved codes.
The literal codes in this README's examples are real, but they are snapshots — resolve them at runtime the way the example above does.
For swaps FROM Aleo, swap runs the following flow
- Gets quotes from the providers
- Picks a quote
- Creates the bridge order (a real server-side order — unfunded orders expire)
- Makes an Aleo unshield deposit through the source asset's Aleo program
- Optionally polls the order to completion.
The signing wallet for the swap is set during the bridge client's creation.
This can be optionally overridden by providing another wallet to the swap action's
wallet parameter.
import { createBridgeClient, httpBridge } from '@provablehq/veil-aleo-bridges'
const bridge = createBridgeClient({
transport: httpBridge('https://wallet.api.provable.com'),
wallet: walletClient, // @provablehq/veil-core WalletClient — signs deposits
})
const result = await bridge.swap({
from: { asset: 'ALEO_MAINNET', amount: '100' }, // from.chain optional; must be Aleo
to: { chain: 'Solana', asset: 'SOL_SOLANA', address: solAddress }, // chain by id or name
selectQuote: 'best', // or 'fastest', or a callback
poll: true, // wait for COMPLETED
onStage: (s) => console.log(s.status),
})
result.depositTxId // at1... — the Aleo deposit transition
result.orderId // track or audit later
result.finalStatus // present because poll was truthyChain slots accept the API identifier or the display name ('Solana',
'Ethereum'), case-insensitively.
Three more optional parameters include:
providerpins quote selection to one provider by code ('NEAR_INTENTS').refundAddressredirects refunds away from the default (the signing wallet's address).from.chainexists for shape-stability — it defaults to'ALEO'and must resolve to Aleo, since this action signs the deposit with the Aleo wallet.
Every action also exists in viem's standalone, tree-shakable form
(import { swap } from '@provablehq/veil-aleo-bridges' then swap(client, params)) for
bundle-sensitive consumers; the client form above is the primary API.
Compliance-bearing source assets (USDCX_ALEO, USAD_ALEO) require a
merkleProof input for their unshield transition — pass it via
SwapParameters.merkleProof; swap throws before moving anything if it is
missing. If the API lists an Aleo asset this SDK does not know yet, extend the
program map: pass aleoAssetMap: { ...DEFAULT_ALEO_ASSET_MAP, NEW_CODE: { program: '...', decimals: n } }.
An inbound swap starts on the other chain, so its deposit is signed there — Veil's Aleo keys handle everything except that one transfer. For an EVM source the deposit is plain viem, which means the whole flow stays in territory an EVM developer already knows:
import { createBridgeClient, httpBridge } from '@provablehq/veil-aleo-bridges'
import { createWalletClient, http, erc20Abi, parseUnits } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
const bridge = createBridgeClient({ transport: httpBridge('https://wallet.api.provable.com') })
const evm = createWalletClient({
account: privateKeyToAccount(ethPrivateKey),
chain: mainnet,
transport: http(),
})
// 1. Route + quote. Refunds happen on the source chain, so refundAddress is
// the Ethereum account; recipientAddress is where the ALEO lands.
const routes = await bridge.getRoutes({ symbol: 'USDC', externalChain: 'Ethereum' })
const route = routes.find((r) => r.aleoAsset.native && r.externalAsset.symbol === 'USDC')!
const { quotes } = await bridge.getQuotes({
srcChain: route.externalAsset.chain, srcAsset: route.externalAsset.code,
destChain: route.aleoAsset.chain, destAsset: route.aleoAsset.code,
amountIn: '25',
recipientAddress: aleoAddress,
refundAddress: evm.account.address,
})
const quote = quotes[0]
// 2. Create the order — walletAddress is the payout recipient on Aleo.
const order = await bridge.createOrder({
providerId: quote.provider.id,
srcChain: quote.srcChain, destChain: quote.destChain,
srcAsset: quote.srcAsset, destAsset: quote.destAsset,
amountIn: quote.amountIn,
walletAddress: aleoAddress,
quoteId: (quote.quoteId ?? quote.quoteOptionId)!,
refundAddress: evm.account.address,
})
// 3. Pay the deposit from the Ethereum wallet — the one non-Veil step.
// Check order.depositMemo is empty first (an ERC-20 transfer can't carry
// one) and order.expiration hasn't passed.
await evm.writeContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
abi: erc20Abi,
functionName: 'transfer',
args: [order.depositAddress as `0x${string}`, parseUnits(order.depositAmount!, 6)],
})
// 4. Track to completion — the ALEO arrives as public balance, ready to
// shield (transfer with visibility: 'shield') for private use.
await bridge.waitForOrder({ id: order.orderId })One irreversibility note: once the ERC-20 transfer is sent, funds are
committed to the provider flow — recovery is the provider's refund to your
Ethereum address, not a revert. The runnable version of this flow, gated
like the fund-moving tier, lives at
examples/bridge-into-aleo.ts, and
test/integration/inbound.e2e.test.ts
runs it with assertions (including the on-chain balance-delta check — the
reason the example targets native ALEO: arrival is one getBalance read).
Use the individual actions when the wallet is not in the same process (a browser flow where the user's wallet signs), when the source is not Aleo, or when you want control between steps.
import { createBridgeClient, httpBridge } from '@provablehq/veil-aleo-bridges'
const bridge = createBridgeClient({
transport: httpBridge('https://wallet.api.provable.com'),
wallet: walletClient, // @provablehq/veil-core WalletClient — signs deposits
})
// 1. Quote. One entry per provider willing to take the route. Chains accept
// ids or display names, assets accept codes or symbols (resolved per chain).
const { quotes, meta } = await client.getQuotes({
srcChain: 'Ethereum',
srcAsset: 'USDC',
destChain: 'Aleo',
destAsset: 'USDC',
amountIn: '250',
recipientAddress: aleoAddress,
refundAddress: ethAddress,
})
// meta.quoteRequestId identifies this request in support escalations.
// 2. Create an order from the quote you picked.
const q = quotes[0]
const order = await client.createOrder({
providerId: q.provider.id,
srcChain: q.srcChain, destChain: q.destChain,
srcAsset: q.srcAsset, destAsset: q.destAsset,
amountIn: q.amountIn,
walletAddress: aleoAddress, // where the bridged funds land
quoteId: (q.quoteId ?? q.quoteOptionId)!,
})
// 3. Satisfy the deposit instructions. The order does nothing until the
// deposit arrives; unfunded orders expire.
order.depositAddress // send exactly order.depositAmount here
order.depositMemo // include when present — omitting it strands funds
order.expiration // deposit before this
// 4. Track it.
const done = await bridge.waitForOrder({ id: order.orderId }) // → COMPLETED or throws
const status = await bridge.getOrder({ id: order.orderId }) // one snapshot
const audit = await bridge.getOrderAudit({ id: order.orderId }) // + step/provider event logAn order moves through stages (NEW, WAITING, CONFIRMING, EXCHANGING,
COMPLETED, …). waitForOrder polls until the target stage and throws
BridgeOrderFailedError on a terminal failure (FAILED, EXPIRED,
REFUNDED) or BridgeTimeoutError when time runs out. The status DTO's
steps array gives the finer-grained deposit_v1 workflow (order created →
awaiting deposit → deposit detected → … → completed).
All transport-level failures (4xx/5xx) throw TransportError from
@provablehq/veil-core with the response body in the message. Bridge-specific failures
throw BridgeError subclasses: BridgeEnvelopeError (malformed response
envelope), BridgeOrderFailedError (terminal order failure — carries the
order status), BridgeTimeoutError (polling deadline hit).
Bridged-in value lands as an Aleo asset (USDC on Ethereum arrives as
USDC_ALEO, ETH as ETH_ALEO), and from there it is ordinary Aleo money —
including tradeable on the Shield Swap DEX via @provablehq/shield-swap-sdk. Both
packages hang off the same @provablehq/veil-core wallet client, so one signer runs the
whole chain: bridge in, trade privately, bridge back out.
import { createBridgeClient, httpBridge } from '@provablehq/veil-aleo-bridges'
import { shieldSwapActions } from '@provablehq/shield-swap-sdk'
import { createWalletClient, custom, erc20Abi, parseUnits } from 'viem'
import { mainnet } from 'viem/chains'
// One Aleo wallet client, two Veil surfaces.
const bridge = createBridgeClient({
transport: httpBridge('https://wallet.api.provable.com'),
wallet: walletClient,
})
const dex = walletClient.extend(shieldSwapActions({ api: {} }))
// 1. Bridge in: pick the route from the graph, quote it, create the order.
// The symbol filter matches either side, so pin BOTH sides to USDC — routes[0]
// could otherwise be a native-ALEO pair.
const routes = await bridge.getRoutes({ symbol: 'USDC', externalChain: 'Ethereum' })
const route = routes.find((r) => r.aleoAsset.symbol === 'USDC' && r.externalAsset.symbol === 'USDC')!
const { quotes } = await bridge.getQuotes({
srcChain: route.externalAsset.chain, srcAsset: route.externalAsset.code,
destChain: route.aleoAsset.chain, destAsset: route.aleoAsset.code,
amountIn: '250',
recipientAddress: aleoAddress, refundAddress: ethAddress,
})
const q = quotes[0]
const order = await bridge.createOrder({
providerId: q.provider.id,
srcChain: q.srcChain, destChain: q.destChain,
srcAsset: q.srcAsset, destAsset: q.destAsset,
amountIn: q.amountIn,
walletAddress: aleoAddress, // where the bridged USDC lands
quoteId: (q.quoteId ?? q.quoteOptionId)!,
})
// 2. Pay the deposit from the user's EVM wallet — this side is plain viem.
const evm = createWalletClient({ chain: mainnet, transport: custom(window.ethereum) })
const [ethAccount] = await evm.getAddresses()
await evm.writeContract({
account: ethAccount,
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
abi: erc20Abi,
functionName: 'transfer',
args: [order.depositAddress as `0x${string}`, parseUnits(order.depositAmount!, 6)],
})
await bridge.waitForOrder({ id: order.orderId }) // USDC_ALEO arrives
// 3. Trade on the DEX: privatize, swap, claim.
const handle = await dex.swap({ poolKey, tokenInId, amountIn, slippageBps: 50, tokenInProgram, imports })
await dex.claimSwapOutput({ handle, imports })
// 4. Bridge back out — one call, deposit signed by the same Aleo wallet.
await bridge.swap({
from: { asset: 'ALEO_MAINNET', amount: '100' },
to: { chain: 'Solana', asset: 'SOL_SOLANA', address: solAddress },
poll: true,
})The whole chain is exercised by
packages/shield-swap/test/integration/bridgeRoundTrip.e2e.test.ts,
which also documents the two seams to know about: the inbound deposit needs a
source-chain signer, and the DEX currently runs on testnet while the bridge is
mainnet.
Every action ships as an agent tool. createBridgeAgentTools(client) (from
@provablehq/veil-aleo-bridges/agent) returns core-shaped AgentTools for any agent
framework; createBridgeMcpServer(client) (from @provablehq/veil-aleo-bridges/mcp) serves
them over MCP. The tools compose with other Veil packages' tools through
core's toMcpServer:
import { createAgentTools } from '@provablehq/veil-core/agent'
import { toMcpServer } from '@provablehq/veil-core/mcp'
import { createBridgeAgentTools } from '@provablehq/veil-aleo-bridges/agent'
const server = toMcpServer([
...createAgentTools({ client: publicClient }),
...createBridgeAgentTools(bridgeClient),
])bridge_swap signs and broadcasts with the wallet the host wired into the
client — expose it only to agents you intend to let move funds. The rest
(discovery, flags, quotes, order tracking) are read-only against the API,
though bridge_create_order does create a real order server-side. The
discovery tools matter for agents especially: bridge_list_assets,
bridge_list_routes, and bridge_list_providers give the model the chain
ids, asset codes, chain names, decimals, and provider support it must not
guess — the descriptions steer it to discover before quoting, and
bridge_list_routes answers "what can move where" directly.
test/integration/ runs against the live API and its real providers —
never mocked — in two tiers, gated so the default suite stays offline.
VEIL_BRIDGE_API_URL overrides the target deployment for both.
Read-only tier (api.integration.test.ts) needs only
VEIL_INTEGRATION=1. Quotes and error paths; no orders, no funds — though
every quote request does fan out to real provider systems.
VEIL_INTEGRATION=1 pnpm exec vitest run packages/bridge/test/integration/api.integration.test.tsRoute assertions are deliberately loose — everything asserts invariants of whatever comes back, because route availability is a moving target. One reference route (native ALEO → native SOL) is required to quote: it is the pair that consistently quotes in production today, so its silence signals a regression rather than shifting liquidity.
Swap e2e tier (e2e.test.ts) runs the whole chain on mainnet: quote,
create the order, sign and broadcast the Aleo unshield deposit, poll the
order to COMPLETED, and audit it. This spends real ALEO and delivers real
SOL, so it takes an explicit second gate on top of the integration flag plus
a mainnet-funded account and proving credentials:
VEIL_INTEGRATION=1 VEIL_BRIDGE_E2E=1 \
pnpm exec vitest run packages/bridge/test/integration/e2e.test.tsRequires VEIL_E2E_PRIVATE_KEY (funded on mainnet), ALEO_DPS_API_KEY, and
ALEO_CONSUMER_ID. VEIL_BRIDGE_SWAP_AMOUNT sets the decimal ALEO to swap
(default 5 — providers reject amounts below their minimums) and
VEIL_BRIDGE_DEST_ADDRESS the Solana recipient. The test shields a private
record first when none covers the deposit, and fails before moving funds if
no provider quotes the route.
Inbound e2e (inbound.e2e.test.ts) runs the other direction — USDC on
Ethereum → native ALEO — with viem signing the Ethereum deposit. Same gates,
plus ETH_PRIVATE_KEY (an Ethereum account holding USDC and gas);
VEIL_BRIDGE_INBOUND_AMOUNT sets the USDC amount (default 25), and
ETH_RPC_URL/ETH_USDC_CONTRACT override the Ethereum endpoint and token
contract. It spends real USDC and gas — and once the deposit is sent, funds
are committed to the provider flow (recovery is the refund path). The
narrative version is examples/bridge-into-aleo.ts.