@txnlab/deflex 1.0.0-beta.1

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/dist/index.js ADDED
@@ -0,0 +1,994 @@
1
+ import { AlgorandClient } from "@algorandfoundation/algokit-utils";
2
+ import { LogicSigAccount, Transaction, assignGroupID, decodeUnsignedTransaction, isValidAddress, makeApplicationOptInTxnFromObject, msgpackRawDecode, signLogicSigTransactionObject, signTransaction, waitForConfirmation } from "algosdk";
3
+
4
+ //#region src/constants.ts
5
+ /**
6
+ * Supported DEX protocols for swap routing
7
+ */
8
+ let Protocol = /* @__PURE__ */ function(Protocol$1) {
9
+ Protocol$1["TinymanV2"] = "TinymanV2";
10
+ Protocol$1["Algofi"] = "Algofi";
11
+ Protocol$1["Algomint"] = "Algomint";
12
+ Protocol$1["Pact"] = "Pact";
13
+ Protocol$1["Folks"] = "Folks";
14
+ Protocol$1["TAlgo"] = "TAlgo";
15
+ return Protocol$1;
16
+ }({});
17
+ /**
18
+ * Deprecated protocols that are automatically excluded from routing
19
+ *
20
+ * @internal
21
+ */
22
+ const DEPRECATED_PROTOCOLS = ["Humble", "Tinyman"];
23
+ /** Default Algod node URI for mainnet */
24
+ const DEFAULT_ALGOD_URI = "https://mainnet-api.4160.nodely.dev/";
25
+ /** Default Algod node token (empty for public nodes) */
26
+ const DEFAULT_ALGOD_TOKEN = "";
27
+ /** Default Algod node port */
28
+ const DEFAULT_ALGOD_PORT = 443;
29
+ /** Default Deflex API base URL */
30
+ const DEFAULT_API_BASE_URL = "https://deflex.txnlab.dev/api";
31
+ /** Default fee in basis points (0.15%) */
32
+ const DEFAULT_FEE_BPS = 15;
33
+ /** Maximum allowed fee in basis points (3.00%) */
34
+ const MAX_FEE_BPS = 300;
35
+ /** Default maximum transaction group size */
36
+ const DEFAULT_MAX_GROUP_SIZE = 16;
37
+ /** Default maximum routing depth (number of hops) */
38
+ const DEFAULT_MAX_DEPTH = 4;
39
+ /** Default auto opt-in setting (automatic asset/app opt-in detection) */
40
+ const DEFAULT_AUTO_OPT_IN = false;
41
+ /** Default number of rounds to wait for transaction confirmation */
42
+ const DEFAULT_CONFIRMATION_ROUNDS = 4;
43
+
44
+ //#endregion
45
+ //#region src/composer.ts
46
+ /**
47
+ * Status of the SwapComposer transaction group lifecycle
48
+ */
49
+ let SwapComposerStatus = /* @__PURE__ */ function(SwapComposerStatus$1) {
50
+ /** The atomic group is still under construction. */
51
+ SwapComposerStatus$1[SwapComposerStatus$1["BUILDING"] = 0] = "BUILDING";
52
+ /** The atomic group has been finalized, but not yet signed. */
53
+ SwapComposerStatus$1[SwapComposerStatus$1["BUILT"] = 1] = "BUILT";
54
+ /** The atomic group has been finalized and signed, but not yet submitted to the network. */
55
+ SwapComposerStatus$1[SwapComposerStatus$1["SIGNED"] = 2] = "SIGNED";
56
+ /** The atomic group has been finalized, signed, and submitted to the network. */
57
+ SwapComposerStatus$1[SwapComposerStatus$1["SUBMITTED"] = 3] = "SUBMITTED";
58
+ /** The atomic group has been finalized, signed, submitted, and successfully committed to a block. */
59
+ SwapComposerStatus$1[SwapComposerStatus$1["COMMITTED"] = 4] = "COMMITTED";
60
+ return SwapComposerStatus$1;
61
+ }({});
62
+ /**
63
+ * Composer for building and executing atomic swap transaction groups
64
+ *
65
+ * The SwapComposer allows you to build complex transaction groups by adding custom
66
+ * transactions before and after swap transactions. It handles pre-signed transactions,
67
+ * automatic app opt-ins, and provides a fluent API for transaction group construction.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const quote = await deflex.fetchQuote({ ... })
72
+ * const composer = await deflex.newSwap({ quote, address, slippage })
73
+ *
74
+ * await composer
75
+ * .addTransaction(customTxn)
76
+ * .addSwapTransactions()
77
+ * .execute(signer)
78
+ * ```
79
+ */
80
+ var SwapComposer = class SwapComposer {
81
+ /** The maximum size of an atomic transaction group. */
82
+ static MAX_GROUP_SIZE = 16;
83
+ status = SwapComposerStatus.BUILDING;
84
+ transactions = [];
85
+ swapTransactionsAdded = false;
86
+ signedTxns = [];
87
+ txIds = [];
88
+ quote;
89
+ deflexTxns;
90
+ algorand;
91
+ address;
92
+ signer;
93
+ /**
94
+ * Create a new SwapComposer instance
95
+ *
96
+ * Note: Most developers should use DeflexClient.newSwap() instead of constructing
97
+ * this directly, as the factory method handles fetching swap transactions automatically.
98
+ *
99
+ * @param config - Configuration for the composer
100
+ * @param config.quote - The quote response from fetchQuote()
101
+ * @param config.deflexTxns - The swap transactions from fetchSwapTransactions()
102
+ * @param config.algorand - AlgorandClient instance for blockchain operations
103
+ * @param config.address - The address of the account that will sign transactions
104
+ * @param config.signer - Transaction signer function
105
+ */
106
+ constructor(config) {
107
+ if (!config.quote) throw new Error("Quote is required");
108
+ if (!config.deflexTxns) throw new Error("Swap transactions are required");
109
+ if (config.deflexTxns.length === 0) throw new Error("Swap transactions array cannot be empty");
110
+ if (!config.algorand) throw new Error("AlgorandClient instance is required");
111
+ if (!config.signer) throw new Error("Signer is required");
112
+ this.quote = config.quote;
113
+ this.deflexTxns = config.deflexTxns;
114
+ this.algorand = config.algorand;
115
+ this.address = this.validateAddress(config.address);
116
+ this.signer = config.signer;
117
+ }
118
+ /**
119
+ * Get the status of this composer's transaction group
120
+ *
121
+ * @returns The current status of the transaction group
122
+ */
123
+ getStatus() {
124
+ return this.status;
125
+ }
126
+ /**
127
+ * Get the number of transactions currently in this atomic group
128
+ *
129
+ * @returns The number of transactions in the group
130
+ */
131
+ count() {
132
+ return this.transactions.length;
133
+ }
134
+ /**
135
+ * Add a transaction to the atomic group
136
+ *
137
+ * Transactions are added in the order methods are called. For example:
138
+ * ```typescript
139
+ * composer
140
+ * .addTransaction(txn1) // Added first
141
+ * .addSwapTransactions() // Added second
142
+ * .addTransaction(txn2) // Added third
143
+ * ```
144
+ *
145
+ * @param transaction - The transaction to add
146
+ * @returns This composer instance for chaining
147
+ * @throws Error if the composer is not in the BUILDING status
148
+ * @throws Error if the maximum group size is exceeded
149
+ */
150
+ addTransaction(transaction) {
151
+ if (this.status !== SwapComposerStatus.BUILDING) throw new Error("Cannot add transactions when composer status is not BUILDING");
152
+ if (this.transactions.length === SwapComposer.MAX_GROUP_SIZE) throw new Error(`Adding an additional transaction exceeds the maximum atomic group size of ${SwapComposer.MAX_GROUP_SIZE}`);
153
+ this.transactions.push({ txn: transaction });
154
+ return this;
155
+ }
156
+ /**
157
+ * Add swap transactions to the atomic group
158
+ *
159
+ * This method automatically processes required app opt-ins and adds all swap
160
+ * transactions from the quote. Can only be called once per composer instance.
161
+ *
162
+ * @returns This composer instance for chaining
163
+ * @throws Error if the swap transactions have already been added
164
+ * @throws Error if the composer is not in the BUILDING status
165
+ * @throws Error if the maximum group size is exceeded
166
+ */
167
+ async addSwapTransactions() {
168
+ if (this.swapTransactionsAdded) throw new Error("Swap transactions have already been added");
169
+ if (this.status !== SwapComposerStatus.BUILDING) throw new Error("Cannot add swap transactions when composer status is not BUILDING");
170
+ const processedTxns = await this.processSwapTransactions();
171
+ if (this.transactions.length + processedTxns.length > SwapComposer.MAX_GROUP_SIZE) throw new Error(`Adding swap transactions exceeds the maximum atomic group size of ${SwapComposer.MAX_GROUP_SIZE}`);
172
+ this.transactions.push(...processedTxns);
173
+ this.swapTransactionsAdded = true;
174
+ return this;
175
+ }
176
+ /**
177
+ * Sign the transaction group
178
+ *
179
+ * Automatically adds swap transactions if not already added, builds the atomic group,
180
+ * and signs all transactions using the configured signer.
181
+ *
182
+ * @returns A promise that resolves to an array of signed transaction blobs
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * const signedTxns = await composer.sign()
187
+ * ```
188
+ */
189
+ async sign() {
190
+ if (this.status >= SwapComposerStatus.SIGNED) return this.signedTxns;
191
+ if (!this.swapTransactionsAdded) await this.addSwapTransactions();
192
+ const transactions = this.buildGroup();
193
+ const userTransactions = [];
194
+ const userTransactionIndexes = [];
195
+ const deflexSignedTxns = [];
196
+ for (let i = 0; i < transactions.length; i++) {
197
+ const item = transactions[i];
198
+ if (!item) continue;
199
+ if (!item.deflexSignature) {
200
+ userTransactions.push(item.txn);
201
+ userTransactionIndexes.push(userTransactions.length - 1);
202
+ } else {
203
+ const signedTxnBlob = this.signDeflexTransaction(item.txn, item.deflexSignature);
204
+ deflexSignedTxns.push(signedTxnBlob);
205
+ }
206
+ }
207
+ let userSignedTxns = [];
208
+ if (userTransactions.length > 0) userSignedTxns = await this.signer(userTransactions, userTransactionIndexes);
209
+ const signedTxns = [];
210
+ let userSignedIndex = 0;
211
+ let deflexSignedIndex = 0;
212
+ for (const item of transactions) if (!item.deflexSignature) {
213
+ const signedTxn = userSignedTxns[userSignedIndex];
214
+ if (signedTxn) signedTxns.push(signedTxn);
215
+ userSignedIndex++;
216
+ } else {
217
+ const deflexSignedTxn = deflexSignedTxns[deflexSignedIndex];
218
+ if (deflexSignedTxn) signedTxns.push(deflexSignedTxn);
219
+ deflexSignedIndex++;
220
+ }
221
+ const txIds = this.transactions.map((t) => t.txn.txID());
222
+ this.signedTxns = signedTxns;
223
+ this.txIds = txIds;
224
+ this.status = SwapComposerStatus.SIGNED;
225
+ return signedTxns;
226
+ }
227
+ /**
228
+ * Submit the signed transactions to the network
229
+ *
230
+ * This method signs the transaction group (if not already signed) and submits
231
+ * it to the Algorand network. Does not wait for confirmation.
232
+ *
233
+ * @returns The transaction IDs
234
+ * @throws Error if the transaction group has already been submitted
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * const txIds = await composer.submit()
239
+ * console.log('Submitted transactions:', txIds)
240
+ * ```
241
+ */
242
+ async submit() {
243
+ if (this.status > SwapComposerStatus.SUBMITTED) throw new Error("Transaction group cannot be resubmitted");
244
+ const stxns = await this.sign();
245
+ await this.algorand.client.algod.sendRawTransaction(stxns).do();
246
+ this.status = SwapComposerStatus.SUBMITTED;
247
+ return this.txIds;
248
+ }
249
+ /**
250
+ * Execute the swap
251
+ *
252
+ * Signs the transaction group, submits it to the network, and waits for confirmation.
253
+ * This is the primary method for executing swaps and combines sign(), submit(), and
254
+ * waitForConfirmation() into a single call.
255
+ *
256
+ * @param waitRounds - The number of rounds to wait for confirmation (default: 4)
257
+ * @returns Object containing the confirmed round and transaction IDs
258
+ * @throws Error if the transaction group has already been committed
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * const result = await composer.execute()
263
+ * console.log(`Confirmed in round ${result.confirmedRound}`)
264
+ * console.log('Transaction IDs:', result.txIds)
265
+ * ```
266
+ */
267
+ async execute(waitRounds = DEFAULT_CONFIRMATION_ROUNDS) {
268
+ if (this.status === SwapComposerStatus.COMMITTED) throw new Error("Transaction group has already been executed successfully");
269
+ const txIds = await this.submit();
270
+ const confirmedTxnInfo = await waitForConfirmation(this.algorand.client.algod, txIds[0], waitRounds);
271
+ this.status = SwapComposerStatus.COMMITTED;
272
+ return {
273
+ confirmedRound: confirmedTxnInfo.confirmedRound,
274
+ txIds
275
+ };
276
+ }
277
+ /**
278
+ * Validates an Algorand address
279
+ */
280
+ validateAddress(address) {
281
+ if (!isValidAddress(address)) throw new Error(`Invalid Algorand address: ${address}`);
282
+ return address;
283
+ }
284
+ /**
285
+ * Processes app opt-ins and decodes swap transactions from API response
286
+ */
287
+ async processSwapTransactions() {
288
+ const appOptIns = await this.processRequiredAppOptIns();
289
+ const swapTxns = [];
290
+ for (let i = 0; i < this.deflexTxns.length; i++) {
291
+ const deflexTxn = this.deflexTxns[i];
292
+ if (!deflexTxn) continue;
293
+ try {
294
+ const txn = decodeUnsignedTransaction(Buffer.from(deflexTxn.data, "base64"));
295
+ delete txn.group;
296
+ if (deflexTxn.signature !== false) swapTxns.push({
297
+ txn,
298
+ deflexSignature: deflexTxn.signature
299
+ });
300
+ else swapTxns.push({ txn });
301
+ } catch (error) {
302
+ throw new Error(`Failed to process swap transaction at index ${i}: ${error instanceof Error ? error.message : String(error)}`);
303
+ }
304
+ }
305
+ return [...appOptIns, ...swapTxns];
306
+ }
307
+ /**
308
+ * Creates opt-in transactions for apps the user hasn't opted into yet
309
+ */
310
+ async processRequiredAppOptIns() {
311
+ const userApps = (await this.algorand.account.getInformation(this.address))?.appsLocalState?.map((app) => Number(app.id)) || [];
312
+ const appsToOptIn = this.quote.requiredAppOptIns.filter((appId) => !userApps.includes(appId));
313
+ const appOptInTxns = [];
314
+ if (appsToOptIn.length > 0) {
315
+ const suggestedParams = await this.algorand.client.algod.getTransactionParams().do();
316
+ for (const appId of appsToOptIn) {
317
+ const optInTxn = makeApplicationOptInTxnFromObject({
318
+ sender: this.address,
319
+ appIndex: appId,
320
+ suggestedParams
321
+ });
322
+ appOptInTxns.push(optInTxn);
323
+ }
324
+ }
325
+ return appOptInTxns.map((txn) => ({ txn }));
326
+ }
327
+ /**
328
+ * Finalizes the transaction group by assigning group IDs
329
+ *
330
+ * The composer's status will be at least BUILT after executing this method.
331
+ */
332
+ buildGroup() {
333
+ if (this.status === SwapComposerStatus.BUILDING) {
334
+ if (this.transactions.length === 0) throw new Error("Cannot build a group with 0 transactions");
335
+ if (this.transactions.length > 1) assignGroupID(this.transactions.map((t) => t.txn));
336
+ this.status = SwapComposerStatus.BUILT;
337
+ }
338
+ return this.transactions;
339
+ }
340
+ /**
341
+ * Re-signs a Deflex transaction using the provided logic signature or secret key
342
+ */
343
+ signDeflexTransaction(transaction, signature) {
344
+ try {
345
+ if (signature.type === "logic_signature") {
346
+ const valueArray = signature.value;
347
+ const decoded = msgpackRawDecode(new Uint8Array(Object.values(valueArray)));
348
+ if (!decoded.lsig) throw new Error("Logic signature structure missing lsig field");
349
+ const lsig = decoded.lsig;
350
+ return signLogicSigTransactionObject(transaction, new LogicSigAccount(lsig.l, lsig.arg)).blob;
351
+ } else if (signature.type === "secret_key") {
352
+ const valueArray = signature.value;
353
+ return signTransaction(transaction, new Uint8Array(Object.values(valueArray))).blob;
354
+ } else throw new Error(`Unsupported signature type: ${signature.type}`);
355
+ } catch (error) {
356
+ throw new Error(`Failed to re-sign transaction: ${error instanceof Error ? error.message : String(error)}`);
357
+ }
358
+ }
359
+ };
360
+
361
+ //#endregion
362
+ //#region src/quote.ts
363
+ /**
364
+ * Wrapper class for Deflex quote responses with convenience methods
365
+ *
366
+ * The DeflexQuote class provides a developer-friendly interface for working with
367
+ * swap quotes from the Deflex API. It exposes all quote properties via getters
368
+ * and provides additional convenience methods for derived data.
369
+ *
370
+ * @example
371
+ * ```typescript
372
+ * const quote = await deflex.newQuote({
373
+ * address: 'ABC...',
374
+ * fromAssetId: 0,
375
+ * toAssetId: 31566704,
376
+ * amount: 1_000_000,
377
+ * })
378
+ *
379
+ * // Access quote properties directly
380
+ * console.log(quote.quote) // bigint (quoted amount)
381
+ * console.log(quote.fromAssetId) // number
382
+ * console.log(quote.toAssetId) // number
383
+ *
384
+ * // Access metadata
385
+ * console.log(quote.amount) // bigint (original request amount)
386
+ * console.log(quote.address) // string | undefined
387
+ * console.log(quote.createdAt) // number
388
+ *
389
+ * // Use convenience methods for derived data
390
+ * const minReceivedAmount = quote.getSlippageAmount(slippage) // fixed-input swap
391
+ * const maxSentAmount = quote.getSlippageAmount(slippage) // fixed-output swap
392
+ * ```
393
+ */
394
+ var DeflexQuote = class {
395
+ _response;
396
+ _amount;
397
+ _address;
398
+ _createdAt;
399
+ /**
400
+ * Create a new DeflexQuote instance
401
+ *
402
+ * Note: Most developers should use DeflexClient.newQuote() instead of constructing
403
+ * this directly, as the factory method handles fetching the quote automatically.
404
+ *
405
+ * @param config - Configuration for the quote instance
406
+ * @param config.response - The raw quote response from the Deflex API
407
+ * @param config.amount - The original amount from the quote request
408
+ * @param config.address - Optional address parameter from the quote request
409
+ */
410
+ constructor(config) {
411
+ if (!config?.response) throw new Error("Quote response is required");
412
+ if (config.amount === void 0 || config.amount === null) throw new Error("Amount is required");
413
+ this._response = config.response;
414
+ this._amount = BigInt(config.amount);
415
+ this._address = config.address;
416
+ this._createdAt = Date.now();
417
+ }
418
+ /**
419
+ * Get the raw quote response from the Deflex API
420
+ *
421
+ * @returns The raw quote response from the Deflex API
422
+ */
423
+ get response() {
424
+ return this._response;
425
+ }
426
+ /**
427
+ * The original amount from the quote request (in base units)
428
+ *
429
+ * This is the amount that was provided when fetching the quote.
430
+ * For fixed-input swaps, this is the input amount.
431
+ * For fixed-output swaps, this is the desired output amount.
432
+ *
433
+ * @example
434
+ * ```typescript
435
+ * const quote = await deflex.newQuote({
436
+ * fromAssetId: 0,
437
+ * toAssetId: 31566704,
438
+ * amount: 1_000_000, // 1 ALGO
439
+ * type: 'fixed-input'
440
+ * })
441
+ * console.log(quote.amount) // 1000000n (input amount)
442
+ * console.log(quote.quote) // 5000000n (output amount quoted)
443
+ * ```
444
+ */
445
+ get amount() {
446
+ return this._amount;
447
+ }
448
+ /**
449
+ * The address parameter from the quote request
450
+ *
451
+ * This is the address that was provided when fetching the quote (if any).
452
+ * Useful for tracking which account the quote was fetched for, especially
453
+ * when using autoOptIn functionality.
454
+ */
455
+ get address() {
456
+ return this._address;
457
+ }
458
+ /**
459
+ * Timestamp when the quote was created (in milliseconds)
460
+ *
461
+ * Can be used to determine quote freshness. Quotes may become stale
462
+ * over time as market conditions change.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * const ageInSeconds = (Date.now() - quote.createdAt) / 1000
467
+ * if (ageInSeconds > 30) {
468
+ * console.log('Quote may be stale, consider refreshing')
469
+ * }
470
+ * ```
471
+ */
472
+ get createdAt() {
473
+ return this._createdAt;
474
+ }
475
+ /**
476
+ * The quoted output amount or input amount (depending on quote type)
477
+ *
478
+ * For fixed-input swaps: This is the output amount you'll receive
479
+ * For fixed-output swaps: This is the input amount you'll need to provide
480
+ *
481
+ * @returns The quote amount as a bigint in base units
482
+ */
483
+ get quote() {
484
+ const rawQuote = this._response.quote;
485
+ return rawQuote === "" ? 0n : BigInt(rawQuote);
486
+ }
487
+ /**
488
+ * Profit information for the swap
489
+ */
490
+ get profit() {
491
+ return this._response.profit;
492
+ }
493
+ /**
494
+ * Baseline price without fees
495
+ */
496
+ get priceBaseline() {
497
+ return this._response.priceBaseline;
498
+ }
499
+ /**
500
+ * Price impact for the user
501
+ */
502
+ get userPriceImpact() {
503
+ return this._response.userPriceImpact;
504
+ }
505
+ /**
506
+ * Overall market price impact
507
+ */
508
+ get marketPriceImpact() {
509
+ return this._response.marketPriceImpact;
510
+ }
511
+ /**
512
+ * USD value of input amount
513
+ */
514
+ get usdIn() {
515
+ return this._response.usdIn;
516
+ }
517
+ /**
518
+ * USD value of output amount
519
+ */
520
+ get usdOut() {
521
+ return this._response.usdOut;
522
+ }
523
+ /**
524
+ * The routing path(s) for the swap
525
+ */
526
+ get route() {
527
+ return this._response.route;
528
+ }
529
+ /**
530
+ * Flattened view of routing percentages by protocol
531
+ */
532
+ get flattenedRoute() {
533
+ return this._response.flattenedRoute;
534
+ }
535
+ /**
536
+ * Individual quotes from each DEX
537
+ */
538
+ get quotes() {
539
+ return this._response.quotes;
540
+ }
541
+ /**
542
+ * App IDs that require opt-in for this swap
543
+ */
544
+ get requiredAppOptIns() {
545
+ return this._response.requiredAppOptIns;
546
+ }
547
+ /**
548
+ * Encrypted transaction payload for generating swap transactions
549
+ */
550
+ get txnPayload() {
551
+ return this._response.txnPayload;
552
+ }
553
+ /**
554
+ * Fees charged by each protocol
555
+ */
556
+ get protocolFees() {
557
+ return this._response.protocolFees;
558
+ }
559
+ /**
560
+ * Input asset ID
561
+ */
562
+ get fromAssetId() {
563
+ return this._response.fromASAID;
564
+ }
565
+ /**
566
+ * Output asset ID
567
+ */
568
+ get toAssetId() {
569
+ return this._response.toASAID;
570
+ }
571
+ /**
572
+ * Quote type
573
+ */
574
+ get type() {
575
+ return this._response.type;
576
+ }
577
+ /**
578
+ * Performance timing data
579
+ */
580
+ get timing() {
581
+ return this._response.timing;
582
+ }
583
+ /**
584
+ * Calculate the slippage-adjusted amount
585
+ *
586
+ * For fixed-input swaps: Returns the minimum amount you'll receive after slippage
587
+ * For fixed-output swaps: Returns the maximum amount you'll need to send after slippage
588
+ *
589
+ * @param slippage - Slippage tolerance as a percentage (e.g., 1 for 1%)
590
+ * @returns The slippage-adjusted amount as a bigint
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * // Fixed-input swap: 1 ALGO -> USDC
595
+ * const quote = await deflex.newQuote({
596
+ * fromAssetId: 0,
597
+ * toAssetId: 31566704,
598
+ * amount: 1_000_000,
599
+ * type: 'fixed-input'
600
+ * })
601
+ *
602
+ * // Get minimum output with 1% slippage
603
+ * const minOutput = quote.getSlippageAmount(1)
604
+ * console.log(`Minimum you'll receive: ${minOutput}`)
605
+ * ```
606
+ *
607
+ * @example
608
+ * ```typescript
609
+ * // Fixed-output swap: ALGO -> 10 USDC
610
+ * const quote = await deflex.newQuote({
611
+ * fromAssetId: 0,
612
+ * toAssetId: 31566704,
613
+ * amount: 10_000_000,
614
+ * type: 'fixed-output'
615
+ * })
616
+ *
617
+ * // Get maximum input with 1% slippage
618
+ * const maxInput = quote.getSlippageAmount(1)
619
+ * console.log(`Maximum you'll need to send: ${maxInput}`)
620
+ * ```
621
+ */
622
+ getSlippageAmount(slippage) {
623
+ const quoteAmount = this.quote;
624
+ const slippageBps = BigInt(Math.floor(slippage * 100));
625
+ if (this._response.type === "fixed-input") return quoteAmount * (10000n - slippageBps) / 10000n;
626
+ else return quoteAmount * (10000n + slippageBps) / 10000n;
627
+ }
628
+ };
629
+
630
+ //#endregion
631
+ //#region src/utils.ts
632
+ /**
633
+ * HTTP error with status code and response data
634
+ */
635
+ var HTTPError = class extends Error {
636
+ constructor(status, statusText, data) {
637
+ super(`HTTP ${status} ${statusText}`);
638
+ this.status = status;
639
+ this.statusText = statusText;
640
+ this.data = data;
641
+ this.name = "HTTPError";
642
+ }
643
+ };
644
+ /**
645
+ * Make an HTTP request and parse JSON response
646
+ *
647
+ * Simple wrapper around native fetch for API calls. Throws HTTPError for
648
+ * non-2xx responses.
649
+ *
650
+ * @param url - The URL to request
651
+ * @param options - Fetch options
652
+ * @returns Parsed JSON response
653
+ * @throws HTTPError if the response status is not ok
654
+ */
655
+ async function request(url, options) {
656
+ const response = await fetch(url, options);
657
+ if (!response.ok) {
658
+ let errorData;
659
+ try {
660
+ errorData = await response.json();
661
+ } catch {
662
+ try {
663
+ errorData = await response.text();
664
+ } catch {
665
+ errorData = "Failed to parse error response";
666
+ }
667
+ }
668
+ throw new HTTPError(response.status, response.statusText, JSON.stringify(errorData));
669
+ }
670
+ return response.json();
671
+ }
672
+
673
+ //#endregion
674
+ //#region src/client.ts
675
+ /**
676
+ * Client for interacting with the Deflex order router API
677
+ *
678
+ * The DeflexClient provides methods to fetch swap quotes and create transaction composers
679
+ * for executing swaps on the Algorand blockchain. It handles API communication, transaction
680
+ * validation, and automatic asset/app opt-in detection.
681
+ *
682
+ * @example
683
+ * ```typescript
684
+ * const deflex = new DeflexClient({
685
+ * apiKey: 'your-api-key',
686
+ * })
687
+ * ```
688
+ *
689
+ * @example
690
+ * ```typescript
691
+ * const deflex = new DeflexClient({
692
+ * apiKey: 'your-api-key',
693
+ * algodUri: 'https://mainnet-api.4160.nodely.dev/',
694
+ * algodToken: '',
695
+ * algodPort: 443,
696
+ * referrerAddress: 'REFERRER_ADDRESS...',
697
+ * feeBps: 15,
698
+ * autoOptIn: false,
699
+ * })
700
+ * ```
701
+ */
702
+ var DeflexClient = class {
703
+ baseUrl = DEFAULT_API_BASE_URL;
704
+ config;
705
+ algorand;
706
+ /**
707
+ * Create a new DeflexClient instance
708
+ *
709
+ * @param config - Configuration parameters
710
+ * @param config.apiKey - API key for Deflex API (required)
711
+ * @param config.algodUri - Algod node URI (default: https://mainnet-api.4160.nodely.dev/)
712
+ * @param config.algodToken - Algod node token (default: '')
713
+ * @param config.algodPort - Algod node port (default: 443)
714
+ * @param config.referrerAddress - Referrer address for fee sharing (receives 25% of swap fees)
715
+ * @param config.feeBps - Fee in basis points (default: 15 = 0.15%, max: 300 = 3.00%)
716
+ * @param config.autoOptIn - Automatically detect and add required opt-in transactions (default: false)
717
+ */
718
+ constructor(config) {
719
+ this.config = {
720
+ apiKey: this.validateApiKey(config.apiKey),
721
+ algodUri: config.algodUri ?? DEFAULT_ALGOD_URI,
722
+ algodToken: config.algodToken ?? DEFAULT_ALGOD_TOKEN,
723
+ algodPort: config.algodPort ?? DEFAULT_ALGOD_PORT,
724
+ referrerAddress: config.referrerAddress ? this.validateAddress(config.referrerAddress) : void 0,
725
+ feeBps: this.validateFeeBps(config.feeBps ?? DEFAULT_FEE_BPS),
726
+ autoOptIn: config.autoOptIn ?? DEFAULT_AUTO_OPT_IN
727
+ };
728
+ this.algorand = AlgorandClient.fromConfig({ algodConfig: {
729
+ server: this.config.algodUri,
730
+ port: this.config.algodPort,
731
+ token: this.config.algodToken
732
+ } });
733
+ }
734
+ /**
735
+ * Fetch a swap quote from the Deflex API
736
+ *
737
+ * Requests optimal swap routing from the Deflex API. The quote includes routing
738
+ * information, price impact, required opt-ins, and an encrypted transaction payload.
739
+ *
740
+ * @param params - Parameters for the quote request
741
+ * @param params.fromASAID - The ID of the asset to swap from
742
+ * @param params.toASAID - The ID of the asset to swap to
743
+ * @param params.amount - The amount of the asset to swap in base units
744
+ * @param params.type - The type of the quote (default: 'fixed-input')
745
+ * @param params.disabledProtocols - List of protocols to disable (default: [])
746
+ * @param params.maxGroupSize - The maximum group size (default: 16)
747
+ * @param params.maxDepth - The maximum depth (default: 4)
748
+ * @param params.address - The address of the account that will perform the swap (recommended when using `config.autoOptIn` or `params.optIn`)
749
+ * @param params.optIn - Whether to include asset opt-in transaction
750
+ * - If true: API reduces maxGroupSize by 1 and includes opt-in (always included, even if not needed)
751
+ * - If false: No opt-in transaction included
752
+ * - If undefined: Falls back to `config.autoOptIn` behavior with account check (if `params.address` is provided)
753
+ * @returns A FetchQuoteResponse object with routing information
754
+ *
755
+ * @example
756
+ * ```typescript
757
+ * const quote = await deflex.fetchQuote({
758
+ * address: 'ABC...',
759
+ * fromASAID: 0, // ALGO
760
+ * toASAID: 31566704, // USDC
761
+ * amount: 1_000_000, // 1 ALGO
762
+ * })
763
+ * ```
764
+ */
765
+ async fetchQuote(params) {
766
+ const { fromASAID, toASAID, amount, type = "fixed-input", disabledProtocols = [], maxGroupSize = DEFAULT_MAX_GROUP_SIZE, maxDepth = DEFAULT_MAX_DEPTH, optIn, address } = params;
767
+ const allDisabledProtocols = [...new Set([...DEPRECATED_PROTOCOLS, ...disabledProtocols])];
768
+ let includeOptIn = optIn;
769
+ if (includeOptIn === void 0 && this.config.autoOptIn) if (address) includeOptIn = await this.needsAssetOptIn(this.validateAddress(address), toASAID);
770
+ else console.warn("autoOptIn is enabled but no address provided to fetchQuote(). Asset opt-in check skipped.");
771
+ const url = new URL(`${this.baseUrl}/fetchQuote`);
772
+ url.searchParams.append("apiKey", this.config.apiKey);
773
+ url.searchParams.append("algodUri", this.config.algodUri);
774
+ url.searchParams.append("algodToken", this.config.algodToken);
775
+ url.searchParams.append("algodPort", String(this.config.algodPort));
776
+ url.searchParams.append("feeBps", this.config.feeBps.toString());
777
+ url.searchParams.append("fromASAID", BigInt(fromASAID).toString());
778
+ url.searchParams.append("toASAID", BigInt(toASAID).toString());
779
+ url.searchParams.append("amount", BigInt(amount).toString());
780
+ url.searchParams.append("type", type);
781
+ url.searchParams.append("disabledProtocols", allDisabledProtocols.join(","));
782
+ url.searchParams.append("maxGroupSize", maxGroupSize.toString());
783
+ url.searchParams.append("maxDepth", maxDepth.toString());
784
+ url.searchParams.append("optIn", String(includeOptIn));
785
+ if (this.config.referrerAddress) url.searchParams.append("referrerAddress", this.config.referrerAddress);
786
+ return request(url.toString());
787
+ }
788
+ /**
789
+ * Check if asset opt-in is required for the output asset
790
+ *
791
+ * Convenience method to determine if an address needs to opt into the output asset
792
+ * of a swap. This is useful when you want to get a quote without requiring wallet
793
+ * connection upfront, but need to know whether to set `optIn: true` in fetchQuote()
794
+ * to ensure proper routing (as opt-ins reduce maxGroupSize by 1).
795
+ *
796
+ * Note: If you enable `config.autoOptIn`, this check is handled automatically when
797
+ * an address is provided to fetchQuote().
798
+ *
799
+ * @param address - The address to check
800
+ * @param assetId - The output asset ID to check
801
+ * @returns True if asset opt-in is required, false otherwise (always false for ALGO)
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * // Check if opt-in needed for output asset before fetching quote
806
+ * const needsOptIn = await deflex.needsAssetOptIn(userAddress, toAssetId)
807
+ * const quote = await deflex.fetchQuote({
808
+ * fromAssetId,
809
+ * toAssetId,
810
+ * amount,
811
+ * optIn: needsOptIn,
812
+ * })
813
+ * ```
814
+ */
815
+ async needsAssetOptIn(address, assetId) {
816
+ const accountInfo = await this.algorand.account.getInformation(address);
817
+ return BigInt(assetId) !== 0n && accountInfo?.assets?.find((asset) => asset.assetId === BigInt(assetId)) === void 0;
818
+ }
819
+ /**
820
+ * Fetch swap transactions from the Deflex API
821
+ *
822
+ * Decrypts the quote payload and generates executable swap transactions for the
823
+ * specified signer address with the given slippage tolerance.
824
+ *
825
+ * @param params - Parameters for the swap transaction request
826
+ * @param params.quote - The quote response from fetchQuote()
827
+ * @param params.address - The address of the signer
828
+ * @param params.slippage - The slippage tolerance as a percentage (e.g., 1 for 1%)
829
+ * @returns A FetchSwapTxnsResponse object with transaction data
830
+ */
831
+ async fetchSwapTransactions(params) {
832
+ const { quote, address, slippage } = params;
833
+ this.validateAddress(address);
834
+ const url = new URL(`${this.baseUrl}/fetchExecuteSwapTxns`);
835
+ const body = {
836
+ apiKey: this.config.apiKey,
837
+ address,
838
+ txnPayloadJSON: quote.txnPayload,
839
+ slippage
840
+ };
841
+ return request(url.toString(), {
842
+ method: "POST",
843
+ headers: { "Content-Type": "application/json" },
844
+ body: JSON.stringify(body)
845
+ });
846
+ }
847
+ /**
848
+ * Fetch a quote and return a DeflexQuote wrapper instance
849
+ *
850
+ * This is the recommended way to fetch quotes. It wraps the raw API response
851
+ * in a DeflexQuote class that provides convenient methods for accessing quote data.
852
+ *
853
+ * @param params - Parameters for the quote request
854
+ * @param params.fromAssetId - The ID of the asset to swap from
855
+ * @param params.toAssetId - The ID of the asset to swap to
856
+ * @param params.amount - The amount of the asset to swap in base units
857
+ * @param params.type - The type of the quote (default: 'fixed-input')
858
+ * @param params.disabledProtocols - List of protocols to disable (default: [])
859
+ * @param params.maxGroupSize - The maximum group size (default: 16)
860
+ * @param params.maxDepth - The maximum depth (default: 4)
861
+ * @param params.address - The address of the account that will perform the swap (recommended when using `config.autoOptIn` or `params.optIn`)
862
+ * @param params.optIn - Whether to include asset opt-in transaction
863
+ * @returns A DeflexQuote instance with convenience methods
864
+ *
865
+ * @example
866
+ * ```typescript
867
+ * const quote = await deflex.newQuote({
868
+ * address: 'ABC...',
869
+ * fromAssetId: 0,
870
+ * toAssetId: 31566704,
871
+ * amount: 1_000_000,
872
+ * })
873
+ *
874
+ * // Access quote data
875
+ * console.log(quote.quote) // bigint (quoted amount)
876
+ * console.log(quote.fromAssetId) // number
877
+ * console.log(quote.toAssetId) // number
878
+ *
879
+ * // Access metadata
880
+ * console.log(quote.amount) // bigint (original request amount)
881
+ * console.log(quote.address) // string | undefined
882
+ * console.log(quote.createdAt) // number
883
+ *
884
+ * // Use convenience methods
885
+ * const minReceivedAmount = quote.getSlippageAmount(slippage) // fixed-input swap
886
+ * const maxSentAmount = quote.getSlippageAmount(slippage) // fixed-output swap
887
+ * ```
888
+ */
889
+ async newQuote(params) {
890
+ return new DeflexQuote({
891
+ response: await this.fetchQuote({
892
+ fromASAID: params.fromAssetId,
893
+ toASAID: params.toAssetId,
894
+ amount: params.amount,
895
+ type: params.type,
896
+ disabledProtocols: params.disabledProtocols,
897
+ maxGroupSize: params.maxGroupSize,
898
+ maxDepth: params.maxDepth,
899
+ optIn: params.optIn,
900
+ address: params.address
901
+ }),
902
+ amount: params.amount,
903
+ address: params.address
904
+ });
905
+ }
906
+ /**
907
+ * Create a SwapComposer instance
908
+ *
909
+ * This factory method creates a composer that allows you to add custom transactions
910
+ * before and after the swap transactions, with automatic handling of pre-signed transactions
911
+ * and opt-ins.
912
+ *
913
+ * @param config.quote - The quote response from fetchQuote() or a DeflexQuote instance
914
+ * @param config.address - The address of the signer
915
+ * @param config.slippage - The slippage tolerance
916
+ * @param config.signer - Transaction signer function
917
+ * @returns A SwapComposer instance ready for building transaction groups
918
+ *
919
+ * @example
920
+ * ```typescript
921
+ * // Basic swap
922
+ * const quote = await deflex.newQuote({ ... })
923
+ * await deflex.newSwap({ quote, address, slippage, signer })
924
+ * .execute()
925
+ * ```
926
+ *
927
+ * @example
928
+ * ```typescript
929
+ * // Advanced swap with custom transactions
930
+ * const quote = await deflex.newQuote({ ... })
931
+ * const swap = await deflex.newSwap({
932
+ * quote,
933
+ * address,
934
+ * slippage,
935
+ * signer,
936
+ * })
937
+ *
938
+ * console.log(swap.getStatus()) // BUILDING
939
+ *
940
+ * const signedTxns = await swap
941
+ * .addTransaction(beforeTxn)
942
+ * .addSwapTransactions() // Adds swap transactions to the group
943
+ * .addTransaction(afterTxn)
944
+ * .sign()
945
+ *
946
+ * console.log(swap.getStatus()) // SIGNED
947
+ *
948
+ * const result = await swap.execute(waitRounds)
949
+ * console.log(result.confirmedRound, result.txIds)
950
+ *
951
+ * console.log(swap.getStatus()) // COMMITTED
952
+ * ```
953
+ */
954
+ async newSwap(config) {
955
+ const { quote, address, slippage, signer } = config;
956
+ const quoteResponse = quote instanceof DeflexQuote ? quote.response : quote;
957
+ return new SwapComposer({
958
+ quote: quoteResponse,
959
+ deflexTxns: (await this.fetchSwapTransactions({
960
+ quote: quoteResponse,
961
+ address,
962
+ slippage
963
+ })).txns,
964
+ algorand: this.algorand,
965
+ address,
966
+ signer
967
+ });
968
+ }
969
+ /**
970
+ * Validates the API key
971
+ */
972
+ validateApiKey(apiKey) {
973
+ if (!apiKey) throw new Error("API key is required");
974
+ return apiKey;
975
+ }
976
+ /**
977
+ * Validates an Algorand address
978
+ */
979
+ validateAddress(address) {
980
+ if (!isValidAddress(address)) throw new Error(`Invalid Algorand address: ${address}`);
981
+ return address;
982
+ }
983
+ /**
984
+ * Validates the fee in basis points (max 300 = 3.00%)
985
+ */
986
+ validateFeeBps(feeBps) {
987
+ if (feeBps < 0 || feeBps > MAX_FEE_BPS) throw new Error(`Invalid fee: ${feeBps} basis points (must be between 0 and ${MAX_FEE_BPS})`);
988
+ return feeBps;
989
+ }
990
+ };
991
+
992
+ //#endregion
993
+ export { DEFAULT_ALGOD_PORT, DEFAULT_ALGOD_TOKEN, DEFAULT_ALGOD_URI, DEFAULT_API_BASE_URL, DEFAULT_AUTO_OPT_IN, DEFAULT_CONFIRMATION_ROUNDS, DEFAULT_FEE_BPS, DEFAULT_MAX_DEPTH, DEFAULT_MAX_GROUP_SIZE, DEPRECATED_PROTOCOLS, DeflexClient, DeflexQuote, HTTPError, MAX_FEE_BPS, Protocol, SwapComposer, SwapComposerStatus, request };
994
+ //# sourceMappingURL=index.js.map