From 47589840ed22d53ed71a606284afbfd5d7d9b9e5 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:10:39 -0700 Subject: [PATCH 1/4] feat: replace postActions/extraMsg with safe_deposit for UTXO deposits The bridge API now uses view calls instead of on-chain transactions to generate deposit addresses, avoiding NotEnoughBalance errors. Replace postActions/extraMsg with safe_deposit on getUtxoDepositAddress, and use safe_verify_deposit when finalizing deposits with safe_deposit. Matches bridge-sdk-rs#260. --- docs/reference/core.mdx | 60 +++++++++----------------------- docs/reference/near.mdx | 7 +++- packages/core/src/api.ts | 19 +++++----- packages/core/src/bridge.ts | 27 ++++---------- packages/core/src/index.ts | 1 + packages/core/tests/api.test.ts | 18 +++------- packages/near/src/builder.ts | 6 ++-- packages/near/src/index.ts | 1 + packages/near/src/types.ts | 10 ++++++ packages/near/tests/utxo.test.ts | 25 ++++++++++++- 10 files changed, 82 insertions(+), 92 deletions(-) diff --git a/docs/reference/core.mdx b/docs/reference/core.mdx index 870f8ea9..a8e5d17e 100644 --- a/docs/reference/core.mdx +++ b/docs/reference/core.mdx @@ -20,6 +20,7 @@ import { type TransferStatus, type Chain, type PostAction, + type SafeDeposit, type UtxoChainParam, type UtxoDepositAddressResponse, @@ -427,12 +428,14 @@ bridge.getUtxoDepositAddress( Optional configuration for the deposit. - - Post-actions to execute after the deposit is finalized on NEAR. Used for automatic bridging to other chains. - - - - Extra message to include in the deposit. + + Safe deposit message. When provided, the deposit will use `safe_verify_deposit` on finalization. + + + + Message for the safe deposit. + + @@ -454,10 +457,6 @@ bridge.getUtxoDepositAddress( The recipient on NEAR. - - - Post-actions if any were provided. - @@ -474,16 +473,12 @@ const deposit = await bridge.getUtxoDepositAddress( console.log(`Send BTC to: ${deposit.address}`) -// With post-actions for automatic bridging to Ethereum -const depositWithBridge = await bridge.getUtxoDepositAddress( +// With safe deposit +const safeDeposit = await bridge.getUtxoDepositAddress( ChainKind.Btc, "alice.near", { - postActions: [{ - receiver_id: "omni.bridge.near", - amount: "0", - msg: JSON.stringify({ recipient: "eth:0x..." }), - }] + safeDeposit: { msg: "safe deposit" } } ) ``` @@ -860,8 +855,7 @@ Gets a deposit address for Bitcoin or Zcash. api.getUtxoDepositAddress( chain: UtxoChainParam, recipient: string, - postActions?: PostAction[] | null, - extraMsg?: string | null + safeDeposit?: SafeDeposit | null ): Promise ``` @@ -875,36 +869,16 @@ api.getUtxoDepositAddress( NEAR recipient account ID. - - Optional post-actions to execute after deposit finalization. - - - - NEAR account to receive the action. - - - - Amount to send with the action. - + + Optional safe deposit message. + - Message/arguments for the action. - - - - Optional gas limit. - - - - Optional memo. + Message for the safe deposit. - - Optional extra message. - - ### Returns diff --git a/docs/reference/near.mdx b/docs/reference/near.mdx index 009a1d0e..185900c3 100644 --- a/docs/reference/near.mdx +++ b/docs/reference/near.mdx @@ -61,6 +61,7 @@ import { type UtxoDepositFinalizationParams, type UtxoDepositMsg, type UtxoPostAction, + type UtxoSafeDeposit, type UtxoWithdrawalInitParams, type UtxoWithdrawalOutput, type UtxoWithdrawalVerifyParams, @@ -785,7 +786,7 @@ Methods for interacting with the Bitcoin and Zcash UTXO connectors on NEAR. ### buildUtxoDepositFinalization -Build a transaction to finalize a UTXO deposit on NEAR. Calls `verify_deposit` on the connector contract. +Build a transaction to finalize a UTXO deposit on NEAR. Calls `verify_deposit` (or `safe_verify_deposit` when `safe_deposit` is provided) on the connector contract. #### Signature @@ -814,6 +815,10 @@ buildUtxoDepositFinalization(params: UtxoDepositFinalizationParams): NearUnsigne Optional extra message. + + + Safe deposit message. When provided, uses `safe_verify_deposit` instead of `verify_deposit`. + diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 0817848f..8b778ee4 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -195,6 +195,10 @@ const PostActionSchema = z.object({ }) export type PostAction = z.infer +export interface SafeDeposit { + msg: string +} + const UtxoDepositAddressResponseSchema = z.object({ address: z.string(), }) @@ -358,19 +362,15 @@ export class BridgeAPI { async getUtxoDepositAddress( chain: UtxoChainParam, recipient: string, - postActions?: PostAction[] | null, - extraMsg?: string | null, + safeDeposit?: SafeDeposit | null, ): Promise { const body: Record = { chain, recipient, } - if (postActions !== undefined && postActions !== null) { - body["post_actions"] = postActions - } - if (extraMsg !== undefined && extraMsg !== null) { - body["extra_msg"] = extraMsg + if (safeDeposit !== undefined && safeDeposit !== null) { + body["safe_deposit"] = safeDeposit } const url = this.buildUrl("/api/v3/utxo/get_user_deposit_address") @@ -401,9 +401,8 @@ export class BridgeAPI { async getUtxoUserDepositAddress( chain: UtxoChainParam, recipient: string, - postActions?: PostAction[] | null, - extraMsg?: string | null, + safeDeposit?: SafeDeposit | null, ): Promise { - return this.getUtxoDepositAddress(chain, recipient, postActions, extraMsg) + return this.getUtxoDepositAddress(chain, recipient, safeDeposit) } } diff --git a/packages/core/src/bridge.ts b/packages/core/src/bridge.ts index b3b05e17..6836a014 100644 --- a/packages/core/src/bridge.ts +++ b/packages/core/src/bridge.ts @@ -3,7 +3,7 @@ */ import { Near } from "near-kit" -import { BridgeAPI, type Chain, type PostAction, type UtxoChainParam } from "./api.js" +import { BridgeAPI, type Chain, type SafeDeposit, type UtxoChainParam } from "./api.js" import { type ChainAddresses, getAddresses } from "./config.js" import { ValidationError } from "./errors.js" import { @@ -28,14 +28,10 @@ export interface BridgeConfig { */ export interface UtxoDepositOptions { /** - * Post-actions to execute after the deposit is finalized on NEAR. - * Used for automatic bridging to other chains. + * Safe deposit message. When provided, the deposit will use `safe_verify_deposit` + * which requires a NEAR deposit but doesn't need post-actions. */ - postActions?: PostAction[] - /** - * Extra message to include in the deposit - */ - extraMsg?: string + safeDeposit?: SafeDeposit } /** @@ -54,10 +50,6 @@ export interface UtxoDepositResult { * The recipient on NEAR */ recipient: string - /** - * Post-actions if any were provided - */ - postActions?: PostAction[] } /** @@ -331,21 +323,14 @@ class BridgeImpl implements Bridge { const response = await this.api.getUtxoDepositAddress( chainParam, recipient, - options?.postActions, - options?.extraMsg, + options?.safeDeposit, ) - const result: UtxoDepositResult = { + return { address: response.address, chain: chainParam, recipient, } - - if (options?.postActions) { - result.postActions = options.postActions - } - - return result } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 24a83def..0058dd95 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,6 +11,7 @@ export { type BridgeAPIConfig, type Chain, type PostAction, + type SafeDeposit, type Transfer, type TransferStatus, type UtxoChainParam, diff --git a/packages/core/tests/api.test.ts b/packages/core/tests/api.test.ts index 2ad7e50d..dddb1389 100644 --- a/packages/core/tests/api.test.ts +++ b/packages/core/tests/api.test.ts @@ -262,20 +262,10 @@ describe("BridgeAPI", () => { }) }) - it("should handle post actions parameter", async () => { - const postActions = [ - { - receiver_id: "receiver.near", - amount: "1000000000000000000000000", - msg: "test message", - }, - ] - const response = await api.getUtxoDepositAddress( - "btc", - "recipient.near", - postActions, - "extra message", - ) + it("should handle safe_deposit parameter", async () => { + const response = await api.getUtxoDepositAddress("btc", "recipient.near", { + msg: "safe deposit message", + }) expect(response).toEqual({ address: "tb1qssh0ejglq0v53pwrsxlhxpxw29gfu6c4ls9eyy", }) diff --git a/packages/near/src/builder.ts b/packages/near/src/builder.ts index 7b45439f..1ead3d6a 100644 --- a/packages/near/src/builder.ts +++ b/packages/near/src/builder.ts @@ -607,6 +607,7 @@ class NearBuilderImpl implements NearBuilder { const connector = this.getUtxoConnectorAddress(params.chain) // Build deposit_msg for the contract (convert bigint amounts to strings) + const isSafeDeposit = params.depositMsg.safe_deposit != null const depositMsg = { recipient_id: params.depositMsg.recipient_id, post_actions: params.depositMsg.post_actions?.map((action) => ({ @@ -617,6 +618,7 @@ class NearBuilderImpl implements NearBuilder { gas: action.gas?.toString(), })), extra_msg: params.depositMsg.extra_msg, + safe_deposit: params.depositMsg.safe_deposit, } const args = { @@ -630,10 +632,10 @@ class NearBuilderImpl implements NearBuilder { const action: NearAction = { type: "FunctionCall", - methodName: "verify_deposit", + methodName: isSafeDeposit ? "safe_verify_deposit" : "verify_deposit", args: encodeArgs(args), gas: GAS.UTXO_VERIFY_DEPOSIT, - deposit: 0n, + deposit: isSafeDeposit ? DEPOSIT.SAFE_VERIFY_DEPOSIT : 0n, } return { diff --git a/packages/near/src/index.ts b/packages/near/src/index.ts index da34ad39..aa410613 100644 --- a/packages/near/src/index.ts +++ b/packages/near/src/index.ts @@ -47,6 +47,7 @@ export { type UtxoDepositFinalizationParams, type UtxoDepositMsg, type UtxoPostAction, + type UtxoSafeDeposit, type UtxoWithdrawalInitParams, type UtxoWithdrawalOutput, type UtxoWithdrawalSignParams, diff --git a/packages/near/src/types.ts b/packages/near/src/types.ts index b3bd294a..4bb32f8b 100644 --- a/packages/near/src/types.ts +++ b/packages/near/src/types.ts @@ -30,6 +30,7 @@ export const GAS = { export const DEPOSIT = { ONE_YOCTO: BigInt(parseAmount("1 yocto")), MPC_SIGNING: BigInt(parseAmount("0.25 NEAR")), + SAFE_VERIFY_DEPOSIT: 1_200_000_000_000_000_000_000n, } as const /** @@ -254,6 +255,14 @@ export interface UtxoPostAction { gas?: bigint } +/** + * Safe deposit message for UTXO deposits. + * When present, the finalization uses `safe_verify_deposit` instead of `verify_deposit`. + */ +export interface UtxoSafeDeposit { + msg: string +} + /** * Deposit message for UTXO deposits */ @@ -261,6 +270,7 @@ export interface UtxoDepositMsg { recipient_id: string post_actions?: UtxoPostAction[] extra_msg?: string + safe_deposit?: UtxoSafeDeposit } /** diff --git a/packages/near/tests/utxo.test.ts b/packages/near/tests/utxo.test.ts index 5e3edeed..b58b6e33 100644 --- a/packages/near/tests/utxo.test.ts +++ b/packages/near/tests/utxo.test.ts @@ -1,7 +1,7 @@ import { ChainKind } from "@omni-bridge/core" import { describe, expect, it } from "vitest" import { createNearBuilder } from "../src/builder.js" -import { GAS, DEPOSIT } from "../src/types.js" +import { DEPOSIT, GAS } from "../src/types.js" describe("NearBuilder UTXO methods", () => { const builder = createNearBuilder({ network: "testnet" }) @@ -98,6 +98,29 @@ describe("NearBuilder UTXO methods", () => { expect(args.deposit_msg.post_actions).toHaveLength(1) expect(args.deposit_msg.post_actions[0].amount).toBe("1000000") }) + + it("uses safe_verify_deposit when safe_deposit is provided", () => { + const tx = builder.buildUtxoDepositFinalization({ + chain: "btc", + depositMsg: { + recipient_id: "alice.testnet", + safe_deposit: { msg: "safe deposit" }, + }, + txBytes: [0x01, 0x02], + vout: 0, + txBlockBlockhash: "00000000000000000001abc123", + txIndex: 1, + merkleProof: ["hash1"], + signerId: "relayer.testnet", + }) + + expect(tx.actions[0]?.methodName).toBe("safe_verify_deposit") + expect(tx.actions[0]?.deposit).toBe(DEPOSIT.SAFE_VERIFY_DEPOSIT) + + const argsJson = new TextDecoder().decode(tx.actions[0]?.args) + const args = JSON.parse(argsJson) + expect(args.deposit_msg.safe_deposit).toEqual({ msg: "safe deposit" }) + }) }) describe("buildUtxoWithdrawalInit", () => { From 110473910c04337bff6010fd81d09f68984f7139 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:10:45 -0700 Subject: [PATCH 2/4] fix: resolve noNonNullAssertion lint warnings in starknet encoding --- packages/starknet/src/encoding.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/starknet/src/encoding.ts b/packages/starknet/src/encoding.ts index 1365f73f..c26718af 100644 --- a/packages/starknet/src/encoding.ts +++ b/packages/starknet/src/encoding.ts @@ -24,7 +24,11 @@ export function decodeByteArray(data: string[], offset: number): [string, number throw new Error(`decodeByteArray: offset ${offset} out of bounds (length ${data.length})`) } - const numFullWords = Number(BigInt(data[offset]!)) + const numFullWordsRaw = data[offset] + if (numFullWordsRaw === undefined) { + throw new Error(`decodeByteArray: missing data at offset ${offset}`) + } + const numFullWords = Number(BigInt(numFullWordsRaw)) const totalFelts = 1 + numFullWords + 2 if (offset + totalFelts > data.length) { @@ -34,11 +38,13 @@ export function decodeByteArray(data: string[], offset: number): [string, number } const pendingWordIdx = offset + 1 + numFullWords + const pendingWord = data[pendingWordIdx] as string + const pendingWordLen = data[pendingWordIdx + 1] as string const decoded = byteArray.stringFromByteArray({ data: data.slice(offset + 1, offset + 1 + numFullWords), - pending_word: data[pendingWordIdx]!, - pending_word_len: Number(BigInt(data[pendingWordIdx + 1]!)), + pending_word: pendingWord, + pending_word_len: Number(BigInt(pendingWordLen)), }) return [decoded, offset + totalFelts] From f6d49ef15eff430a18e83b79514a3e1f221045eb Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:18:07 -0700 Subject: [PATCH 3/4] fix: address PR feedback on type safety and constant style --- packages/near/src/types.ts | 2 +- packages/starknet/src/encoding.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/near/src/types.ts b/packages/near/src/types.ts index 4bb32f8b..eebe9f77 100644 --- a/packages/near/src/types.ts +++ b/packages/near/src/types.ts @@ -30,7 +30,7 @@ export const GAS = { export const DEPOSIT = { ONE_YOCTO: BigInt(parseAmount("1 yocto")), MPC_SIGNING: BigInt(parseAmount("0.25 NEAR")), - SAFE_VERIFY_DEPOSIT: 1_200_000_000_000_000_000_000n, + SAFE_VERIFY_DEPOSIT: BigInt(parseAmount("0.0012 NEAR")), } as const /** diff --git a/packages/starknet/src/encoding.ts b/packages/starknet/src/encoding.ts index c26718af..7ba42482 100644 --- a/packages/starknet/src/encoding.ts +++ b/packages/starknet/src/encoding.ts @@ -38,8 +38,11 @@ export function decodeByteArray(data: string[], offset: number): [string, number } const pendingWordIdx = offset + 1 + numFullWords - const pendingWord = data[pendingWordIdx] as string - const pendingWordLen = data[pendingWordIdx + 1] as string + const pendingWord = data[pendingWordIdx] + const pendingWordLen = data[pendingWordIdx + 1] + if (pendingWord === undefined || pendingWordLen === undefined) { + throw new Error(`decodeByteArray: missing pending_word data at offset ${pendingWordIdx}`) + } const decoded = byteArray.stringFromByteArray({ data: data.slice(offset + 1, offset + 1 + numFullWords), From 44daec76ff1b9f9bdf22a60a0f912d6161e334c5 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:19:44 -0700 Subject: [PATCH 4/4] chore: add changeset for safe_deposit --- .changeset/safe-deposit-utxo.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/safe-deposit-utxo.md diff --git a/.changeset/safe-deposit-utxo.md b/.changeset/safe-deposit-utxo.md new file mode 100644 index 00000000..9ae7d346 --- /dev/null +++ b/.changeset/safe-deposit-utxo.md @@ -0,0 +1,6 @@ +--- +"@omni-bridge/core": minor +"@omni-bridge/near": minor +--- + +Replace `postActions`/`extraMsg` with `safe_deposit` on `getUtxoDepositAddress`. NEAR builder now calls `safe_verify_deposit` when `safe_deposit` is provided.