@txnlab/haystack-router 3.3.0 → 3.5.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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as Logger } from "./logger-CYA908f8.mjs";
2
- import { Algodv2, AtomicTransactionComposer, LogicSigAccount, TransactionType, decodeUint64, decodeUnsignedTransaction, encodeAddress, getApplicationAddress, isValidAddress, makeApplicationOptInTxnFromObject, makeAssetTransferTxnWithSuggestedParamsFromObject, msgpackRawDecode, signLogicSigTransactionObject, signTransaction } from "algosdk";
2
+ import { Algodv2, AtomicTransactionComposer, LogicSigAccount, TransactionType, decodeUint64, decodeUnsignedTransaction, encodeAddress, getApplicationAddress, isTransactionWithSigner, isValidAddress, makeApplicationOptInTxnFromObject, makeAssetTransferTxnWithSuggestedParamsFromObject, msgpackRawDecode, signLogicSigTransactionObject, signTransaction } from "algosdk";
3
3
  // src/constants.ts
4
4
  /**
5
5
  * Supported DEX protocols for swap routing
@@ -42,8 +42,24 @@ const DEFAULT_MAX_DEPTH = 4;
42
42
  const DEFAULT_AUTO_OPT_IN = false;
43
43
  /** Default debug level (no debug logging) */
44
44
  const DEFAULT_DEBUG_LEVEL = "none";
45
+ /**
46
+ * What a Falcon (post-quantum) signature adds to its transaction's fee, in
47
+ * microAlgos: algod charges a transaction it signs 3 fee units instead of 1
48
+ */
49
+ const FALCON_EXTRA_FEE = 2000n;
45
50
  /** Default number of rounds to wait for transaction confirmation */
46
51
  const DEFAULT_CONFIRMATION_ROUNDS = 10;
52
+ // src/firststage.ts
53
+ /**
54
+ * Normalize a quote's raw First Stage object, coercing amounts to bigint
55
+ */
56
+ function toSwapFirstStageQuote(firstStage) {
57
+ return {
58
+ fromTax: BigInt(firstStage.fromTax),
59
+ toTax: BigInt(firstStage.toTax),
60
+ wrapped: [...firstStage.wrapped ?? []]
61
+ };
62
+ }
47
63
  // src/launch.ts
48
64
  /**
49
65
  * Normalize a quote's raw launch object, coercing amounts to bigint and
@@ -159,6 +175,15 @@ var SwapComposer = class SwapComposer {
159
175
  signer;
160
176
  middleware;
161
177
  note;
178
+ signerType;
179
+ signerFeesIncluded;
180
+ /** Transactions the account sends whose Falcon fees the composer adds */
181
+ signerFeeTxns = 0;
182
+ /** The account's first transaction in the group, which carries them when there's no input transfer */
183
+ firstUserTxn;
184
+ /** The swap's input transfer, whose fee carries a Falcon signer's extra fees */
185
+ inputTxn;
186
+ signerFeesAdded = false;
162
187
  inputTransactionIndex;
163
188
  outputTransactionIndex;
164
189
  curveTransactionIndex;
@@ -193,6 +218,8 @@ var SwapComposer = class SwapComposer {
193
218
  this.signer = config.signer;
194
219
  this.middleware = config.middleware ?? [];
195
220
  this.note = config.note;
221
+ this.signerType = config.signerType ?? config.quote.signerType ?? "ed25519";
222
+ this.signerFeesIncluded = config.signerFeesIncluded ?? false;
196
223
  }
