naracli 1.0.83 → 1.0.84

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naracli",
3
- "version": "1.0.83",
3
+ "version": "1.0.84",
4
4
  "description": "CLI for the Nara chain (Solana-compatible)",
5
5
  "homepage": "https://nara.build",
6
6
  "repository": {
@@ -57,6 +57,9 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@clack/prompts": "^1.0.1",
60
+ "@meteora-ag/cp-amm-sdk": "^1.4.1",
61
+ "@meteora-ag/dlmm": "^1.9.4",
62
+ "@meteora-ag/dynamic-bonding-curve-sdk": "^1.5.7",
60
63
  "@solana/spl-token": "^0.4.14",
61
64
  "@solana/web3.js": "^1.98.4",
62
65
  "bip39": "^3.1.0",
@@ -64,7 +67,7 @@
64
67
  "bs58": "^6.0.0",
65
68
  "commander": "^12.1.0",
66
69
  "ed25519-hd-key": "^1.3.0",
67
- "nara-sdk": "^1.0.74",
70
+ "nara-sdk": "^1.0.78",
68
71
  "picocolors": "^1.1.1"
69
72
  },
70
73
  "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Bridge commands - cross-chain transfer between Solana and Nara
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
7
+ import { getAssociatedTokenAddress } from "@solana/spl-token";
8
+ import { loadWallet, getRpcUrl } from "../utils/wallet";
9
+ import {
10
+ printError,
11
+ printInfo,
12
+ printSuccess,
13
+ formatOutput,
14
+ } from "../utils/output";
15
+ import type { GlobalOptions } from "../types";
16
+ import {
17
+ bridgeTransfer,
18
+ extractMessageId,
19
+ queryMessageStatus,
20
+ queryMessageSignatures,
21
+ BRIDGE_TOKENS,
22
+ type BridgeChain,
23
+ } from "nara-sdk";
24
+
25
+ const DEFAULT_SOLANA_RPC = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com";
26
+ const NARA_RPC = "https://mainnet-api.nara.build/";
27
+
28
+ function getSolanaRpc(solanaRpc?: string): string {
29
+ return solanaRpc || DEFAULT_SOLANA_RPC;
30
+ }
31
+
32
+ function getSourceConnection(fromChain: BridgeChain, rpcUrl?: string, solanaRpc?: string): Connection {
33
+ if (fromChain === "nara") return new Connection(rpcUrl || NARA_RPC, "confirmed");
34
+ return new Connection(getSolanaRpc(solanaRpc), "confirmed");
35
+ }
36
+
37
+ function getDestConnection(toChain: BridgeChain, solanaRpc?: string): Connection {
38
+ if (toChain === "solana") return new Connection(getSolanaRpc(solanaRpc), "confirmed");
39
+ return new Connection(NARA_RPC, "confirmed");
40
+ }
41
+
42
+ // ─── Command: bridge transfer ────────────────────────────────────
43
+
44
+ async function handleBridgeTransfer(
45
+ token: string,
46
+ amount: string,
47
+ options: GlobalOptions & { from: string; to?: string; recipient?: string; solanaRpc?: string }
48
+ ) {
49
+ const fromChain = options.from as BridgeChain;
50
+ if (fromChain !== "solana" && fromChain !== "nara") {
51
+ printError('--from must be "solana" or "nara"');
52
+ process.exit(1);
53
+ }
54
+ const toChain: BridgeChain = fromChain === "solana" ? "nara" : "solana";
55
+
56
+ const tokenUpper = token.toUpperCase();
57
+ const tokenConfig = (BRIDGE_TOKENS as Record<string, any>)[tokenUpper];
58
+ if (!tokenConfig) {
59
+ printError(`Unknown token "${token}". Supported: ${Object.keys(BRIDGE_TOKENS).join(", ")}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ const wallet = await loadWallet(options.wallet);
64
+ const recipientPubkey = options.recipient
65
+ ? new PublicKey(options.recipient)
66
+ : wallet.publicKey;
67
+
68
+ const decimals = tokenConfig.decimals;
69
+ const rawAmount = BigInt(Math.floor(parseFloat(amount) * 10 ** decimals));
70
+
71
+ const rpcUrl = fromChain === "nara" ? getRpcUrl(options.rpcUrl) : undefined;
72
+ const connection = getSourceConnection(fromChain, rpcUrl, options.solanaRpc);
73
+
74
+ // Check gas balance
75
+ const gasBalance = await connection.getBalance(wallet.publicKey);
76
+ const minGas = 0.001 * LAMPORTS_PER_SOL;
77
+ if (gasBalance < minGas) {
78
+ const coin = fromChain === "solana" ? "SOL" : "NARA";
79
+ printError(`Insufficient gas. Balance: ${gasBalance / LAMPORTS_PER_SOL} ${coin}, need at least 0.001 ${coin}.`);
80
+ process.exit(1);
81
+ }
82
+
83
+ // Check token balance
84
+ const sourceSide = tokenConfig[fromChain];
85
+ if (sourceSide.mint) {
86
+ // SPL token — check token account balance
87
+ const tokenAccount = await getAssociatedTokenAddress(sourceSide.mint, wallet.publicKey, false, sourceSide.tokenProgram);
88
+ try {
89
+ const bal = await connection.getTokenAccountBalance(tokenAccount);
90
+ const rawBalance = BigInt(bal.value.amount);
91
+ if (rawBalance < rawAmount) {
92
+ printError(`Insufficient ${tokenUpper} balance. Have: ${bal.value.uiAmountString}, need: ${amount}`);
93
+ process.exit(1);
94
+ }
95
+ } catch {
96
+ printError(`No ${tokenUpper} token account found. Balance: 0`);
97
+ process.exit(1);
98
+ }
99
+ } else {
100
+ // Native token — check SOL/NARA balance (minus gas reserve)
101
+ const available = BigInt(gasBalance) - BigInt(Math.ceil(minGas));
102
+ if (available < rawAmount) {
103
+ const coin = fromChain === "solana" ? "SOL" : "NARA";
104
+ printError(`Insufficient ${coin} balance. Available: ${Number(available) / LAMPORTS_PER_SOL} (after gas reserve), need: ${amount}`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ if (!options.json) {
110
+ printInfo(`Bridging ${amount} ${tokenUpper} from ${fromChain} to ${toChain}...`);
111
+ printInfo(`Sender: ${wallet.publicKey.toBase58()}`);
112
+ printInfo(`Recipient: ${recipientPubkey.toBase58()}`);
113
+ }
114
+
115
+ const result = await bridgeTransfer(connection, wallet, {
116
+ token: tokenUpper,
117
+ fromChain,
118
+ recipient: recipientPubkey,
119
+ amount: rawAmount,
120
+ });
121
+
122
+ if (!options.json) {
123
+ printSuccess("Bridge transfer submitted!");
124
+ console.log(` Transaction: ${result.signature}`);
125
+ console.log(` Message ID: ${result.messageId ?? "(pending)"}`);
126
+ console.log(` Fee: ${Number(result.feeAmount) / 10 ** decimals} ${tokenUpper}`);
127
+ console.log(` Bridged: ${Number(result.bridgeAmount) / 10 ** decimals} ${tokenUpper}`);
128
+ console.log("");
129
+ console.log(` Track delivery: npx naracli bridge status ${result.messageId ?? result.signature} --from ${fromChain}`);
130
+ } else {
131
+ formatOutput({
132
+ signature: result.signature,
133
+ messageId: result.messageId,
134
+ feeAmount: result.feeAmount.toString(),
135
+ bridgeAmount: result.bridgeAmount.toString(),
136
+ fromChain,
137
+ toChain,
138
+ token: tokenUpper,
139
+ }, true);
140
+ }
141
+ }
142
+
143
+ // ─── Command: bridge status ──────────────────────────────────────
144
+
145
+ async function handleBridgeStatus(
146
+ id: string,
147
+ options: GlobalOptions & { from: string; solanaRpc?: string }
148
+ ) {
149
+ const fromChain = options.from as BridgeChain;
150
+ if (fromChain !== "solana" && fromChain !== "nara") {
151
+ printError('--from must be "solana" or "nara"');
152
+ process.exit(1);
153
+ }
154
+ const toChain: BridgeChain = fromChain === "solana" ? "nara" : "solana";
155
+
156
+ let messageId = id;
157
+
158
+ // If it doesn't look like a message ID (0x...), treat as tx signature
159
+ if (!id.startsWith("0x")) {
160
+ if (!options.json) printInfo("Extracting message ID from transaction...");
161
+ const sourceConn = getSourceConnection(fromChain, fromChain === "nara" ? getRpcUrl(options.rpcUrl) : undefined, options.solanaRpc);
162
+ const extracted = await extractMessageId(sourceConn, id);
163
+ if (!extracted) {
164
+ printError("Could not extract message ID from transaction. It may not have been confirmed yet.");
165
+ process.exit(1);
166
+ }
167
+ messageId = extracted;
168
+ if (!options.json) console.log(` Message ID: ${messageId}`);
169
+ }
170
+
171
+ // Query delivery status
172
+ if (!options.json) printInfo("Checking delivery status...");
173
+ const destConn = getDestConnection(toChain, options.solanaRpc);
174
+ const status = await queryMessageStatus(destConn, messageId, toChain);
175
+
176
+ // Query validator signatures
177
+ if (!options.json) printInfo("Checking validator signatures...");
178
+ const sigStatus = await queryMessageSignatures(messageId, fromChain);
179
+
180
+ if (options.json) {
181
+ formatOutput({
182
+ messageId,
183
+ fromChain,
184
+ toChain,
185
+ delivered: status.delivered,
186
+ deliverySignature: status.deliverySignature,
187
+ validators: {
188
+ signed: sigStatus.signedCount,
189
+ total: sigStatus.totalValidators,
190
+ fullySigned: sigStatus.fullySigned,
191
+ },
192
+ }, true);
193
+ } else {
194
+ console.log("");
195
+ console.log(` Message ID: ${messageId}`);
196
+ console.log(` Route: ${fromChain} → ${toChain}`);
197
+ console.log(` Delivered: ${status.delivered ? "yes" : "no"}`);
198
+ if (status.deliverySignature) console.log(` Delivery TX: ${status.deliverySignature}`);
199
+ console.log(` Validators: ${sigStatus.signedCount}/${sigStatus.totalValidators} signed${sigStatus.fullySigned ? " (complete)" : ""}`);
200
+ console.log("");
201
+ }
202
+ }
203
+
204
+ // ─── Command: bridge tokens ──────────────────────────────────────
205
+
206
+ function handleBridgeTokens(options: GlobalOptions) {
207
+ const tokens = Object.values(BRIDGE_TOKENS) as any[];
208
+ if (options.json) {
209
+ formatOutput(tokens.map((t: any) => ({
210
+ symbol: t.symbol,
211
+ decimals: t.decimals,
212
+ solanaMint: t.solana.mint?.toBase58() ?? "native",
213
+ naraMint: t.nara.mint?.toBase58() ?? "native",
214
+ })), true);
215
+ } else {
216
+ console.log("");
217
+ for (const t of tokens) {
218
+ console.log(` ${t.symbol} (${t.decimals} decimals)`);
219
+ console.log(` Solana: ${t.solana.mint?.toBase58() ?? "native SOL"} (${t.solana.mode})`);
220
+ console.log(` Nara: ${t.nara.mint?.toBase58() ?? "native NARA"} (${t.nara.mode})`);
221
+ }
222
+ console.log("");
223
+ }
224
+ }
225
+
226
+ // ─── Command: bridge info ────────────────────────────────────────
227
+
228
+ async function handleBridgeInfo(options: GlobalOptions & { solanaRpc?: string }) {
229
+ const wallet = await loadWallet(options.wallet);
230
+ const owner = wallet.publicKey;
231
+
232
+ const naraConn = new Connection(getRpcUrl(options.rpcUrl), "confirmed");
233
+ const solConn = new Connection(getSolanaRpc(options.solanaRpc), "confirmed");
234
+
235
+ const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } = await import("@solana/spl-token");
236
+
237
+ const tokens = Object.values(BRIDGE_TOKENS) as any[];
238
+
239
+ // Build all ATA addresses for both chains
240
+ const queries: Array<{ symbol: string; chain: string; conn: Connection; ata: PublicKey | null; decimals: number; isNative: boolean }> = [];
241
+
242
+ for (const t of tokens) {
243
+ // Solana side
244
+ if (t.solana.mint) {
245
+ const ata = await getAssociatedTokenAddress(t.solana.mint, owner, true, t.solana.tokenProgram);
246
+ queries.push({ symbol: t.symbol, chain: "solana", conn: solConn, ata, decimals: t.decimals, isNative: false });
247
+ } else {
248
+ queries.push({ symbol: t.symbol, chain: "solana", conn: solConn, ata: null, decimals: t.decimals, isNative: true });
249
+ }
250
+ // Nara side
251
+ if (t.nara.mint) {
252
+ const ata = await getAssociatedTokenAddress(t.nara.mint, owner, true, t.nara.tokenProgram);
253
+ queries.push({ symbol: t.symbol, chain: "nara", conn: naraConn, ata, decimals: t.decimals, isNative: false });
254
+ } else {
255
+ queries.push({ symbol: t.symbol, chain: "nara", conn: naraConn, ata: null, decimals: t.decimals, isNative: true });
256
+ }
257
+ }
258
+
259
+ // Batch fetch per chain
260
+ const solQueries = queries.filter(q => q.chain === "solana");
261
+ const naraQueries = queries.filter(q => q.chain === "nara");
262
+
263
+ // Fetch native balances
264
+ const [solNativeBalance, naraNativeBalance] = await Promise.all([
265
+ solConn.getBalance(owner),
266
+ naraConn.getBalance(owner),
267
+ ]);
268
+
269
+ // Fetch token accounts
270
+ const solAccounts = await solConn.getMultipleAccountsInfo(
271
+ solQueries.filter(q => q.ata).map(q => q.ata!)
272
+ );
273
+ const naraAccounts = await naraConn.getMultipleAccountsInfo(
274
+ naraQueries.filter(q => q.ata).map(q => q.ata!)
275
+ );
276
+
277
+ function parseTokenAmount(data: Buffer | null, decimals: number): string {
278
+ if (!data) return "0";
279
+ try {
280
+ const raw = BigInt("0x" + Buffer.from(data.slice(64, 72)).reverse().toString("hex"));
281
+ return (Number(raw) / 10 ** decimals).toString();
282
+ } catch { return "0"; }
283
+ }
284
+
285
+ // Build results
286
+ const results: Array<{ symbol: string; solana: string; nara: string }> = [];
287
+ let solTokenIdx = 0, naraTokenIdx = 0;
288
+
289
+ for (const t of tokens) {
290
+ let solBalance: string, naraBalance: string;
291
+
292
+ // Solana side
293
+ const sq = solQueries.find(q => q.symbol === t.symbol)!;
294
+ if (sq.isNative) {
295
+ solBalance = (solNativeBalance / LAMPORTS_PER_SOL).toString();
296
+ } else {
297
+ solBalance = parseTokenAmount(solAccounts[solTokenIdx]?.data as Buffer | null, sq.decimals);
298
+ solTokenIdx++;
299
+ }
300
+
301
+ // Nara side
302
+ const nq = naraQueries.find(q => q.symbol === t.symbol)!;
303
+ if (nq.isNative) {
304
+ naraBalance = (naraNativeBalance / LAMPORTS_PER_SOL).toString();
305
+ } else {
306
+ naraBalance = parseTokenAmount(naraAccounts[naraTokenIdx]?.data as Buffer | null, nq.decimals);
307
+ naraTokenIdx++;
308
+ }
309
+
310
+ results.push({ symbol: t.symbol, solana: solBalance, nara: naraBalance });
311
+ }
312
+
313
+ if (options.json) {
314
+ formatOutput({ owner: owner.toBase58(), balances: results }, true);
315
+ } else {
316
+ console.log(`\n Owner: ${owner.toBase58()}\n`);
317
+ console.log(` ${"Token".padEnd(8)} ${"Solana".padEnd(20)} Nara`);
318
+ console.log(` ${"─".repeat(8)} ${"─".repeat(20)} ${"─".repeat(20)}`);
319
+ for (const r of results) {
320
+ console.log(` ${r.symbol.padEnd(8)} ${r.solana.padEnd(20)} ${r.nara}`);
321
+ }
322
+ console.log("");
323
+ }
324
+ }
325
+
326
+ // ─── Register commands ───────────────────────────────────────────
327
+
328
+ export function registerBridgeCommands(program: Command): void {
329
+ const bridge = program
330
+ .command("bridge")
331
+ .description("Cross-chain bridge between Solana and Nara (powered by Hyperlane)")
332
+ .addHelpText("after", `
333
+ 1. Solana and Nara use the same wallet address (Ed25519). To bridge from Solana, your wallet must have SOL on Solana mainnet for gas.
334
+ 2. Delivery takes ~5-10 minutes (Hyperlane multi-validator signing).
335
+ 3. Bridge fee: 0.5%.
336
+
337
+ Examples:
338
+ npx naracli bridge transfer USDC 10 --from solana # Solana → Nara
339
+ npx naracli bridge transfer USDC 10 --from nara # Nara → Solana
340
+ npx naracli bridge transfer SOL 1 --from solana --solana-rpc https://my-rpc.com
341
+
342
+ `);
343
+
344
+ // bridge transfer
345
+ bridge
346
+ .command("transfer <token> <amount>")
347
+ .description("Bridge tokens between Solana and Nara (e.g. bridge transfer USDC 10 --from solana)")
348
+ .requiredOption("--from <chain>", 'Source chain: "solana" or "nara"')
349
+ .option("--recipient <address>", "Recipient address on destination chain (defaults to sender)")
350
+ .option("--solana-rpc <url>", "Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)")
351
+ .action(async (token: string, amount: string, opts: any, cmd: Command) => {
352
+ try {
353
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
354
+ await handleBridgeTransfer(token, amount, { ...globalOpts, from: opts.from, recipient: opts.recipient, solanaRpc: opts.solanaRpc });
355
+ } catch (error: any) {
356
+ printError(error.message);
357
+ process.exit(1);
358
+ }
359
+ });
360
+
361
+ // bridge status
362
+ bridge
363
+ .command("status <tx-or-message-id>")
364
+ .description("Check bridge transfer status by transaction signature or message ID")
365
+ .requiredOption("--from <chain>", 'Source chain: "solana" or "nara"')
366
+ .option("--solana-rpc <url>", "Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)")
367
+ .action(async (id: string, opts: any, cmd: Command) => {
368
+ try {
369
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
370
+ await handleBridgeStatus(id, { ...globalOpts, from: opts.from, solanaRpc: opts.solanaRpc });
371
+ } catch (error: any) {
372
+ printError(error.message);
373
+ process.exit(1);
374
+ }
375
+ });
376
+
377
+ // bridge info
378
+ bridge
379
+ .command("info")
380
+ .description("Show bridgeable token balances on both Solana and Nara")
381
+ .option("--solana-rpc <url>", "Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)")
382
+ .action(async (opts: any, cmd: Command) => {
383
+ try {
384
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
385
+ await handleBridgeInfo({ ...globalOpts, solanaRpc: opts.solanaRpc });
386
+ } catch (error: any) {
387
+ printError(error.message);
388
+ process.exit(1);
389
+ }
390
+ });
391
+
392
+ // bridge tokens
393
+ bridge
394
+ .command("tokens")
395
+ .description("List supported bridge tokens")
396
+ .action(async (_opts: any, cmd: Command) => {
397
+ try {
398
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
399
+ handleBridgeTokens(globalOpts);
400
+ } catch (error: any) {
401
+ printError(error.message);
402
+ process.exit(1);
403
+ }
404
+ });
405
+ }