nara-sdk 1.0.73 → 1.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ---
13
13
 
14
- TypeScript/JavaScript SDK for interacting with the Nara blockchain. Build agents, submit transactions, query accounts, and integrate with on-chain programs.
14
+ TypeScript/JavaScript SDK for interacting with the Nara blockchain. Build agents, submit transactions, query accounts, cross-chain bridge, and integrate with on-chain programs.
15
15
 
16
16
  ## Install
17
17
 
@@ -19,29 +19,162 @@ TypeScript/JavaScript SDK for interacting with the Nara blockchain. Build agents
19
19
  npm install nara-sdk
20
20
  ```
21
21
 
22
+ ## Features
23
+
24
+ - **Agent Registry** — Register agents, bind Twitter, submit tweets, verify, referral system
25
+ - **Quest (PoMI)** — Proof of Machine Intelligence ZK quest system, stake, answer-to-earn
26
+ - **Skills Hub** — On-chain skill registry for AI agents, upload/query skill content
27
+ - **ZK ID** — Zero-knowledge anonymous identity, deposit, withdraw, ownership proofs
28
+ - **Cross-chain Bridge** — Nara ↔ Solana bridge via Hyperlane warp routes (USDC, SOL), with in-tx fee extraction and validator signature tracking
29
+
22
30
  ## Quick Start
23
31
 
24
- ```js
25
- import { Connection, Keypair, Transaction } from 'nara-sdk';
32
+ ```ts
33
+ import { Connection, Keypair } from '@solana/web3.js';
34
+ import { getQuestInfo, submitAnswer, generateProof } from 'nara-sdk';
26
35
 
27
36
  const connection = new Connection('https://mainnet-api.nara.build');
28
- const balance = await connection.getBalance(publicKey);
29
37
  ```
30
38
 
31
- ## Features
39
+ ## Cross-chain Bridge
40
+
41
+ Bridge tokens between Solana and Nara with built-in 0.5% fee extraction.
42
+
43
+ ### One-step bridge
44
+
45
+ ```ts
46
+ import { Connection, Keypair } from '@solana/web3.js';
47
+ import { bridgeTransfer, setAltAddress } from 'nara-sdk';
48
+
49
+ const solanaConn = new Connection('https://api.mainnet-beta.solana.com');
50
+
51
+ // Disable Nara ALT when sending from Solana
52
+ setAltAddress(null);
53
+
54
+ const result = await bridgeTransfer(solanaConn, wallet, {
55
+ token: 'USDC', // 'USDC' | 'SOL'
56
+ fromChain: 'solana', // 'solana' | 'nara'
57
+ recipient: targetPubkey, // destination chain address
58
+ amount: 1_000_000n, // raw units (1 USDC = 1_000_000)
59
+ });
32
60
 
61
+ console.log(result.signature); // source chain tx
62
+ console.log(result.messageId); // cross-chain message ID (0x...)
63
+ console.log(result.feeAmount); // fee deducted
64
+ console.log(result.bridgeAmount); // net amount bridged
33
65
  ```
34
- Transactions Build, sign, and send transactions
35
- Accounts Query balances, token accounts, and program state
36
- Programs Interact with Nara on-chain programs (Agent Registry, PoMI, ZK ID)
37
- Keypairs Generate and manage wallet keypairs
38
- RPC Client Full RPC method coverage
66
+
67
+ ### Build instructions for relay
68
+
69
+ ```ts
70
+ import { makeBridgeIxs } from 'nara-sdk';
71
+
72
+ const { instructions, uniqueMessageKeypair, feeAmount, bridgeAmount } =
73
+ makeBridgeIxs({
74
+ token: 'USDC',
75
+ fromChain: 'solana',
76
+ sender: userPubkey,
77
+ recipient: targetPubkey,
78
+ amount: 1_000_000n,
79
+ });
80
+
81
+ // uniqueMessageKeypair must sign the tx
39
82
  ```
40
83
 
41
- ## CLI
84
+ ### Track cross-chain message
42
85
 
