Skip to content

Commit 5635df6

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/hypercore-support
# Conflicts: # packages/core/tests/bridge.test.ts
2 parents 4c195a1 + 9f6024d commit 5635df6

10 files changed

Lines changed: 216 additions & 57 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@omni-bridge/core": patch
3+
---
4+
5+
Add destination memo typing and validation for Zcash memo handoff.

docs/core-concepts/omni-addresses.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ enum ChainKind {
105105
Btc = 6,
106106
Zcash = 7,
107107
Pol = 8,
108-
Abs = 9,
109-
Strk = 10
108+
HyperEvm = 9,
109+
Strk = 10,
110+
Abs = 11,
111+
Fogo = 12,
110112
}
111113
```
112114

docs/guides/advanced/manual-finalization.mdx

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,28 @@ const initResult = await toNearKitTransaction(near, initTx).send()
8686
After the transfer is initialized, you must call `signTransfer` to trigger MPC signing:
8787

8888
```typescript
89-
// Extract the transfer ID from the InitTransferEvent in the logs
90-
const initEvent = parseInitTransferEvent(initResult)
89+
import { ChainKind } from "@omni-bridge/core"
90+
import type { InitTransferEvent } from "@omni-bridge/near"
91+
92+
// Parse the InitTransferEvent from NEAR logs
93+
const initEventLog = initResult.receipts_outcome
94+
.flatMap((r) => r.outcome.logs)
95+
.find((log) => log.includes("InitTransferEvent"))
96+
97+
if (!initEventLog) throw new Error("InitTransferEvent not found in logs")
98+
const initEvent: InitTransferEvent = JSON.parse(initEventLog).InitTransferEvent
9199

92100
const signTx = nearBuilder.buildSignTransfer(
93101
{
94-
origin_chain: "Near",
95-
origin_nonce: initEvent.nonce,
102+
origin_chain: ChainKind.Near,
103+
origin_nonce: BigInt(initEvent.transfer_message.origin_nonce),
104+
},
105+
signerId, // fee recipient
106+
{
107+
fee: initEvent.transfer_message.fee.fee,
108+
native_fee: initEvent.transfer_message.fee.native_fee,
96109
},
97-
signerId
110+
signerId,
98111
)
99112

100113
const signResult = await toNearKitTransaction(near, signTx).send()
@@ -105,24 +118,46 @@ const signResult = await toNearKitTransaction(near, signTx).send()
105118
Parse the `SignTransferEvent` from the sign transaction logs and finalize on EVM:
106119

107120
```typescript
108-
import { createEvmBuilder } from "@omni-bridge/evm"
121+
import { createEvmBuilder, type TransferPayload } from "@omni-bridge/evm"
109122
import { ChainKind } from "@omni-bridge/core"
110-
import { MPCSignature } from "@omni-bridge/near"
123+
import { MPCSignature, type SignTransferEvent } from "@omni-bridge/near"
111124

112125
const evm = createEvmBuilder({ network: "mainnet", chain: ChainKind.Eth })
113126

114-
// Parse SignTransferEvent from signResult logs
115-
const signEvent = parseSignTransferEvent(signResult)
127+
// Parse SignTransferEvent from NEAR logs
128+
const signEventLog = signResult.receipts_outcome
129+
.flatMap((r) => r.outcome.logs)
130+
.find((log) => log.includes("SignTransferEvent"))
131+
132+
if (!signEventLog) throw new Error("SignTransferEvent not found in logs")
133+
const signEvent: SignTransferEvent = JSON.parse(signEventLog).SignTransferEvent
134+
135+
// Convert signature to EVM format (adds 27 to recovery_id)
136+
const signature = MPCSignature.fromRaw(signEvent.signature).toBytes(true)
137+
138+
// Convert the NEAR payload (snake_case OmniAddresses) into the EVM TransferPayload
139+
const stripPrefix = (addr: string) => (addr.includes(":") ? addr.split(":")[1] : addr)
140+
let originChain = signEvent.message_payload.transfer_id.origin_chain
141+
if (typeof originChain === "string") {
142+
originChain = ChainKind[originChain as keyof typeof ChainKind]
143+
}
116144

117-
// Convert signature for EVM (adds 27 to recovery ID)
118-
const signature = MPCSignature.fromSignTransferEvent(signEvent).toBytes(true)
145+
const payload: TransferPayload = {
146+
destinationNonce: BigInt(signEvent.message_payload.destination_nonce),
147+
originChain: Number(originChain),
148+
originNonce: BigInt(signEvent.message_payload.transfer_id.origin_nonce),
149+
tokenAddress: stripPrefix(signEvent.message_payload.token_address) as `0x${string}`,
150+
amount: BigInt(signEvent.message_payload.amount),
151+
recipient: stripPrefix(signEvent.message_payload.recipient) as `0x${string}`,
152+
feeRecipient: signEvent.message_payload.fee_recipient ?? "",
153+
}
119154

120-
const tx = evm.buildFinalization(signEvent.message_payload, signature)
155+
const tx = evm.buildFinalization(payload, signature)
121156
await walletClient.sendTransaction(tx)
122157
```
123158