197
224
  /**
198
225
  * Get the status of this composer's transaction group
@@ -233,6 +260,7 @@ var SwapComposer = class SwapComposer {
233
260
  txn: transaction,
234
261
  signer
235
262
  });
263
+ this.trackSignerFee(transaction.sender, transaction);
236
264
  return this;
237
265
  }
238
266
  /**
@@ -253,6 +281,8 @@ var SwapComposer = class SwapComposer {
253
281
  ...methodCall,
254
282
  signer: methodCall.signer ?? signer
255
283
  });
284
+ for (const arg of methodCall.methodArgs ?? []) if (isTransactionWithSigner(arg)) this.trackSignerFee(arg.txn.sender, arg.txn);
285
+ this.trackSignerFee(methodCall.sender);
256
286
  return this;
257
287
  }
258
288
  /**
@@ -276,15 +306,25 @@ var SwapComposer = class SwapComposer {
276
306
  if (this.getStatus() !== 0) throw new Error("Cannot add swap transactions when composer status is not BUILDING");
277
307
  const beforeTxns = await this.executeMiddlewareHooks("beforeSwap");
278
308
  if (this.atc.count() + beforeTxns.length > SwapComposer.MAX_GROUP_SIZE) throw new Error(`Adding beforeSwap transactions exceeds the maximum atomic group size of ${SwapComposer.MAX_GROUP_SIZE}`);
279
- for (const txnWithSigner of beforeTxns) this.atc.addTransaction(txnWithSigner);
280
- const { txns: processedTxns, inputTxnRelativeIndex, outputTxnRelativeIndex, curveTxnRelativeIndex } = await this.processSwapTransactions();
309
+ for (const txnWithSigner of beforeTxns) {
310
+ this.atc.addTransaction(txnWithSigner);
311
+ this.trackSignerFee(txnWithSigner.txn.sender, txnWithSigner.txn);
312
+ }
313
+ const { txns: processedTxns, apiTxns, inputTxn, inputTxnRelativeIndex, outputTxnRelativeIndex, curveTxnRelativeIndex } = await this.processSwapTransactions();
281
314
  const afterTxns = await this.executeMiddlewareHooks("afterSwap");
282
315
  if (this.atc.count() + processedTxns.length + afterTxns.length > SwapComposer.MAX_GROUP_SIZE) throw new Error(`Adding swap transactions exceeds the maximum atomic group size of ${SwapComposer.MAX_GROUP_SIZE}`);
283
316
  if (inputTxnRelativeIndex !== void 0) this.inputTransactionIndex = this.atc.count() + inputTxnRelativeIndex;
284
317
  if (outputTxnRelativeIndex !== void 0) this.outputTransactionIndex = this.atc.count() + outputTxnRelativeIndex;
285
318
  if (curveTxnRelativeIndex !== void 0) this.curveTransactionIndex = this.atc.count() + curveTxnRelativeIndex;
286
- for (const txnWithSigner of processedTxns) this.atc.addTransaction(txnWithSigner);
287
- for (const txnWithSigner of afterTxns) this.atc.addTransaction(txnWithSigner);
319
+ for (const txnWithSigner of processedTxns) {
320
+ this.atc.addTransaction(txnWithSigner);
321
+ this.trackSignerFee(txnWithSigner.txn.sender, txnWithSigner.txn, this.signerFeesIncluded && apiTxns.has(txnWithSigner.txn));
322
+ }
323
+ this.inputTxn = inputTxn;
324
+ for (const txnWithSigner of afterTxns) {
325
+ this.atc.addTransaction(txnWithSigner);
326
+ this.trackSignerFee(txnWithSigner.txn.sender, txnWithSigner.txn);
327
+ }
288
328
  this.swapTransactionsAdded = true;
289
329
  return this;
290
330
  }
@@ -312,6 +352,7 @@ var SwapComposer = class SwapComposer {
312
352
  * ```
313
353
  */
314
354
  buildGroup() {
355
+ this.addSignerFees();
315
356
  return this.atc.buildGroup();
316
357
  }
317
358
  /**
@@ -330,6 +371,7 @@ var SwapComposer = class SwapComposer {
330
371
  async sign() {
331
372
  if (this.getStatus() >= 2) return this.atc.gatherSignatures();
332
373
  if (!this.swapTransactionsAdded) await this.addSwapTransactions();
374
+ this.addSignerFees();
333
375
  return await this.atc.gatherSignatures();
334
376
  }
335
377
  /**
@@ -349,6 +391,7 @@ var SwapComposer = class SwapComposer {
349
391
  */
350
392
  async submit() {
351
393
  if (!this.swapTransactionsAdded) await this.addSwapTransactions();
394
+ this.addSignerFees();
352
395
  return await this.atc.submit(this.algodClient);
353
396
  }