43
- ```bash
44
- npx nara-sdk --help
86
+ ```ts
87
+ import {
88
+ extractMessageId,
89
+ queryMessageSignatures,
90
+ queryMessageStatus,
91
+ } from 'nara-sdk';
92
+
93
+ // 1. Extract message ID from source tx
94
+ const messageId = await extractMessageId(connection, signature);
95
+
96
+ // 2. Query validator signatures (3-way parallel scan on S3)
97
+ const sigs = await queryMessageSignatures(messageId, 'solana');
98
+ console.log(sigs.signedCount, '/', sigs.totalValidators); // e.g. 3/3
99
+ console.log(sigs.fullySigned); // true
100
+
101
+ // 3. Check delivery on destination chain
102
+ const status = await queryMessageStatus(naraConn, messageId, 'nara');
103
+ console.log(status.delivered); // true
104
+ console.log(status.deliverySignature); // destination tx
105
+ ```
106
+
107
+ ### Supported tokens
108
+
109
+ | Token | Solana side | Nara side | Decimals |
110
+ |---|---|---|---|
111
+ | USDC | collateral (lock) | synthetic (mint, Token-2022) | 6 |
112
+ | SOL | native (lamports) | synthetic (mint, Token-2022) | 9 |
113
+
114
+ Add new tokens at runtime:
115
+
116
+ ```ts
117
+ import { registerBridgeToken } from 'nara-sdk';
118
+
119
+ registerBridgeToken('USDT', {
120
+ symbol: 'USDT',
121
+ decimals: 6,
122
+ solana: { warpProgram, mode: 'collateral', mint, tokenProgram },
123
+ nara: { warpProgram, mode: 'synthetic', mint, tokenProgram },
124
+ });
125
+ ```
126
+
127
+ ### Fee configuration
128
+
129
+ Default fee: **0.5%** (50 bps), deducted from the bridged amount on the source chain.
130
+
131
+ ```ts
132
+ import { setBridgeFeeRecipient } from 'nara-sdk';
133
+
134
+ // Override fee recipient at runtime
135
+ setBridgeFeeRecipient('YourFeeRecipientPubkey...');
136
+
137
+ // Or per-call
138
+ await bridgeTransfer(conn, wallet, {
139
+ ...params,
140
+ feeBps: 100, // 1%
141
+ feeRecipient: customPubkey, // override
142
+ skipFee: true, // or skip entirely
143
+ });
144
+ ```
145
+
146
+ ## Agent Registry
147
+
148
+ ```ts
149
+ import {
150
+ registerAgent,
151
+ getAgentRecord,
152
+ setTwitter,
153
+ verifyTwitter,
154
+ submitTweet,
155
+ approveTweet,
156
+ } from 'nara-sdk';
157
+
158
+ // Register an agent
159
+ await registerAgent(connection, wallet, agentId, name, metadataUri);
160
+
161
+ // Twitter verification flow
162
+ await setTwitter(connection, wallet, agentId, handle);
163
+ await verifyTwitter(connection, verifierWallet, agentId);
164
+
165
+ // Tweet submission & approval
166
+ await submitTweet(connection, wallet, agentId, tweetId, tweetUrl);
167
+ await approveTweet(connection, verifierWallet, agentId, tweetId, freeCredits);
168
+ ```
169
+
170
+ ## Quest (PoMI)
171
+
172
+ ```ts
173
+ import { getQuestInfo, generateProof, submitAnswer } from 'nara-sdk';
174
+
175
+ const quest = await getQuestInfo(connection);
176
+ const proof = await generateProof(quest.question, answer);
177
+ const sig = await submitAnswer(connection, wallet, proof);
45
178
  ```
46
179
 
47
180
  ## Documentation
package/index.ts CHANGED
@@ -15,6 +15,9 @@ export {
15
15
  DEFAULT_ZKID_PROGRAM_ID,
16
16
  DEFAULT_AGENT_REGISTRY_PROGRAM_ID,
17
17
  DEFAULT_ALT_ADDRESS,
18
+ DEFAULT_BRIDGE_FEE_BPS,
19
+ DEFAULT_BRIDGE_FEE_RECIPIENT,
20
+ BRIDGE_FEE_BPS_DENOMINATOR,
18
21
  } from "./src/constants";
19
22
 
20
23
  // Export signing utilities
@@ -165,6 +168,52 @@ export {
165
168
  type AgentRegistryOptions,
166
169
  } from "./src/agent_registry";
167
170
 
171
+ // Export bridge functions and types
172
+ export {
173
+ // Constants
174
+ SOLANA_DOMAIN,
175
+ NARA_DOMAIN,
176
+ SOLANA_MAILBOX,
177
+ NARA_MAILBOX,
178
+ SPL_NOOP,
179
+ BRIDGE_TOKENS,
180
+ // Token registry
181
+ registerBridgeToken,
182
+ // Fee recipient runtime override
183
+ setBridgeFeeRecipient,
184
+ getBridgeFeeRecipient,
185
+ // PDA helpers
186
+ deriveOutboxPda,
187
+ deriveDispatchAuthorityPda,
188
+ deriveDispatchedMessagePda,
189
+ deriveTokenPda,
190
+ deriveEscrowPda,
191
+ deriveNativeCollateralPda,
192
+ // Encoders
193
+ encodeTransferRemote,
194
+ // Fee + ix builders
195
+ calculateBridgeFee,
196
+ makeBridgeFeeIxs,
197
+ makeTransferRemoteIx,
198
+ makeBridgeIxs,
199
+ // High level
200
+ bridgeTransfer,
201
+ extractMessageId,
202
+ queryMessageStatus,
203
+ queryMessageSignatures,
204
+ type BridgeChain,
205
+ type BridgeMode,
206
+ type BridgeTokenSide,
207
+ type BridgeTokenConfig,
208
+ type BridgeTransferParams,
209
+ type BridgeIxsResult,
210
+ type BridgeTransferResult,
211
+ type FeeSplit,
212
+ type MessageStatus,
213
+ type ValidatorSignature,
214
+ type MessageSignatureStatus,
215
+ } from "./src/bridge";
216
+
168
217
  // Export IDLs and types
169
218
  export { default as NaraQuestIDL } from "./src/idls/nara_quest.json";
170
219
  export { default as NaraSkillsHubIDL } from "./src/idls/nara_skills_hub.json";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nara-sdk",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "SDK for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
@@ -259,7 +259,6 @@ function parseAgentRecordData(data: Buffer | Uint8Array): AgentRecord {
259
259
  : null;
260
260
  offset += 32; // referral_id fixed array
261
261
 
262
- offset += 4; // _padding
263
262
  const referralCount = buf.readUInt32LE(offset);
264
263
 
265
264
  return {
package/src/bridge.ts ADDED
@@ -0,0 +1,780 @@
1
+ /**
2
+ * Cross-chain bridge SDK between Nara and Solana via Hyperlane warp routes.
3
+ *
4
+ * Supports two assets out of the box (USDC, SOL) in both directions, with
5
+ * an in-tx fee deduction (default 0.5%) sent to BRIDGE_FEE_RECIPIENT.
6
+ *
7
+ * Adding a new asset:
8
+ * 1. Append a new entry to BRIDGE_TOKENS with {symbol, decimals, solana, nara}.
9
+ * 2. The PDA derivation, account-list construction, and instruction encoding
10
+ * are mode-driven (collateral / synthetic / native), no per-token code paths.
11
+ */
12
+
13
+ import {
14
+ Connection,
15
+ Keypair,
16
+ PublicKey,
17
+ SystemProgram,
18
+ TransactionInstruction,
19
+ } from "@solana/web3.js";
20
+ import {
21
+ createAssociatedTokenAccountIdempotentInstruction,
22
+ createTransferCheckedInstruction,
23
+ getAssociatedTokenAddressSync,
24
+ TOKEN_2022_PROGRAM_ID,
25
+ TOKEN_PROGRAM_ID,
26
+ } from "@solana/spl-token";
27
+ import {
28
+ BRIDGE_FEE_BPS_DENOMINATOR,
29
+ DEFAULT_BRIDGE_FEE_BPS,
30
+ DEFAULT_BRIDGE_FEE_RECIPIENT,
31
+ } from "./constants";
32
+ import { sendTx } from "./tx";
33
+
34
+ // ─── Types ────────────────────────────────────────────────────────
35
+
36
+ export type BridgeChain = "solana" | "nara";
37
+ export type BridgeMode = "collateral" | "synthetic" | "native";
38
+
39
+ export interface BridgeTokenSide {
40
+ warpProgram: PublicKey;
41
+ mode: BridgeMode;
42
+ /** SPL mint pubkey. null for Solana native SOL. */
43
+ mint: PublicKey | null;
44
+ /** SPL token program. null for Solana native SOL. */
45
+ tokenProgram: PublicKey | null;
46
+ }
47
+
48
+ export interface BridgeTokenConfig {
49
+ symbol: string;
50
+ decimals: number;
51
+ solana: BridgeTokenSide;
52
+ nara: BridgeTokenSide;
53
+ }
54
+
55
+ export interface BridgeTransferParams {
56
+ /** Token symbol from BRIDGE_TOKENS (e.g. "USDC", "SOL") */
57
+ token: string;
58
+ /** Source chain — fee is deducted on this chain in the source token */
59
+ fromChain: BridgeChain;
60
+ /** Sender pubkey on the source chain */
61
+ sender: PublicKey;
62
+ /** Recipient pubkey on the destination chain */
63
+ recipient: PublicKey;
64
+ /** Gross amount (raw units, source-chain decimals). Fee is deducted from this. */
65
+ amount: bigint;
66
+ /** Optional fee bps override (defaults to BRIDGE_FEE_BPS) */
67
+ feeBps?: number;
68
+ /** Optional fee recipient override (defaults to BRIDGE_FEE_RECIPIENT) */
69
+ feeRecipient?: PublicKey;
70
+ /** Skip fee deduction entirely */
71
+ skipFee?: boolean;
72
+ }
73
+
74
+ export interface BridgeIxsResult {
75
+ /** All instructions in order: [feeIx?, atapayerIx?, transferRemoteIx] */
76
+ instructions: TransactionInstruction[];
77
+ /** Extra signer required by the transferRemote ix (the unique message keypair) */
78
+ uniqueMessageKeypair: Keypair;
79
+ /** Fee deducted in source token raw units */
80
+ feeAmount: bigint;
81
+ /** Net amount actually bridged */
82
+ bridgeAmount: bigint;
83
+ }
84
+
85
+ export interface BridgeTransferResult {
86
+ signature: string;
87
+ messageId: string | null;
88
+ feeAmount: bigint;
89
+ bridgeAmount: bigint;
90
+ }
91
+
92
+ // ─── Chain constants ───────────────────────────────────────────────
93
+
94
+ export const SOLANA_DOMAIN = 1399811149;
95
+ export const NARA_DOMAIN = 40778959;
96
+
97
+ export const SOLANA_MAILBOX = new PublicKey(
98
+ "E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi"
99
+ );
100
+ export const NARA_MAILBOX = new PublicKey(
101
+ "EjtLD3MCBJregFKAce2pQqPtSnnmBWK5oAZ3wBifHnaH"
102
+ );
103
+
104
+ export const SPL_NOOP = new PublicKey(
105
+ "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"
106
+ );
107
+
108
+ function mailboxFor(chain: BridgeChain): PublicKey {
109
+ return chain === "solana" ? SOLANA_MAILBOX : NARA_MAILBOX;
110
+ }
111
+
112
+ function destinationDomainFor(toChain: BridgeChain): number {
113
+ return toChain === "solana" ? SOLANA_DOMAIN : NARA_DOMAIN;
114
+ }
115
+
116
+ // ─── Token registry ───────────────────────────────────────────────
117
+
118
+ export const BRIDGE_TOKENS: Record<string, BridgeTokenConfig> = {
119
+ USDC: {
120
+ symbol: "USDC",
121
+ decimals: 6,
122
+ solana: {
123
+ warpProgram: new PublicKey("4GcZJTa8s9vxtTz97Vj1RrwKMqPkT3DiiJkvUQDwsuZP"),
124
+ mode: "collateral",
125
+ mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
126
+ tokenProgram: TOKEN_PROGRAM_ID,
127
+ },
128
+ nara: {
129
+ warpProgram: new PublicKey("BC2j6WrdPs9xhU9CfBwJsYSnJrGq5Tcm4SEen9ENv7go"),
130
+ mode: "synthetic",
131
+ mint: new PublicKey("8P7UGWjq86N3WUmwEgKeGHJZLcoMJqr5jnRUmeBN7YwR"),
132
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
133
+ },
134
+ },
135
+ SOL: {
136
+ symbol: "SOL",
137
+ decimals: 9,
138
+ solana: {
139
+ warpProgram: new PublicKey("46MmAWwKRAt9uvn7m44NXbVq2DCWBQE2r1TDw25nyXrt"),
140
+ mode: "native",
141
+ mint: null,
142
+ tokenProgram: null,
143
+ },
144
+ nara: {
145
+ warpProgram: new PublicKey("6bKmjEMbjcJUnqAiNw7AXuMvUALzw5XRKiV9dBsterxg"),
146
+ mode: "synthetic",
147
+ mint: new PublicKey("7fKh7DqPZmsYPHdGvt9Qw2rZkSEGp9F5dBa3XuuuhavU"),
148
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
149
+ },
150
+ },
151
+ };
152
+
153
+ /** Register a new bridge token at runtime */
154
+ export function registerBridgeToken(symbol: string, config: BridgeTokenConfig): void {
155
+ BRIDGE_TOKENS[symbol] = config;
156
+ }
157
+
158
+ function getToken(symbol: string): BridgeTokenConfig {
159
+ const t = BRIDGE_TOKENS[symbol];
160
+ if (!t) throw new Error(`Unknown bridge token: ${symbol}`);
161
+ return t;
162
+ }
163
+
164
+ // ─── Fee recipient (runtime override) ─────────────────────────────
165
+
166
+ let _feeRecipientOverride: PublicKey | null = null;
167
+
168
+ /** Override the bridge fee recipient at runtime */
169
+ export function setBridgeFeeRecipient(recipient: PublicKey | string | null): void {
170
+ if (recipient === null) {
171
+ _feeRecipientOverride = null;
172
+ return;
173
+ }
174
+ _feeRecipientOverride =
175
+ typeof recipient === "string" ? new PublicKey(recipient) : recipient;
176
+ }
177
+
178
+ export function getBridgeFeeRecipient(): PublicKey {
179
+ if (_feeRecipientOverride) return _feeRecipientOverride;
180
+ return new PublicKey(DEFAULT_BRIDGE_FEE_RECIPIENT);
181
+ }
182
+
183
+ // ─── PDA derivation ───────────────────────────────────────────────
184
+
185
+ export function deriveOutboxPda(mailbox: PublicKey): PublicKey {
186
+ const [pda] = PublicKey.findProgramAddressSync(
187
+ [Buffer.from("hyperlane"), Buffer.from("-"), Buffer.from("outbox")],
188
+ mailbox
189
+ );
190
+ return pda;
191
+ }
192
+
193
+ export function deriveDispatchAuthorityPda(warpProgram: PublicKey): PublicKey {
194
+ const [pda] = PublicKey.findProgramAddressSync(
195
+ [
196
+ Buffer.from("hyperlane_dispatcher"),
197
+ Buffer.from("-"),
198
+ Buffer.from("dispatch_authority"),
199
+ ],
200
+ warpProgram
201
+ );
202
+ return pda;
203
+ }
204
+
205
+ export function deriveDispatchedMessagePda(
206
+ mailbox: PublicKey,
207
+ uniqueMessagePubkey: PublicKey
208
+ ): PublicKey {
209
+ const [pda] = PublicKey.findProgramAddressSync(
210
+ [
211
+ Buffer.from("hyperlane"),
212
+ Buffer.from("-"),
213
+ Buffer.from("dispatched_message"),
214
+ Buffer.from("-"),
215
+ uniqueMessagePubkey.toBuffer(),
216
+ ],
217
+ mailbox
218
+ );
219
+ return pda;
220
+ }
221
+
222
+ export function deriveTokenPda(warpProgram: PublicKey): PublicKey {
223
+ const [pda] = PublicKey.findProgramAddressSync(
224
+ [
225
+ Buffer.from("hyperlane_message_recipient"),
226
+ Buffer.from("-"),
227
+ Buffer.from("handle"),
228
+ Buffer.from("-"),
229
+ Buffer.from("account_metas"),
230
+ ],
231
+ warpProgram
232
+ );
233
+ return pda;
234
+ }
235
+
236
+ export function deriveEscrowPda(warpProgram: PublicKey): PublicKey {
237
+ const [pda] = PublicKey.findProgramAddressSync(
238
+ [
239
+ Buffer.from("hyperlane_token"),
240
+ Buffer.from("-"),
241
+ Buffer.from("escrow"),
242
+ ],
243
+ warpProgram
244
+ );
245
+ return pda;
246
+ }
247
+
248
+ export function deriveNativeCollateralPda(warpProgram: PublicKey): PublicKey {
249
+ const [pda] = PublicKey.findProgramAddressSync(
250
+ [
251
+ Buffer.from("hyperlane_token"),
252
+ Buffer.from("-"),
253
+ Buffer.from("native_collateral"),
254
+ ],
255
+ warpProgram
256
+ );
257
+ return pda;
258
+ }
259
+
260
+ // ─── Instruction data encoder ─────────────────────────────────────
261
+
262
+ /**
263
+ * Encodes a TransferRemote instruction body.
264
+ * Layout:
265
+ * [9 bytes prefix 0x01*9] [4 bytes destinationDomain u32 LE]
266
+ * [32 bytes recipient] [32 bytes amount as U256 LE]
267
+ */
268
+ export function encodeTransferRemote(
269
+ destinationDomain: number,
270
+ recipient: PublicKey,
271
+ amount: bigint
272
+ ): Buffer {
273
+ const buf = Buffer.alloc(9 + 4 + 32 + 32);
274
+ for (let i = 0; i < 9; i++) buf[i] = 0x01;
275
+ buf.writeUInt32LE(destinationDomain, 9);
276
+ recipient.toBuffer().copy(buf, 13);
277
+ buf.writeBigUInt64LE(amount, 45);
278
+ return buf;
279
+ }
280
+
281
+ // ─── Fee calculation ──────────────────────────────────────────────
282
+
283
+ export interface FeeSplit {
284
+ feeAmount: bigint;
285
+ bridgeAmount: bigint;
286
+ feeBps: number;
287
+ }
288
+
289
+ export function calculateBridgeFee(amount: bigint, feeBps?: number): FeeSplit {
290
+ const bps = feeBps ?? DEFAULT_BRIDGE_FEE_BPS;
291
+ if (bps < 0 || bps > BRIDGE_FEE_BPS_DENOMINATOR) {
292
+ throw new Error(`Invalid feeBps: ${bps}`);
293
+ }
294
+ const feeAmount = (amount * BigInt(bps)) / BigInt(BRIDGE_FEE_BPS_DENOMINATOR);
295
+ const bridgeAmount = amount - feeAmount;
296
+ return { feeAmount, bridgeAmount, feeBps: bps };
297
+ }
298
+
299
+ // ─── Fee instruction builder ──────────────────────────────────────
300
+
301
+ /**
302
+ * Build the fee-collection instructions on the source chain in the source token.
303
+ *
304
+ * For SPL tokens: returns [createIdempotentATA(feeRecipient), transferChecked].
305
+ * For native SOL on Solana: returns [SystemProgram.transfer].
306
+ *
307
+ * Returns empty array if feeAmount == 0.
308
+ */
309
+ export function makeBridgeFeeIxs(params: {
310
+ token: string;
311
+ fromChain: BridgeChain;
312
+ sender: PublicKey;
313
+ feeRecipient: PublicKey;
314
+ feeAmount: bigint;
315
+ }): TransactionInstruction[] {
316
+ const { token, fromChain, sender, feeRecipient, feeAmount } = params;
317
+ if (feeAmount === 0n) return [];
318
+
319
+ const tokenCfg = getToken(token);
320
+ const side = tokenCfg[fromChain];
321
+
322
+ // Native SOL → SystemProgram.transfer
323
+ if (side.mode === "native") {
324
+ return [
325
+ SystemProgram.transfer({
326
+ fromPubkey: sender,
327
+ toPubkey: feeRecipient,
328
+ lamports: feeAmount,
329
+ }),
330
+ ];
331
+ }
332
+
333
+ // SPL token (collateral or synthetic) → ensure recipient ATA + transferChecked
334
+ if (!side.mint || !side.tokenProgram) {
335
+ throw new Error(`Token ${token} on ${fromChain} missing mint/tokenProgram`);
336
+ }
337
+
338
+ const senderAta = getAssociatedTokenAddressSync(
339
+ side.mint,
340
+ sender,
341
+ false,
342
+ side.tokenProgram
343
+ );
344
+ const recipientAta = getAssociatedTokenAddressSync(
345
+ side.mint,
346
+ feeRecipient,
347
+ true, // allowOwnerOffCurve — fee recipient may be a PDA
348
+ side.tokenProgram
349
+ );
350
+
351
+ return [
352
+ createAssociatedTokenAccountIdempotentInstruction(
353
+ sender,
354
+ recipientAta,
355
+ feeRecipient,
356
+ side.mint,
357
+ side.tokenProgram
358
+ ),
359
+ createTransferCheckedInstruction(
360
+ senderAta,
361
+ side.mint,
362
+ recipientAta,
363
+ sender,
364
+ feeAmount,
365
+ tokenCfg.decimals,
366
+ [],
367
+ side.tokenProgram
368
+ ),
369
+ ];
370
+ }
371
+
372
+ // ─── TransferRemote instruction builder ───────────────────────────
373
+
374
+ /**
375
+ * Build the warp route TransferRemote instruction for a given token+direction.
376
+ * The unique message keypair must be passed in (it's a required tx signer).
377
+ */
378
+ export function makeTransferRemoteIx(params: {
379
+ token: string;
380
+ fromChain: BridgeChain;
381
+ sender: PublicKey;
382
+ recipient: PublicKey;
383
+ amount: bigint;
384
+ uniqueMessageKeypair: Keypair;
385
+ }): TransactionInstruction {
386
+ const { token, fromChain, sender, recipient, amount, uniqueMessageKeypair } =
387
+ params;
388
+
389
+ const tokenCfg = getToken(token);
390
+ const side = tokenCfg[fromChain];
391
+ const toChain: BridgeChain = fromChain === "solana" ? "nara" : "solana";
392
+
393
+ const mailbox = mailboxFor(fromChain);
394
+ const tokenPda = deriveTokenPda(side.warpProgram);
395
+ const dispatchAuthPda = deriveDispatchAuthorityPda(side.warpProgram);
396
+ const outboxPda = deriveOutboxPda(mailbox);
397
+ const dispatchedMsgPda = deriveDispatchedMessagePda(
398
+ mailbox,
399
+ uniqueMessageKeypair.publicKey
400
+ );
401
+
402
+ // First 9 accounts are common to all modes
403
+ const keys = [
404
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
405
+ { pubkey: SPL_NOOP, isSigner: false, isWritable: false },
406
+ { pubkey: tokenPda, isSigner: false, isWritable: false },
407
+ { pubkey: mailbox, isSigner: false, isWritable: false },
408
+ { pubkey: outboxPda, isSigner: false, isWritable: true },
409
+ { pubkey: dispatchAuthPda, isSigner: false, isWritable: false },
410
+ { pubkey: sender, isSigner: true, isWritable: true },
411
+ { pubkey: uniqueMessageKeypair.publicKey, isSigner: true, isWritable: false },
412
+ { pubkey: dispatchedMsgPda, isSigner: false, isWritable: true },
413
+ ];
414
+
415
+ // Plugin-specific accounts
416
+ if (side.mode === "collateral") {
417
+ if (!side.mint || !side.tokenProgram) {
418
+ throw new Error(`Collateral mode requires mint+tokenProgram for ${token}`);
419
+ }
420
+ const senderAta = getAssociatedTokenAddressSync(
421
+ side.mint,
422
+ sender,
423
+ false,
424
+ side.tokenProgram
425
+ );
426
+ const escrowPda = deriveEscrowPda(side.warpProgram);
427
+ keys.push(
428
+ { pubkey: side.tokenProgram, isSigner: false, isWritable: false },
429
+ { pubkey: side.mint, isSigner: false, isWritable: true },
430
+ { pubkey: senderAta, isSigner: false, isWritable: true },
431
+ { pubkey: escrowPda, isSigner: false, isWritable: true }
432
+ );
433
+ } else if (side.mode === "synthetic") {
434
+ if (!side.mint || !side.tokenProgram) {
435
+ throw new Error(`Synthetic mode requires mint+tokenProgram for ${token}`);
436
+ }
437
+ const senderAta = getAssociatedTokenAddressSync(
438
+ side.mint,
439
+ sender,
440
+ false,
441
+ side.tokenProgram
442
+ );
443
+ keys.push(
444
+ { pubkey: side.tokenProgram, isSigner: false, isWritable: false },
445
+ { pubkey: side.mint, isSigner: false, isWritable: true },
446
+ { pubkey: senderAta, isSigner: false, isWritable: true }
447
+ );
448
+ } else if (side.mode === "native") {
449
+ const nativeCollateralPda = deriveNativeCollateralPda(side.warpProgram);
450
+ keys.push(
451
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
452
+ { pubkey: nativeCollateralPda, isSigner: false, isWritable: true }
453
+ );
454
+ }
455
+
456
+ const data = encodeTransferRemote(destinationDomainFor(toChain), recipient, amount);
457
+
458
+ return new TransactionInstruction({
459
+ programId: side.warpProgram,
460
+ keys,
461
+ data,
462
+ });
463
+ }
464
+
465
+ // ─── High-level: build full bridge instruction set ────────────────
466
+
467
+ /**
468
+ * Build all instructions needed to bridge tokens cross-chain with in-tx fee.
469
+ * Returns the instructions, the unique-message keypair (must sign the tx),
470
+ * and the fee/bridge amounts.
471
+ */
472
+ export function makeBridgeIxs(params: BridgeTransferParams): BridgeIxsResult {
473
+ const {
474
+ token,
475
+ fromChain,
476
+ sender,
477
+ recipient,
478
+ amount,
479
+ feeBps,
480
+ feeRecipient,
481
+ skipFee,
482
+ } = params;
483
+
484
+ if (amount <= 0n) throw new Error("amount must be > 0");
485
+
486
+ const split = skipFee
487
+ ? { feeAmount: 0n, bridgeAmount: amount, feeBps: 0 }
488
+ : calculateBridgeFee(amount, feeBps);
489
+
490
+ if (split.bridgeAmount <= 0n) {
491
+ throw new Error("bridge amount after fee is zero — increase amount or lower feeBps");
492
+ }
493
+
494
+ const recipientForFee = feeRecipient ?? getBridgeFeeRecipient();
495
+ const feeIxs = makeBridgeFeeIxs({
496
+ token,
497
+ fromChain,
498
+ sender,
499
+ feeRecipient: recipientForFee,
500
+ feeAmount: split.feeAmount,
501
+ });
502
+
503
+ const uniqueMessageKeypair = Keypair.generate();
504
+ const transferIx = makeTransferRemoteIx({
505
+ token,
506
+ fromChain,
507
+ sender,
508
+ recipient,
509
+ amount: split.bridgeAmount,
510
+ uniqueMessageKeypair,
511
+ });
512
+
513
+ return {
514
+ instructions: [...feeIxs, transferIx],
515
+ uniqueMessageKeypair,
516
+ feeAmount: split.feeAmount,
517
+ bridgeAmount: split.bridgeAmount,
518
+ };
519
+ }
520
+
521
+ // ─── Send + confirm ───────────────────────────────────────────────
522
+
523
+ /**
524
+ * Build, sign, and send a bridge transfer in one call.
525
+ * `wallet` is the sender keypair on the source chain and pays fees + signs.
526
+ *
527
+ * `connection` must point to the SOURCE chain RPC:
528
+ * - fromChain: "solana" → Solana mainnet RPC
529
+ * - fromChain: "nara" → Nara mainnet RPC
530
+ */
531
+ export async function bridgeTransfer(
532
+ connection: Connection,
533
+ wallet: Keypair,
534
+ params: Omit<BridgeTransferParams, "sender">,
535
+ opts?: { skipPreflight?: boolean; computeUnitLimit?: number; computeUnitPrice?: number | "auto" }
536
+ ): Promise<BridgeTransferResult> {
537
+ const built = makeBridgeIxs({
538
+ ...params,
539
+ sender: wallet.publicKey,
540
+ });
541
+
542
+ const signature = await sendTx(
543
+ connection,
544
+ wallet,
545
+ built.instructions,
546
+ [built.uniqueMessageKeypair],
547
+ {
548
+ computeUnitLimit: opts?.computeUnitLimit ?? 1_400_000,
549
+ computeUnitPrice: opts?.computeUnitPrice,
550
+ skipPreflight: opts?.skipPreflight,
551
+ }
552
+ );
553
+
554
+ // Try to extract message_id from logs
555
+ const messageId = await extractMessageId(connection, signature);
556
+
557
+ return {
558
+ signature,
559
+ messageId,
560
+ feeAmount: built.feeAmount,
561
+ bridgeAmount: built.bridgeAmount,
562
+ };
563
+ }
564
+
565
+ // ─── Message ID extraction ────────────────────────────────────────
566
+
567
+ /**
568
+ * Extract the cross-chain message_id from a dispatch tx's logs.
569
+ * Returns null if the log is missing (tx not yet visible to RPC, etc.).
570
+ */
571
+ export async function extractMessageId(
572
+ connection: Connection,
573
+ signature: string
574
+ ): Promise<string | null> {
575
+ const tx = await connection.getTransaction(signature, {
576
+ maxSupportedTransactionVersion: 0,
577
+ commitment: "confirmed",
578
+ });
579
+ if (!tx?.meta?.logMessages) return null;
580
+ for (const log of tx.meta.logMessages) {
581
+ const m = log.match(/Dispatched message to \d+, ID (0x[0-9a-fA-F]+)/);
582
+ if (m && m[1]) return m[1];
583
+ }
584
+ return null;
585
+ }
586
+
587
+ // ─── Message delivery status ──────────────────────────────────────
588
+
589
+ export interface MessageStatus {
590
+ /** Whether the message has been processed on the destination chain */
591
+ delivered: boolean;
592
+ /** Destination chain tx signature (null if not yet delivered) */
593
+ deliverySignature: string | null;
594
+ }
595
+
596
+ /**
597
+ * Query whether a cross-chain message has been delivered on the destination chain.
598
+ *
599
+ * Scans recent transactions on the destination Mailbox for the message_id.
600
+ * The Hyperlane inbox emits: "Hyperlane inbox processed message 0x..."
601
+ *
602
+ * @param destConnection - connection to the DESTINATION chain RPC
603
+ * @param messageId - the 0x-prefixed message ID from extractMessageId()
604
+ * @param toChain - "solana" | "nara" (which chain to check)
605
+ * @param opts.limit - how many recent Mailbox txs to scan (default 50)
606
+ */
607
+ export async function queryMessageStatus(
608
+ destConnection: Connection,
609
+ messageId: string,
610
+ toChain: BridgeChain,
611
+ opts?: { limit?: number }
612
+ ): Promise<MessageStatus> {
613
+ const mailbox = mailboxFor(toChain);
614
+ const limit = opts?.limit ?? 50;
615
+
616
+ const sigs = await destConnection.getSignaturesForAddress(mailbox, {
617
+ limit,
618
+ });
619
+
620
+ for (const entry of sigs) {
621
+ if (entry.err) continue;
622
+ const tx = await destConnection.getTransaction(entry.signature, {
623
+ maxSupportedTransactionVersion: 0,
624
+ commitment: "confirmed",
625
+ });
626
+ if (!tx?.meta?.logMessages) continue;
627
+ for (const log of tx.meta.logMessages) {
628
+ if (log.includes(messageId)) {
629
+ return { delivered: true, deliverySignature: entry.signature };
630
+ }
631
+ }
632
+ }
633
+
634
+ return { delivered: false, deliverySignature: null };
635
+ }
636
+
637
+ // ─── Validator signature status (S3) ──────────────────────────────
638
+
639
+ const S3_BASE = "https://nara-hyperlane.s3.us-west-1.amazonaws.com";
640
+
641
+ /** Validator S3 prefixes per source chain */
642
+ const VALIDATOR_PREFIXES: Record<BridgeChain, string[]> = {
643
+ solana: ["validator-solana-1", "validator-solana-2", "validator-solana-3"],
644
+ nara: ["validator-nara-1", "validator-nara-2", "validator-nara-3"],
645
+ };
646
+
647
+ export interface ValidatorSignature {
648
+ folder: string;
649
+ signed: boolean;
650
+ latestIndex: number;
651
+ serializedSignature: string | null;
652
+ }
653
+
654
+ export interface MessageSignatureStatus {
655
+ messageId: string;
656
+ /** Merkle tree checkpoint index for this message (null if not found) */
657
+ checkpointIndex: number | null;
658
+ sourceChain: BridgeChain;
659
+ validators: ValidatorSignature[];
660
+ signedCount: number;
661
+ totalValidators: number;
662
+ /** All validators have signed */
663
+ fullySigned: boolean;
664
+ }
665
+
666
+ async function s3Json<T = unknown>(key: string): Promise<T | null> {
667
+ try {
668
+ const resp = await fetch(`${S3_BASE}/${key}`);
669
+ if (!resp.ok) return null;
670
+ return (await resp.json()) as T;
671
+ } catch {
672
+ return null;
673
+ }
674
+ }
675
+
676
+ interface S3Checkpoint {
677
+ value: {
678
+ checkpoint: {
679
+ merkle_tree_hook_address: string;
680
+ mailbox_domain: number;
681
+ root: string;
682
+ index: number;
683
+ };
684
+ message_id: string;
685
+ };
686
+ signature: { r: string; s: string; v: number };
687
+ serialized_signature: string;
688
+ }
689
+
690
+ /**
691
+ * Query Hyperlane validator signatures for a cross-chain message from AWS S3.
692
+ *
693
+ * Each validator stores signed merkle checkpoints in S3. Each checkpoint
694
+ * at index N contains the message_id of the Nth dispatched message and the
695
+ * validator's ECDSA signature over the merkle root.
696
+ *
697
+ * All validators are scanned concurrently (one goroutine per validator).
698
+ *
699
+ * @param messageId - 0x-prefixed message ID from extractMessageId()
700
+ * @param sourceChain - source chain where the message was dispatched
701
+ * @param opts.maxScan - how many checkpoints to scan backwards (default 200)
702
+ */
703
+ export async function queryMessageSignatures(
704
+ messageId: string,
705
+ sourceChain: BridgeChain,
706
+ opts?: { maxScan?: number }
707
+ ): Promise<MessageSignatureStatus> {
708
+ const prefixes = VALIDATOR_PREFIXES[sourceChain];
709
+ const maxScan = opts?.maxScan ?? 200;
710
+
711
+ // Each validator independently: fetch latest index → reverse-scan for messageId
712
+ const results = await Promise.all(
713
+ prefixes.map((folder) => scanValidator(folder, messageId, maxScan))
714
+ );
715
+
716
+ // checkpointIndex comes from whichever validator found it
717
+ const found = results.find((r) => r.checkpointIndex !== null);
718
+ const checkpointIndex = found?.checkpointIndex ?? null;
719
+
720
+ const validators: ValidatorSignature[] = results.map((r) => ({
721
+ folder: r.folder,
722
+ signed: r.signed,
723
+ latestIndex: r.latestIndex,
724
+ serializedSignature: r.serializedSignature,
725
+ }));
726
+
727
+ const signedCount = validators.filter((v) => v.signed).length;
728
+ return {
729
+ messageId,
730
+ checkpointIndex,
731
+ sourceChain,
732
+ validators,
733
+ signedCount,
734
+ totalValidators: prefixes.length,
735
+ fullySigned: signedCount === prefixes.length,
736
+ };
737
+ }
738
+
739
+ async function scanValidator(
740
+ folder: string,
741
+ messageId: string,
742
+ maxScan: number
743
+ ): Promise<ValidatorSignature & { checkpointIndex: number | null }> {
744
+ const latestIndex = await s3Json<number>(
745
+ `${folder}/checkpoint_latest_index.json`
746
+ );
747
+ if (latestIndex === null) {
748
+ return {
749
+ folder,
750
+ signed: false,
751
+ latestIndex: -1,
752
+ serializedSignature: null,
753
+ checkpointIndex: null,
754
+ };
755
+ }
756
+
757
+ const minIndex = Math.max(0, latestIndex - maxScan);
758
+ for (let i = latestIndex; i >= minIndex; i--) {
759
+ const cp = await s3Json<S3Checkpoint>(
760
+ `${folder}/checkpoint_${i}_with_id.json`
761
+ );
762
+ if (cp?.value?.message_id === messageId) {
763
+ return {
764
+ folder,
765
+ signed: true,
766
+ latestIndex,
767
+ serializedSignature: cp.serialized_signature,
768
+ checkpointIndex: i,
769
+ };
770
+ }
771
+ }
772
+
773
+ return {
774
+ folder,
775
+ signed: false,
776
+ latestIndex,
777
+ serializedSignature: null,
778
+ checkpointIndex: null,
779
+ };
780
+ }
package/src/constants.ts CHANGED
@@ -45,3 +45,23 @@ export const DEFAULT_AGENT_REGISTRY_PROGRAM_ID =
45
45
  * When empty, uses legacy transactions.
46
46
  */
47
47
  export const DEFAULT_ALT_ADDRESS = process.env.ALT_ADDRESS || "3uw7RatGTB4hdHnuVLXjsqcMZ87zXsMSc3XbyoPA8mB7";
48
+
49
+ /**
50
+ * Bridge fee in basis points (1 bps = 0.01%). 50 = 0.5%.
51
+ * Deducted from the bridged amount and transferred to DEFAULT_BRIDGE_FEE_RECIPIENT
52
+ * in the same transaction.
53
+ */
54
+ export const DEFAULT_BRIDGE_FEE_BPS = 50;
55
+
56
+ /** BPS denominator (10000 = 100%) */
57
+ export const BRIDGE_FEE_BPS_DENOMINATOR = 10000;
58
+
59
+ /**
60
+ * Default fee recipient pubkey for cross-chain bridge transactions.
61
+ * Same Ed25519 keypair works on both Solana and Nara chains.
62
+ * Override at runtime via setBridgeFeeRecipient() in src/bridge.ts.
63
+ *
64
+ * NOTE: replace with actual fee recipient before mainnet usage.
65
+ */
66
+ export const DEFAULT_BRIDGE_FEE_RECIPIENT =
67
+ "FERLFwBpCyoEuvFP68eP6Fv4FCVocnNyyFUCYwpfmjqn";
@@ -4861,10 +4861,6 @@
4861
4861
  ]
4862
4862
  }
4863
4863
  },
4864
- {
4865
- "name": "_padding",
4866
- "type": "u32"
4867
- },
4868
4864
  {
4869
4865
  "name": "referral_count",
4870
4866
  "type": "u32"
@@ -4874,25 +4870,7 @@
4874
4870
  "type": {
4875
4871
  "array": [
4876
4872
  "u8",
4877
- 32
4878
- ]
4879
- }
4880
- },
4881
- {
4882
- "name": "_reserved2",
4883
- "type": {
4884
- "array": [
4885
- "u8",
4886
- 16
4887
- ]
4888
- }
4889
- },
4890
- {
4891
- "name": "_reserved3",
4892
- "type": {
4893
- "array": [
4894
- "u8",
4895
- 12
4873
+ 64
4896
4874
  ]
4897
4875
  }
4898
4876
  }
@@ -4867,10 +4867,6 @@ export type NaraAgentRegistry = {
4867
4867
  ]
4868
4868
  }
4869
4869
  },
4870
- {
4871
- "name": "padding",
4872
- "type": "u32"
4873
- },
4874
4870
  {
4875
4871
  "name": "referralCount",
4876
4872
  "type": "u32"
@@ -4880,25 +4876,7 @@ export type NaraAgentRegistry = {
4880
4876
  "type": {
4881
4877
  "array": [
4882
4878
  "u8",
4883
- 32
4884
- ]
4885
- }
4886
- },
4887
- {
4888
- "name": "reserved2",
4889
- "type": {
4890
- "array": [
4891
- "u8",
4892
- 16
4893
- ]
4894
- }
4895
- },
4896
- {
4897
- "name": "reserved3",
4898
- "type": {
4899
- "array": [
4900
- "u8",
4901
- 12
4879
+ 64
4902
4880
  ]
4903
4881
  }
4904
4882
  }