Bitcoin and Zcash use a different flow than other chains. They’re UTXO-based, so:
- Deposits generate a unique address, you send funds, then finalize on NEAR
- Withdrawals initiate on NEAR, wait for MPC signing, then broadcast
Setup
import { createBridge, ChainKind } from "@omni-bridge/core"
import { createBtcBuilder } from "@omni-bridge/btc"
import { createNearBuilder, toNearKitTransaction } from "@omni-bridge/near"
import { Near } from "near-kit"
const bridge = createBridge({ network: "mainnet" })
const btc = createBtcBuilder({ network: "mainnet", chain: "btc" })
const nearBuilder = createNearBuilder({ network: "mainnet" })
const near = new Near({ network: "mainnet", privateKey: "ed25519:..." })
For Zcash, change chain: "zcash":
const zec = createBtcBuilder({ network: "mainnet", chain: "zcash" })
Deposits: Bitcoin → NEAR
Depositing Bitcoin gives you nBTC on NEAR.
Step 1: Get a Deposit Address
const { address } = await bridge.getUtxoDepositAddress(
ChainKind.Btc,
"alice.near" // NEAR recipient
)
console.log("Send BTC to:", address)
Each deposit address is unique to the recipient. Don’t reuse addresses for different accounts.
Step 2: Send Bitcoin
Send Bitcoin to the deposit address using any wallet. Wait for at least 2 confirmations (~20 minutes).
Step 3: Finalize on NEAR
After the Bitcoin transaction confirms, submit a proof to NEAR:
// Get the proof
const proof = await btc.getDepositProof(
"your_btc_tx_hash",
0 // vout (output index)
)
// Build finalization transaction
const finalizeTx = nearBuilder.buildUtxoDepositFinalization({
chain: "btc",
depositMsg: { recipient_id: "alice.near" },
txBytes: proof.tx_bytes,
vout: 0,
txBlockBlockhash: proof.tx_block_blockhash,
txIndex: proof.tx_index,
merkleProof: proof.merkle_proof,
signerId: "alice.near",
})
// Send to NEAR
await toNearKitTransaction(near, finalizeTx).send()
// Check balance
const balance = await nearBuilder.getUtxoTokenBalance("btc", "alice.near")
console.log("nBTC balance:", balance, "satoshis")
Finding the Output Index (vout)
Check your Bitcoin transaction on a block explorer:
- Go to mempool.space or blockstream.info
- Find your transaction
- Look at the outputs — find the one going to your deposit address
- The position (0, 1, 2, …) is your
vout
Withdrawals: NEAR → Bitcoin
Withdrawing nBTC from NEAR to Bitcoin.
Step 1: Check Balance and Build Plan
import type { UTXO } from "@omni-bridge/core"
const signerId = "alice.near"
const targetAddress = "bc1q..." // Your Bitcoin address
// Check balance
const balance = await nearBuilder.getUtxoTokenBalance("btc", signerId)
const config = await nearBuilder.getUtxoConnectorConfig("btc")
console.log("Balance:", balance, "satoshis")
console.log("Min withdraw:", config.min_withdraw_amount)
// Get available UTXOs and build withdrawal plan
const utxos: UTXO[] = await nearBuilder.getUtxoAvailableOutputs("btc")
const withdrawAmount = 100_000n // satoshis
const plan = btc.buildWithdrawalPlan(
utxos,
withdrawAmount,
targetAddress,
config.change_address,
2 // fee rate (sat/vB)
)
const bridgeFee = await nearBuilder.calculateUtxoWithdrawalFee("btc", withdrawAmount)
const totalAmount = withdrawAmount + plan.fee + bridgeFee
console.log("Network fee:", plan.fee, "satoshis")
console.log("Bridge fee:", bridgeFee, "satoshis")
console.log("Total:", totalAmount, "satoshis")
Step 2: Initiate on NEAR
const withdrawTx = nearBuilder.buildUtxoWithdrawalInit({
chain: "btc",
targetAddress,
inputs: plan.inputs,
outputs: plan.outputs,
totalAmount,
signerId,
})
const result = await toNearKitTransaction(near, withdrawTx).send()
console.log("Withdrawal initiated:", result.transaction.hash)
Step 3: Wait for MPC Signing
import { BridgeAPI } from "@omni-bridge/core"
const api = new BridgeAPI("mainnet")
let signedTxHash: string | undefined
for (let i = 0; i < 60; i++) {
const transfers = await api.getTransfer({
transactionHash: result.transaction.hash,
})
const transfer = transfers[0]
if (transfer?.signed?.NearReceipt?.transaction_hash) {
signedTxHash = transfer.signed.NearReceipt.transaction_hash
break
}
console.log(`Waiting for MPC signature... (${i + 1}/60)`)
await new Promise((r) => setTimeout(r, 5000))
}
if (!signedTxHash) throw new Error("MPC signing timed out")
Step 4: Broadcast to Bitcoin
// Get signed transaction from NEAR logs
const signedTx = await near.getTransactionStatus(signedTxHash, signerId, "FINAL")
const signedLog = signedTx.receipts_outcome
.flatMap((r) => r.outcome.logs)
.find((log) => log.includes("signed_btc_transaction"))
const jsonPart = signedLog.split("EVENT_JSON:")[1]
const signedData = JSON.parse(jsonPart)
const txBytes: number[] = signedData.data[0].tx_bytes
// Convert to hex and broadcast
const txHex = txBytes.map((b) => b.toString(16).padStart(2, "0")).join("")
const btcTxHash = await btc.broadcastTransaction(txHex)
console.log("Bitcoin TX:", btcTxHash)
console.log("Explorer:", `https://mempool.space/tx/${btcTxHash}`)
Zcash
Zcash works identically — just change the chain parameter:
const zec = createBtcBuilder({ network: "mainnet", chain: "zcash" })
// Deposit
const { address } = await bridge.getUtxoDepositAddress(
ChainKind.Zcash,
"alice.near"
)
// Withdrawal
const utxos = await nearBuilder.getUtxoAvailableOutputs("zcash")
const config = await nearBuilder.getUtxoConnectorConfig("zcash")
const plan = zec.buildWithdrawalPlan(
utxos,
100_000n,
"t1...", // Zcash transparent address
config.change_address
)
Zcash only supports transparent addresses (t1...). Shielded addresses are not supported.
Fee Calculation
Bitcoin uses a linear fee model based on transaction size:
import { linearFeeCalculator } from "@omni-bridge/btc"
const calculator = linearFeeCalculator({
base: 10, // Base overhead
input: 68, // Bytes per input
output: 31, // Bytes per output
rate: 5, // sat/vB
})
const fee = calculator(2, 2) // 2 inputs, 2 outputs
Zcash uses ZIP-317 marginal fees (handled automatically).
Connector Configuration
Check limits and fees:
const config = await nearBuilder.getUtxoConnectorConfig("btc")
console.log({
minDeposit: config.min_deposit_amount,
minWithdraw: config.min_withdraw_amount,
depositFee: config.deposit_bridge_fee,
withdrawFee: config.withdraw_bridge_fee,
})
Error Handling
try {
const proof = await btc.getDepositProof(txHash, vout)
} catch (error) {
if (error.message.includes("not found")) {
console.error("Transaction not found or not confirmed yet")
} else if (error.message.includes("invalid vout")) {
console.error("Wrong output index")
}
}
try {
const plan = btc.buildWithdrawalPlan(utxos, amount, to, change)
} catch (error) {
if (error.message.includes("No UTXOs")) {
console.error("No UTXOs available in bridge pool")
} else if (error.message.includes("Insufficient")) {
console.error("Not enough UTXOs to cover amount + fees")
}
}
Next Steps