354
397
  /**
@@ -372,6 +415,7 @@ var SwapComposer = class SwapComposer {
372
415
  async execute(waitRounds = 10) {
373
416
  try {
374
417
  if (!this.swapTransactionsAdded) await this.addSwapTransactions();
418
+ this.addSignerFees();
375
419
  const { txIDs, ...result } = await this.atc.execute(this.algodClient, waitRounds);
376
420
  if (this.summaryData) {
377
421
  if (this.inputTransactionIndex !== void 0) this.summaryData.inputTxnId = txIDs[this.inputTransactionIndex];
@@ -480,6 +524,40 @@ var SwapComposer = class SwapComposer {
480
524
  };
481
525
  }
482
526
  /**
527
+ * Counts a transaction from sender toward a Falcon signer's extra fees
528
+ * (addSignerFees) when the account sends it, unless the router's fees
529
+ * already include it (included). Every way of adding a transaction calls
530
+ * it; txn is the transaction when the composer holds it (not a method call
531
+ * the ATC builds), which can then carry the fees.
532
+ */
533
+ trackSignerFee(sender, txn, included = false) {
534
+ if (sender.toString() !== this.address) return;
535
+ if (txn) this.firstUserTxn ??= txn;
536
+ if (!included) this.signerFeeTxns++;
537
+ }
538
+ /**
539
+ * Adds a Falcon signer's extra fees before the group is built: a Falcon
540
+ * signature costs 3 fee units where an ed25519 one costs 1, and fees pool
541
+ * across the group, so the fee of the swap's input transfer (which already
542
+ * carries the router's fee budget) rises by FALCON_EXTRA_FEE for each
543
+ * transaction the account sends that the router's fees don't include. The
544
+ * account pays it, like every fee of the group. Runs once, while the group
545
+ * is still being built.
546
+ */
547
+ addSignerFees() {
548
+ if (this.signerType !== "falcon" || this.signerFeesAdded || this.getStatus() !== 0) return;
549
+ if (this.signerFeeTxns > 0) {
550
+ const carrier = this.inputTxn ?? this.firstUserTxn;
551
+ if (!carrier) throw new Error(`No transaction from ${this.address} in the group to carry its Falcon signature fees`);
552
+ carrier.fee += FALCON_EXTRA_FEE * BigInt(this.signerFeeTxns);
553
+ Logger.debug("Added Falcon signature fees", {
554
+ transactions: this.signerFeeTxns,
555
+ fee: carrier.fee
556
+ });
557
+ }
558
+ this.signerFeesAdded = true;
559
+ }
560
+ /**
483
561
  * Validates an Algorand address
484
562
  */
485
563
  validateAddress(address) {
@@ -524,10 +602,12 @@ var SwapComposer = class SwapComposer {
524
602
  };
525
603
  }
526
604
  const curveIndex = this.initLaunchSummary(decoded);
527
- const outputIndex = this.summaryData?.launch?.side === "buy" && curveIndex !== void 0 ? curveIndex : swapTxns.length - 1;
605
+ const outputIndex = this.summaryData?.launch?.side === "buy" && curveIndex !== void 0 ? curveIndex : this.findOutputTransactionIndex(decoded);
528
606
  const outputTxnRelativeIndex = outputIndex >= 0 ? appOptIns.length + outputIndex : void 0;
