naracli 0.1.0

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.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Swap commands
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import BN from "bn.js";
7
+ import { NaraSDK } from "../../client";
8
+ import { buyToken, sellToken, getSwapQuote, SwapMode } from "../../swap";
9
+ import { loadWallet, getRpcUrl } from "../utils/wallet";
10
+ import {
11
+ validatePublicKey,
12
+ validatePositiveNumber,
13
+ validateNonNegativeNumber,
14
+ validateSwapMode,
15
+ validateDirection,
16
+ } from "../utils/validation";
17
+ import {
18
+ handleTransaction,
19
+ printTransactionResult,
20
+ } from "../utils/transaction";
21
+ import { formatOutput, printError, printInfo } from "../utils/output";
22
+ import type {
23
+ SwapBuyOptions,
24
+ SwapSellOptions,
25
+ SwapQuoteOptions,
26
+ } from "../types";
27
+
28
+ /**
29
+ * Register swap commands
30
+ * @param program Commander program
31
+ */
32
+ export function registerSwapCommands(program: Command): void {
33
+ const swap = program.command("swap").description("Token swap commands");
34
+
35
+ // swap buy
36
+ swap
37
+ .command("buy <token-address> <amount>")
38
+ .description("Buy tokens with NSO")
39
+ .option("--slippage <number>", "Slippage in basis points", "100")
40
+ .option(
41
+ "--mode <mode>",
42
+ "Swap mode: exact-in|partial-fill|exact-out",
43
+ "partial-fill"
44
+ )
45
+ .option("-e, --export-tx", "Export unsigned transaction", false)
46
+ .action(
47
+ async (
48
+ tokenAddress: string,
49
+ amount: string,
50
+ options: Omit<SwapBuyOptions, "tokenAddress" | "amount">
51
+ ) => {
52
+ try {
53
+ await handleSwapBuy(tokenAddress, amount, options);
54
+ } catch (error: any) {
55
+ printError(error.message);
56
+ process.exit(1);
57
+ }
58
+ }
59
+ );
60
+
61
+ // swap sell
62
+ swap
63
+ .command("sell <token-address> <amount>")
64
+ .description("Sell tokens for NSO")
65
+ .option("--decimals <number>", "Token decimals", "6")
66
+ .option("--slippage <number>", "Slippage in basis points", "100")
67
+ .option(
68
+ "--mode <mode>",
69
+ "Swap mode: exact-in|partial-fill|exact-out",
70
+ "partial-fill"
71
+ )
72
+ .option("-e, --export-tx", "Export unsigned transaction", false)
73
+ .action(
74
+ async (
75
+ tokenAddress: string,
76
+ amount: string,
77
+ options: Omit<SwapSellOptions, "tokenAddress" | "amount">
78
+ ) => {
79
+ try {
80
+ await handleSwapSell(tokenAddress, amount, options);
81
+ } catch (error: any) {
82
+ printError(error.message);
83
+ process.exit(1);
84
+ }
85
+ }
86
+ );
87
+
88
+ // swap quote
89
+ swap
90
+ .command("quote <token-address> <amount> <direction>")
91
+ .description("Get swap quote (direction: buy|sell)")
92
+ .option("--decimals <number>", "Token decimals (for sell only)", "6")
93
+ .option("--slippage <number>", "Slippage in basis points", "100")
94
+ .action(
95
+ async (
96
+ tokenAddress: string,
97
+ amount: string,
98
+ direction: string,
99
+ options: Omit<SwapQuoteOptions, "tokenAddress" | "amount" | "direction">
100
+ ) => {
101
+ try {
102
+ await handleSwapQuote(tokenAddress, amount, direction, options);
103
+ } catch (error: any) {
104
+ printError(error.message);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Convert swap mode string to enum
113
+ * @param mode Mode string
114
+ * @returns SwapMode enum value
115
+ */
116
+ function parseSwapMode(mode: string): SwapMode {
117
+ const normalized = validateSwapMode(mode);
118
+ switch (normalized) {
119
+ case "exact-in":
120
+ return SwapMode.ExactIn;
121
+ case "partial-fill":
122
+ return SwapMode.PartialFill;
123
+ case "exact-out":
124
+ return SwapMode.ExactOut;
125
+ default:
126
+ return SwapMode.PartialFill;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Handle swap buy command
132
+ * @param tokenAddress Token address
133
+ * @param amount Amount in SOL
134
+ * @param options Command options
135
+ */
136
+ async function handleSwapBuy(
137
+ tokenAddress: string,
138
+ amount: string,
139
+ options: Omit<SwapBuyOptions, "tokenAddress" | "amount">
140
+ ): Promise<void> {
141
+ // Load wallet
142
+ const wallet = await loadWallet(options.wallet);
143
+ const rpcUrl = getRpcUrl(options.rpcUrl);
144
+
145
+ printInfo(`Using RPC: ${rpcUrl}`);
146
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
147
+
148
+ // Validate inputs
149
+ validatePublicKey(tokenAddress);
150
+ const amountInSOL = validatePositiveNumber(amount, "amount");
151
+ const slippage = validateNonNegativeNumber(
152
+ options.slippage || "100",
153
+ "slippage"
154
+ );
155
+ const swapMode = parseSwapMode(options.mode || "partial-fill");
156
+
157
+ // Initialize SDK
158
+ const sdk = new NaraSDK({
159
+ rpcUrl,
160
+ commitment: "confirmed",
161
+ });
162
+
163
+ printInfo(`Buying tokens with ${amountInSOL} NSO...`);
164
+
165
+ // Buy tokens
166
+ const result = await buyToken(sdk, {
167
+ tokenAddress,
168
+ amountInSOL,
169
+ owner: wallet.publicKey,
170
+ slippageBps: slippage,
171
+ swapMode,
172
+ });
173
+
174
+ printInfo(`Expected output: ${result.expectedAmountOut} tokens (smallest unit)`);
175
+ printInfo(`Minimum output: ${result.minimumAmountOut} tokens (smallest unit)`);
176
+
177
+ // Handle transaction
178
+ const txResult = await handleTransaction(
179
+ sdk,
180
+ result.transaction,
181
+ [wallet],
182
+ options.exportTx || false
183
+ );
184
+
185
+ // Output result
186
+ if (options.json) {
187
+ const output = {
188
+ amountIn: result.amountIn,
189
+ expectedAmountOut: result.expectedAmountOut,
190
+ minimumAmountOut: result.minimumAmountOut,
191
+ ...(txResult.signature && { signature: txResult.signature }),
192
+ ...(txResult.base64 && { transaction: txResult.base64 }),
193
+ };
194
+ console.log(JSON.stringify(output, null, 2));
195
+ } else {
196
+ console.log(`\nSwap Details:`);
197
+ console.log(` Input: ${(parseInt(result.amountIn) / 1e9).toFixed(4)} NSO`);
198
+ console.log(` Expected Output: ${result.expectedAmountOut} tokens`);
199
+ console.log(` Minimum Output: ${result.minimumAmountOut} tokens`);
200
+ printTransactionResult(txResult, false);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Handle swap sell command
206
+ * @param tokenAddress Token address
207
+ * @param amount Amount in tokens
208
+ * @param options Command options
209
+ */
210
+ async function handleSwapSell(
211
+ tokenAddress: string,
212
+ amount: string,
213
+ options: Omit<SwapSellOptions, "tokenAddress" | "amount">
214
+ ): Promise<void> {
215
+ // Load wallet
216
+ const wallet = await loadWallet(options.wallet);
217
+ const rpcUrl = getRpcUrl(options.rpcUrl);
218
+
219
+ printInfo(`Using RPC: ${rpcUrl}`);
220
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
221
+
222
+ // Validate inputs
223
+ validatePublicKey(tokenAddress);
224
+ const amountInToken = validatePositiveNumber(amount, "amount");
225
+ const decimals = parseInt(String(options.decimals || "6"));
226
+ const slippage = validateNonNegativeNumber(
227
+ options.slippage || "100",
228
+ "slippage"
229
+ );
230
+ const swapMode = parseSwapMode(options.mode || "partial-fill");
231
+
232
+ // Initialize SDK
233
+ const sdk = new NaraSDK({
234
+ rpcUrl,
235
+ commitment: "confirmed",
236
+ });
237
+
238
+ printInfo(`Selling ${amountInToken} tokens...`);
239
+
240
+ // Sell tokens
241
+ const result = await sellToken(sdk, {
242
+ tokenAddress,
243
+ amountInToken,
244
+ owner: wallet.publicKey,
245
+ tokenDecimals: decimals,
246
+ slippageBps: slippage,
247
+ swapMode,
248
+ });
249
+
250
+ printInfo(`Expected output: ${(parseInt(result.expectedAmountOut) / 1e9).toFixed(4)} NSO`);
251
+ printInfo(`Minimum output: ${(parseInt(result.minimumAmountOut) / 1e9).toFixed(4)} NSO`);
252
+
253
+ // Handle transaction
254
+ const txResult = await handleTransaction(
255
+ sdk,
256
+ result.transaction,
257
+ [wallet],
258
+ options.exportTx || false
259
+ );
260
+
261
+ // Output result
262
+ if (options.json) {
263
+ const output = {
264
+ amountIn: result.amountIn,
265
+ expectedAmountOut: result.expectedAmountOut,
266
+ minimumAmountOut: result.minimumAmountOut,
267
+ ...(txResult.signature && { signature: txResult.signature }),
268
+ ...(txResult.base64 && { transaction: txResult.base64 }),
269
+ };
270
+ console.log(JSON.stringify(output, null, 2));
271
+ } else {
272
+ console.log(`\nSwap Details:`);
273
+ console.log(` Input: ${result.amountIn} tokens`);
274
+ console.log(` Expected Output: ${(parseInt(result.expectedAmountOut) / 1e9).toFixed(4)} NSO`);
275
+ console.log(` Minimum Output: ${(parseInt(result.minimumAmountOut) / 1e9).toFixed(4)} NSO`);
276
+ printTransactionResult(txResult, false);
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Handle swap quote command
282
+ * @param tokenAddress Token address
283
+ * @param amount Amount
284
+ * @param direction Direction (buy or sell)
285
+ * @param options Command options
286
+ */
287
+ async function handleSwapQuote(
288
+ tokenAddress: string,
289
+ amount: string,
290
+ direction: string,
291
+ options: Omit<SwapQuoteOptions, "tokenAddress" | "amount" | "direction">
292
+ ): Promise<void> {
293
+ const rpcUrl = getRpcUrl(options.rpcUrl);
294
+
295
+ printInfo(`Using RPC: ${rpcUrl}`);
296
+
297
+ // Validate inputs
298
+ validatePublicKey(tokenAddress);
299
+ const amountNum = validatePositiveNumber(amount, "amount");
300
+ const dir = validateDirection(direction);
301
+ const decimals = parseInt(String(options.decimals || "6"));
302
+ const slippage = validateNonNegativeNumber(
303
+ options.slippage || "100",
304
+ "slippage"
305
+ );
306
+
307
+ // Initialize SDK
308
+ const sdk = new NaraSDK({
309
+ rpcUrl,
310
+ commitment: "confirmed",
311
+ });
312
+
313
+ // Prepare parameters based on direction
314
+ const swapBaseForQuote = dir === "sell";
315
+ const amountIn = swapBaseForQuote
316
+ ? new BN(amountNum * 10 ** decimals) // Tokens to smallest unit
317
+ : new BN(amountNum * 1e9); // SOL to lamports
318
+
319
+ printInfo(`Getting ${dir} quote for ${amountNum} ${swapBaseForQuote ? "tokens" : "NSO"}...`);
320
+
321
+ // Get quote
322
+ const quote = await getSwapQuote(
323
+ sdk,
324
+ tokenAddress,
325
+ amountIn,
326
+ swapBaseForQuote,
327
+ slippage
328
+ );
329
+
330
+ // Output result
331
+ if (options.json) {
332
+ console.log(JSON.stringify(quote, null, 2));
333
+ } else {
334
+ console.log(`\nQuote:`);
335
+ if (swapBaseForQuote) {
336
+ // Selling tokens for SOL
337
+ console.log(` Input: ${(parseInt(quote.amountIn) / 10 ** decimals).toFixed(4)} tokens`);
338
+ console.log(` Expected Output: ${(parseInt(quote.outputAmount) / 1e9).toFixed(4)} NSO`);
339
+ console.log(` Minimum Output: ${(parseInt(quote.minimumAmountOut) / 1e9).toFixed(4)} NSO`);
340
+ } else {
341
+ // Buying tokens with NSO
342
+ console.log(` Input: ${(parseInt(quote.amountIn) / 1e9).toFixed(4)} NSO`);
343
+ console.log(` Expected Output: ${quote.outputAmount} tokens (smallest unit)`);
344
+ console.log(` Minimum Output: ${quote.minimumAmountOut} tokens (smallest unit)`);
345
+ }
346
+ console.log(` Trading Fee: ${quote.tradingFee}`);
347
+ console.log(` Protocol Fee: ${quote.protocolFee}`);
348
+ }
349
+ }