124159
<Note>
125-
The MPC signature needs format conversion for EVM — use `MPCSignature.toBytes(true)` to add 27 to the recovery ID. For Solana, use `toBytes(false)`.
160+
The MPC signature needs format conversion for EVM — use `MPCSignature.fromRaw(raw).toBytes(true)` to add 27 to the recovery ID. For Solana, pass the `MPCSignature` instance directly to the builder.
126161
</Note>
127162

128163
## From EVM to NEAR
@@ -132,24 +167,27 @@ The MPC signature needs format conversion for EVM — use `MPCSignature.toBytes(
132167
Ethereum uses the NEAR light client for verification:
133168

134169
```typescript
135-
import { createNearBuilder, toNearKitTransaction } from "@omni-bridge/near"
136-
import { getEvmProof, ProofKind } from "@omni-bridge/evm"
170+
import { ChainKind } from "@omni-bridge/core"
171+
import { createNearBuilder, ProofKind, toNearKitTransaction } from "@omni-bridge/near"
172+
import { getEvmProof, getInitTransferTopic } from "@omni-bridge/evm"
137173

138174
const nearBuilder = createNearBuilder({ network: "mainnet" })
139175

140176
// Wait for light client to sync (~15-20 min after source tx confirms)
141177

142-
// Generate Merkle proof
143-
const proof = await getEvmProof(txHash, ChainKind.Eth)
178+
// Generate Merkle proof — needs the InitTransfer event topic and the source network
179+
const proof = await getEvmProof(txHash, getInitTransferTopic(), ChainKind.Eth, "mainnet")
144180