529
607
  return {
530
608
  txns: [...appOptIns, ...swapTxns],
609
+ apiTxns: new Set(decoded.map(({ txn }) => txn)),
610
+ inputTxn,
531
611
  inputTxnRelativeIndex: inputIndex !== void 0 ? appOptIns.length + inputIndex : void 0,
532
612
  outputTxnRelativeIndex,
533
613
  curveTxnRelativeIndex: curveIndex !== void 0 ? appOptIns.length + curveIndex : void 0
@@ -559,6 +639,20 @@ var SwapComposer = class SwapComposer {
559
639
  for (let i = txns.length - 1; i >= 0; i--) if (txns[i]?.signature === false) return i;
560
640
  }
561
641
  /**
642
+ * Finds the router's last app call among the API's transactions: v4's
643
+ * route call or v3's finalize, which pays the output. The router's calls
644
+ * are the pre-signed ones. A First Stage swap ends with the user's own tax
645
+ * payment and First Stage calls after it, so the last transaction isn't
646
+ * always the router's. Falls back to the last transaction.
647
+ */
648
+ findOutputTransactionIndex(txns) {
649
+ for (let i = txns.length - 1; i >= 0; i--) {
650
+ const t = txns[i];
651
+ if (t?.signature !== false && t?.txn.type === TransactionType.appl) return i;
652
+ }
653
+ return txns.length - 1;
654
+ }
655
+ /**
562
656
  * For a launch quote, finds the user-signed curve buy or sell call and
563
657
  * records the curve leg in summaryData. On a sell, the input amount becomes
564
658
  * the launch tokens sold.
@@ -653,11 +747,16 @@ var SwapComposer = class SwapComposer {
653
747
  */
654
748
  async executeMiddlewareHooks(hookName) {
655
749
  const allTxns = [];
656
- const quote = "createdAt" in this.quote ? this.quote : {
750
+ const quote = "createdAt" in this.quote ? {
751
+ ...this.quote,
752
+ signerType: this.signerType
753
+ } : {
657
754
  ...this.quote,
658
755
  quote: this.quote.quote === "" ? 0n : BigInt(this.quote.quote),
659
756
  amount: 0n,
660
757
  launch: this.quote.launch && toSwapLaunchQuote(this.quote.launch),
758
+ firstStage: this.quote.firstStage && toSwapFirstStageQuote(this.quote.firstStage),
759
+ signerType: this.signerType,
661
760
  createdAt: Date.now()
662
761
  };
663
762
  const quoteContext = {
@@ -804,14 +903,14 @@ async function request(url, options) {
804
903
  * @example
805
904
  * ```typescript
806
905
  * const router = new RouterClient({
807
- * apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
906
+ * apiKey: 'hay_pk_4zhj33wahuxfplteyif5xpcgnm', // Free tier (60 requests/min)
808
907
  * })
809
908
  * ```
810
909
  *
811
910
  * @example
812
911
  * ```typescript
813
912
  * const router = new RouterClient({
814
- * apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
913
+ * apiKey: 'hay_pk_4zhj33wahuxfplteyif5xpcgnm', // Free tier (60 requests/min)
815
914
  * apiBaseUrl: 'https://hayrouter.txnlab.dev',
816
915
  * algodUri: 'https://mainnet-api.4160.nodely.dev/',
817
916
  * algodToken: '',
@@ -891,8 +990,9 @@ var RouterClient = class {
891
990
  * ```
892
991
  */
893
992
  async fetchQuote(params) {
894
- const { fromASAID, toASAID, amount, type = "fixed-input", disabledProtocols = [], maxGroupSize = 16, maxDepth = 4, optIn, address, slippage, _allowNonComposableSwaps = false } = params;
993
+ const { fromASAID, toASAID, amount, type = "fixed-input", disabledProtocols = [], maxGroupSize = 16, maxDepth = 4, optIn, address, slippage, signerType, _allowNonComposableSwaps = false } = params;
895
994
  if (slippage !== void 0) this.validateSlippage(slippage);
995
+ if (signerType !== void 0 && signerType !== "ed25519" && signerType !== "falcon") throw new Error(`Invalid signerType: ${String(signerType)}. Must be 'ed25519' or 'falcon'`);
896
996
  if (_allowNonComposableSwaps && (optIn === true || this.config.autoOptIn)) throw new Error("Cannot use _allowNonComposableSwaps with optIn or autoOptIn. When allowing non-composable swaps (Tinyman v1), you must handle asset opt-ins and opt-outs in separate transaction groups.");
897
997
  const allDisabledProtocols = _allowNonComposableSwaps ? [...new Set(disabledProtocols)] : [.../* @__PURE__ */ new Set([...DEPRECATED_PROTOCOLS, ...disabledProtocols])];
898
998
  let includeOptIn = optIn;
@@ -913,7 +1013,9 @@ var RouterClient = class {
913
1013
  url.searchParams.append("maxGroupSize", maxGroupSize.toString());
914
1014
  url.searchParams.append("maxDepth", maxDepth.toString());
915
1015
  if (typeof includeOptIn === "boolean") url.searchParams.append("optIn", String(includeOptIn));
1016
+ if (address) url.searchParams.append("address", this.validateAddress(address));
916
1017
  if (slippage !== void 0) url.searchParams.append("slippage", String(slippage));
1018
+ if (signerType === "falcon") url.searchParams.append("signerType", signerType);
917
1019
  if (this.config.referrerAddress) url.searchParams.append("referrerAddress", this.config.referrerAddress);
918
1020
  return request(url.toString(), { headers: this.authHeaders() });
919
1021
  }
@@ -1034,7 +1136,7 @@ var RouterClient = class {
1034
1136
  };
1035
1137
  for (const mw of this.middleware) if (await mw.shouldApply(quoteContext) && mw.adjustQuoteParams) adjustedParams = await mw.adjustQuoteParams(adjustedParams);
1036
1138
  const response = await this.fetchQuote(adjustedParams);
1037
- const { launch, ...rest } = response;
1139
+ const { launch, firstStage, ...rest } = response;
1038
1140
  return {
1039
1141
  ...rest,
1040
1142
  quote: response.quote === "" ? 0n : BigInt(response.quote),
@@ -1042,6 +1144,8 @@ var RouterClient = class {
1042
1144
  address: params.address ?? void 0,
1043
1145
  slippage: adjustedParams.slippage,
1044
1146
  launch: launch && toSwapLaunchQuote(launch),
1147
+ firstStage: firstStage && toSwapFirstStageQuote(firstStage),
1148
+ signerType: adjustedParams.signerType ?? params.signerType ?? "ed25519",
1045
1149
  createdAt: Date.now()
1046
1150
  };
1047
1151
  }
@@ -1095,19 +1199,23 @@ var RouterClient = class {
1095
1199
  */
1096
1200
  async newSwap(config) {
1097
1201
  const { quote, address, slippage, signer, note } = config;
1202
+ const signerType = config.signerType ?? quote.signerType ?? "ed25519";
1098
1203
  if (quote.launch && "createdAt" in quote && quote.slippage !== slippage) console.warn(quote.slippage === void 0 ? `Launch token quote was fetched without slippage but the swap uses ${slippage}%. Pass the same slippage to newQuote() for an accurate quoted output.` : `Launch token quote was fetched with ${quote.slippage}% slippage but the swap uses ${slippage}%. The quoted output may not match what is delivered.`);
1204
+ const swapResponse = await this.fetchSwapTransactions({
1205
+ quote,
1206
+ address,
1207
+ slippage
1208
+ });
1099
1209
  return new SwapComposer({
1100
1210
  quote,
1101
- swapTxns: (await this.fetchSwapTransactions({
1102
- quote,
1103
- address,
1104
- slippage
1105
- })).txns,
1211
+ swapTxns: swapResponse.txns,
1106
1212
  algodClient: this.algodClient,
1107
1213
  address,
1108
1214
  signer,
1109
1215
  middleware: this.middleware,
1110
1216
  note,
1217
+ signerType,
1218
+ signerFeesIncluded: swapResponse.signerType === "falcon",
1111
1219
  debugLevel: this.config.debugLevel
1112
1220
  });
1113
1221
  }
@@ -1154,7 +1262,7 @@ var RouterClient = class {
1154
1262
  * })
1155
1263
  *
1156
1264
  * const router = new RouterClient({
1157
- * apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
1265
+ * apiKey: 'hay_pk_4zhj33wahuxfplteyif5xpcgnm', // Free tier (60 requests/min)
1158
1266
  * middleware: [autoOptOut],
1159
1267
  * })
1160
1268
  *
@@ -1210,6 +1318,6 @@ var AutoOptOutMiddleware = class {
1210
1318
  }];
1211
1319
  }
1212
1320
  };
1213
- export { API_KEY_HEADER, AutoOptOutMiddleware, DEFAULT_ALGOD_PORT, DEFAULT_ALGOD_TOKEN, DEFAULT_ALGOD_URI, DEFAULT_API_BASE_URL, DEFAULT_AUTO_OPT_IN, DEFAULT_CONFIRMATION_ROUNDS, DEFAULT_DEBUG_LEVEL, DEFAULT_FEE_BPS, DEFAULT_MAX_DEPTH, DEFAULT_MAX_GROUP_SIZE, DEPRECATED_PROTOCOLS, HTTPError, MAX_FEE_BPS, Protocol, RouterClient, SwapComposer, SwapComposerStatus, request };
1321
+ export { API_KEY_HEADER, AutoOptOutMiddleware, DEFAULT_ALGOD_PORT, DEFAULT_ALGOD_TOKEN, DEFAULT_ALGOD_URI, DEFAULT_API_BASE_URL, DEFAULT_AUTO_OPT_IN, DEFAULT_CONFIRMATION_ROUNDS, DEFAULT_DEBUG_LEVEL, DEFAULT_FEE_BPS, DEFAULT_MAX_DEPTH, DEFAULT_MAX_GROUP_SIZE, DEPRECATED_PROTOCOLS, FALCON_EXTRA_FEE, HTTPError, MAX_FEE_BPS, Protocol, RouterClient, SwapComposer, SwapComposerStatus, request };
1214
1322
 
1215
1323
  //# sourceMappingURL=index.mjs.map