145-
// Serialize for NEAR
146-
const proverArgs = nearBuilder.serializeEvmProofArgs({
147-
proof_kind: ProofKind.InitTransfer,
148-
proof,
181+
// Build and send finalization. `buildFinalization` serializes the proof internally.
182+
const tx = nearBuilder.buildFinalization({
183+
sourceChain: ChainKind.Eth,
184+
signerId,
185+
evmProof: {
186+
proof_kind: ProofKind.InitTransfer,
187+
proof,
188+
},
189+
storageDepositActions: [], // add entries for recipient/fee_recipient if needed
149190
})
150-
151-
// Build and send finalization
152-
const tx = nearBuilder.buildFinalization(ChainKind.Eth, proverArgs, signerId)
153191
await toNearKitTransaction(near, tx).send()
154192
```
155193

@@ -163,14 +201,13 @@ import { getWormholeVaa } from "@omni-bridge/core"
163201
// Wait for Wormhole guardians to sign (~1 min)
164202
const vaa = await getWormholeVaa(txSignature, "Mainnet")
165203

166-
// Serialize for NEAR
167-
const proverArgs = nearBuilder.serializeWormholeProofArgs({
168-
proof_kind: ProofKind.InitTransfer,
204+
// Finalize. `buildFinalization` serializes the VAA internally.
205+
const tx = nearBuilder.buildFinalization({
206+
sourceChain: ChainKind.Base,
207+
signerId,
169208
vaa,
209+
storageDepositActions: [],
170210
})
171-
172-
// Finalize
173-
const tx = nearBuilder.buildFinalization(ChainKind.Base, proverArgs, signerId)
174211
await toNearKitTransaction(near, tx).send()
175212
```
176213

@@ -183,12 +220,12 @@ import { getWormholeVaa } from "@omni-bridge/core"
183220

184221
const vaa = await getWormholeVaa(solanaSignature, "Mainnet")
185222

186-
const proverArgs = nearBuilder.serializeWormholeProofArgs({
187-
proof_kind: ProofKind.InitTransfer,
223+
const tx = nearBuilder.buildFinalization({
224+
sourceChain: ChainKind.Sol,
225+
signerId,
188226
vaa,
227+
storageDepositActions: [],
189228
})
190-
191-
const tx = nearBuilder.buildFinalization(ChainKind.Sol, proverArgs, signerId)
192229
await toNearKitTransaction(near, tx).send()
193230
```
194231

@@ -199,7 +236,7 @@ Each destination has a `buildFinalization` method:
199236
| Destination | Method |
200237
|-------------|--------|
201238
| EVM | `evmBuilder.buildFinalization(payload, signature)` |
202-
| NEAR | `nearBuilder.buildFinalization(chain, proverArgs, signerId)` |
239+
| NEAR | `nearBuilder.buildFinalization({ sourceChain, signerId, vaa \| evmProof, storageDepositActions })` |
203240
| Solana | `solanaBuilder.buildFinalization(payload, signature, payer)` |
204241

205242
## Working Examples

docs/guides/advanced/token-deployment.mdx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,17 @@ const metadataEvent = JSON.parse(eventLog).LogMetadataEvent
111111
### Step 2: Deploy Token (EVM)
112112

113113
```typescript
114-
const tx = evm.buildDeployToken(
115-
metadataEvent.signature,
116-
{
117-
token: metadataEvent.metadata_payload.token,
118-
name: metadataEvent.metadata_payload.name,
119-
symbol: metadataEvent.metadata_payload.symbol,
120-
decimals: metadataEvent.metadata_payload.decimals,
121-
}
122-
)
114+
import { MPCSignature } from "@omni-bridge/near"
115+
116+
// Convert the MPC signature to EVM format (adds 27 to recovery_id)
117+
const signature = MPCSignature.fromRaw(metadataEvent.signature).toBytes(true)
118+
119+
const tx = evm.buildDeployToken(signature, {
120+
token: metadataEvent.metadata_payload.token,
121+
name: metadataEvent.metadata_payload.name,
122+
symbol: metadataEvent.metadata_payload.symbol,
123+
decimals: metadataEvent.metadata_payload.decimals,
124+
})
123125

124126
await walletClient.sendTransaction(tx)
125127
```
@@ -167,13 +169,16 @@ Solana doesn't require a separate bind step — the mapping is established durin
167169
## Checking If Already Deployed
168170

169171
```typescript
170-
import { BridgeAPI } from "@omni-bridge/core"
172+
import { createBridge, ChainKind } from "@omni-bridge/core"
171173

172-
const api = new BridgeAPI("mainnet")
173-
const tokenInfo = await api.getTokenInfo("eth:0xTokenAddress...")
174+
const bridge = createBridge({ network: "mainnet" })
175+
const bridgedToken = await bridge.getBridgedToken(
176+
"eth:0xTokenAddress...",
177+
ChainKind.Near,
178+
)
174179

175-
if (tokenInfo.nearTokenId) {
176-
console.log("Already deployed:", tokenInfo.nearTokenId)
180+
if (bridgedToken) {
181+
console.log("Already deployed:", bridgedToken)
177182
} else {
178183
console.log("Needs deployment")
179184
}

docs/guides/near.mdx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,13 @@ console.log("Transfer initiated:", result.transaction.hash)
6464
<Accordion title="Using @near-js/* instead of near-kit">
6565
```typescript
6666
import { createBridge } from "@omni-bridge/core"
67-
import { createNearBuilder, sendWithNearApiJs } from "@omni-bridge/near"
67+
import { createNearBuilder, toNearApiJsActions } from "@omni-bridge/near"
6868
import { Account } from "@near-js/accounts"
6969
import { JsonRpcProvider } from "@near-js/providers"
7070
import { InMemoryKeyStore } from "@near-js/keystores"
7171
import { KeyPair } from "@near-js/crypto"
7272
import { InMemorySigner } from "@near-js/signers"
73+
import type { Action } from "@near-js/transactions"
7374

7475
const bridge = createBridge({ network: "mainnet" })
7576
const nearBuilder = createNearBuilder({ network: "mainnet" })
@@ -82,11 +83,18 @@ const account = new Account({ accountId: "alice.near", provider, signer })
8283

8384
const signerId = "alice.near"
8485

86+
// Helper: convert an SDK unsigned tx and dispatch via near-api-js
87+
const send = (tx: ReturnType<typeof nearBuilder.buildTransfer>) =>
88+
account.signAndSendTransaction({
89+
receiverId: tx.receiverId,
90+
actions: toNearApiJsActions(tx) as Action[],
91+
})
92+
8593
// 1. Storage deposit
8694
const deposit = await nearBuilder.getRequiredStorageDeposit(signerId)
8795
if (deposit > 0n) {
8896
const depositTx = nearBuilder.buildStorageDeposit(signerId, deposit)
89-
await sendWithNearApiJs(account, depositTx)
97+
await send(depositTx)
9098
}
9199

92100
// 2. Validate
@@ -101,7 +109,7 @@ const validated = await bridge.validateTransfer({
101109

102110
// 3. Transfer
103111
const tx = nearBuilder.buildTransfer(validated, signerId)
104-
const result = await sendWithNearApiJs(account, tx)
112+
const result = await send(tx)
105113

106114
console.log("Transfer initiated:", result.transaction.hash)
107115
```
@@ -148,8 +156,7 @@ NEAR transactions need runtime context (nonce, block hash) that the SDK doesn't
148156
| Function | Library | What it does |
149157
|----------|---------|--------------|
150158
| `toNearKitTransaction()` | near-kit | Returns a chainable TransactionBuilder |
151-
| `sendWithNearApiJs()` | @near-js/* | Signs and sends in one call |
152-
| `toNearApiJsActions()` | @near-js/* | Returns raw Actions for manual handling |
159+
| `toNearApiJsActions()` | @near-js/* / near-api-js | Returns plain action objects for `Account.signAndSendTransaction` |
153160

154161
The SDK returns a plain object:
155162

docs/reference/core.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1971,7 +1971,7 @@ async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
19711971
import { ProofError } from "@omni-bridge/core"
19721972

19731973
try {
1974-
const proof = await getEvmProof(txHash, topic, chain)
1974+
const proof = await getEvmProof(txHash, topic, chain, network)
19751975
} catch (error) {
19761976
if (error instanceof ProofError) {
19771977
if (error.code === "PROOF_NOT_READY") {

packages/core/src/bridge.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
import { getAddress, getChain, isEvmChain } from "./utils/address.js"
1919
import { normalizeAmount, validateTransferAmount } from "./utils/decimals.js"
2020

21+
const MAX_ZCASH_MEMO_BYTES = 512
22+
2123
export interface BridgeConfig {
2224
network: Network
2325
rpcUrls?: Partial<Record<ChainKind, string>>
@@ -225,6 +227,28 @@ class BridgeImpl implements Bridge {
225227
}
226228
}
227229

230+
if (params.destinationMemo !== undefined) {
231+
if (destChain !== ChainKind.Zcash) {
232+
throw new ValidationError(
233+
"Destination memo is only supported for Zcash transfers",
234+
"INVALID_MEMO",
235+
{ destChain: ChainKind[destChain], supportedChain: ChainKind[ChainKind.Zcash] },
236+
)
237+
}
238+
239+
const byteLength = new TextEncoder().encode(params.destinationMemo).length
240+
if (byteLength > MAX_ZCASH_MEMO_BYTES) {
241+
throw new ValidationError(
242+
`Destination memo exceeds ${MAX_ZCASH_MEMO_BYTES} bytes`,
243+
"INVALID_MEMO",
244+
{
245+
byteLength,
246+
maxByteLength: MAX_ZCASH_MEMO_BYTES,
247+
},
248+
)
249+
}
250+
}
251+
228252
// Look up bridged token first (needed for NEAR source tokens)
229253
let bridgedToken: OmniAddress | undefined
230254
if (tokenChain !== destChain) {

packages/core/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type ValidationErrorCode =
1717
| "INVALID_AMOUNT"
1818
| "INVALID_ADDRESS"
1919
| "INVALID_CHAIN"
20+
| "INVALID_MEMO"
2021
| "TOKEN_NOT_REGISTERED"
2122
| "DECIMAL_OVERFLOW"
2223
| "AMOUNT_TOO_SMALL"

packages/core/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ export interface TransferParams {
8383
sender: OmniAddress
8484
recipient: OmniAddress
8585
message?: string
86+
/**
87+
* Optional memo to attach on the destination chain. Currently supported for
88+
* Zcash shielded recipients, where memos are limited to 512 bytes.
89+
*/
90+
destinationMemo?: string
8691
}
8792

8893
// Validated transfer (output from validation)

0 commit comments

Comments
 (0)