@bananapus/nana-sdk-core 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.
Files changed (69) hide show
  1. package/dist/cjs/review/heldWrite.js +1 -1
  2. package/dist/cjs/review/heldWrite.js.map +1 -1
  3. package/dist/cjs/review/messages.d.ts +3 -1
  4. package/dist/cjs/review/messages.d.ts.map +1 -1
  5. package/dist/cjs/review/messages.js +4 -2
  6. package/dist/cjs/review/messages.js.map +1 -1
  7. package/dist/cjs/review/writeHold.d.ts +9 -6
  8. package/dist/cjs/review/writeHold.d.ts.map +1 -1
  9. package/dist/cjs/review/writeHold.js +12 -4
  10. package/dist/cjs/review/writeHold.js.map +1 -1
  11. package/dist/cjs/review/writeRecovery.d.ts +3 -2
  12. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  13. package/dist/cjs/review/writeRecovery.js +3 -2
  14. package/dist/cjs/review/writeRecovery.js.map +1 -1
  15. package/dist/cjs/v6/defifa.d.ts +4 -2
  16. package/dist/cjs/v6/defifa.d.ts.map +1 -1
  17. package/dist/cjs/v6/defifa.js +10 -6
  18. package/dist/cjs/v6/defifa.js.map +1 -1
  19. package/dist/cjs/v6/executionErrors.d.ts +69 -11
  20. package/dist/cjs/v6/executionErrors.d.ts.map +1 -1
  21. package/dist/cjs/v6/executionErrors.js +160 -75
  22. package/dist/cjs/v6/executionErrors.js.map +1 -1
  23. package/dist/cjs/v6/index.d.ts +2 -1
  24. package/dist/cjs/v6/index.d.ts.map +1 -1
  25. package/dist/cjs/v6/index.js +4 -1
  26. package/dist/cjs/v6/index.js.map +1 -1
  27. package/dist/cjs/v6/pay.d.ts +35 -8
  28. package/dist/cjs/v6/pay.d.ts.map +1 -1
  29. package/dist/cjs/v6/pay.js +487 -48
  30. package/dist/cjs/v6/pay.js.map +1 -1
  31. package/dist/cjs/v6/uniswapV3.d.ts +21 -0
  32. package/dist/cjs/v6/uniswapV3.d.ts.map +1 -0
  33. package/dist/cjs/v6/uniswapV3.js +72 -0
  34. package/dist/cjs/v6/uniswapV3.js.map +1 -0
  35. package/dist/esm/review/heldWrite.js +2 -2
  36. package/dist/esm/review/heldWrite.js.map +1 -1
  37. package/dist/esm/review/messages.d.ts +3 -1
  38. package/dist/esm/review/messages.d.ts.map +1 -1
  39. package/dist/esm/review/messages.js +3 -1
  40. package/dist/esm/review/messages.js.map +1 -1
  41. package/dist/esm/review/writeHold.d.ts +9 -6
  42. package/dist/esm/review/writeHold.d.ts.map +1 -1
  43. package/dist/esm/review/writeHold.js +12 -4
  44. package/dist/esm/review/writeHold.js.map +1 -1
  45. package/dist/esm/review/writeRecovery.d.ts +3 -2
  46. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  47. package/dist/esm/review/writeRecovery.js +3 -2
  48. package/dist/esm/review/writeRecovery.js.map +1 -1
  49. package/dist/esm/v6/defifa.d.ts +4 -2
  50. package/dist/esm/v6/defifa.d.ts.map +1 -1
  51. package/dist/esm/v6/defifa.js +10 -6
  52. package/dist/esm/v6/defifa.js.map +1 -1
  53. package/dist/esm/v6/executionErrors.d.ts +69 -11
  54. package/dist/esm/v6/executionErrors.d.ts.map +1 -1
  55. package/dist/esm/v6/executionErrors.js +159 -76
  56. package/dist/esm/v6/executionErrors.js.map +1 -1
  57. package/dist/esm/v6/index.d.ts +2 -1
  58. package/dist/esm/v6/index.d.ts.map +1 -1
  59. package/dist/esm/v6/index.js +2 -1
  60. package/dist/esm/v6/index.js.map +1 -1
  61. package/dist/esm/v6/pay.d.ts +35 -8
  62. package/dist/esm/v6/pay.d.ts.map +1 -1
  63. package/dist/esm/v6/pay.js +487 -49
  64. package/dist/esm/v6/pay.js.map +1 -1
  65. package/dist/esm/v6/uniswapV3.d.ts +21 -0
  66. package/dist/esm/v6/uniswapV3.d.ts.map +1 -0
  67. package/dist/esm/v6/uniswapV3.js +68 -0
  68. package/dist/esm/v6/uniswapV3.js.map +1 -0
  69. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAY_DEFAULT_SLIPPAGE_BPS = exports.PAY_QUOTE_FAILED_MESSAGE = void 0;
3
4
  exports.buildPayTx = buildPayTx;
4
5
  exports.previewPay = previewPay;
5
6
  exports.effectivePayPreview = effectivePayPreview;
@@ -12,7 +13,9 @@ const constants_js_1 = require("../constants.js");
12
13
  const juicebox_js_1 = require("../generated/juicebox.js");
13
14
  const hook_js_1 = require("../utils/hook.js");
14
15
  const cashOut_js_1 = require("./cashOut.js");
16
+ const terminals_js_1 = require("./terminals.js");
15
17
  const types_js_1 = require("./types.js");
18
+ const uniswapV3_js_1 = require("./uniswapV3.js");
16
19
  const uniswapV4_js_1 = require("./uniswapV4.js");
17
20
  /**
18
21
  * Build a `pay(projectId, token, amount, beneficiary, minReturnedTokens, memo,
@@ -288,6 +291,8 @@ async function resolvedBuybackHookOf(client, chainId, projectId) {
288
291
  return hook === viem_1.zeroAddress ? null : hook;
289
292
  }
290
293
  const maxBigInt = (a, b) => (a > b ? a : b);
294
+ /** What to tell the payer when {@link PreparedUserPay.quoteFailed} is set. */
295
+ exports.PAY_QUOTE_FAILED_MESSAGE = "Couldn't read the pool's price, so this payment's minimum is only the mint rate.";
291
296
  const poolKeyOfAbi = [
292
297
  {
293
298
  type: "function",
@@ -312,13 +317,337 @@ const poolKeyOfAbi = [
312
317
  ],
313
318
  },
314
319
  ];
320
+ /**
321
+ * Whether a preview reverted with the buyback hook's
322
+ * `JBBuybackHook_SpecifiedSlippageExceeded`, which viem keeps as `raw` on the
323
+ * error's cause chain. The full revert decoder would weigh on every pay card.
324
+ */
325
+ function rejectsMinimum(error) {
326
+ for (let e = error; e; e = e.cause) {
327
+ if (typeof e.raw === "string" && e.raw.startsWith("0xe2d708a9")) {
328
+ return true;
329
+ }
330
+ }
331
+ return false;
332
+ }
333
+ /** `prepareUserPay`'s tolerance on its quote and fallback minimum: 1%. */
334
+ exports.PAY_DEFAULT_SLIPPAGE_BPS = 100n;
335
+ /**
336
+ * The buyback pool a payment's spec reports, and the terminal token it is
337
+ * keyed to, or null. A router converts the paid token first, so its pool is
338
+ * keyed to another token the project accepts. The pool id is the key's hash,
339
+ * so a match proves the key.
340
+ */
341
+ async function buybackPoolOf(client, { chainId, hook, projectId, token, poolId, }) {
342
+ const poolOf = async (terminalToken) => ({
343
+ terminalToken,
344
+ key: await client.readContract({
345
+ address: hook,
346
+ abi: poolKeyOfAbi,
347
+ functionName: "poolKeyOf",
348
+ args: [
349
+ projectId,
350
+ terminalToken.toLowerCase() === constants_js_1.NATIVE_TOKEN.toLowerCase()
351
+ ? viem_1.zeroAddress
352
+ : terminalToken,
353
+ ],
354
+ }),
355
+ });
356
+ const paid = await poolOf(token);
357
+ if ((0, uniswapV4_js_1.uniswapV4PoolId)(paid.key) === poolId)
358
+ return paid;
359
+ const contexts = await (0, terminals_js_1.getAccountingContexts)(client, { chainId, projectId });
360
+ const pools = await Promise.all(contexts
361
+ .filter((c) => c.token.toLowerCase() !== token.toLowerCase())
362
+ .map((c) => poolOf(c.token)));
363
+ return pools.find((p) => (0, uniswapV4_js_1.uniswapV4PoolId)(p.key) === poolId) ?? null;
364
+ }
365
+ const previewPayForOutputs = juicebox_js_1.jbMultiTerminalAbi.find((item) => item.type === "function" && item.name === "previewPayFor").outputs;
366
+ // Only these reads of the router and core: their full ABIs would weigh on every pay card.
367
+ const routerConversionAbi = [
368
+ {
369
+ type: "function",
370
+ name: "projectIdOf",
371
+ stateMutability: "view",
372
+ inputs: [{ name: "token", type: "address" }],
373
+ outputs: [{ name: "", type: "uint256" }],
374
+ },
375
+ {
376
+ type: "function",
377
+ name: "primaryTerminalOf",
378
+ stateMutability: "view",
379
+ inputs: [
380
+ { name: "projectId", type: "uint256" },
381
+ { name: "token", type: "address" },
382
+ ],
383
+ outputs: [{ name: "", type: "address" }],
384
+ },
385
+ {
386
+ type: "function",
387
+ name: "previewTerminalPayOf",
388
+ stateMutability: "view",
389
+ inputs: [
390
+ { name: "destTerminal", type: "address" },
391
+ { name: "projectId", type: "uint256" },
392
+ { name: "token", type: "address" },
393
+ { name: "amount", type: "uint256" },
394
+ { name: "beneficiary", type: "address" },
395
+ { name: "metadata", type: "bytes" },
396
+ ],
397
+ outputs: previewPayForOutputs,
398
+ },
399
+ {
400
+ type: "function",
401
+ name: "terminalsOf",
402
+ stateMutability: "view",
403
+ inputs: [{ name: "projectId", type: "uint256" }],
404
+ outputs: [{ name: "", type: "address[]" }],
405
+ },
406
+ {
407
+ type: "function",
408
+ name: "terminalOf",
409
+ stateMutability: "view",
410
+ inputs: [{ name: "projectId", type: "uint256" }],
411
+ outputs: [{ name: "", type: "address" }],
412
+ },
413
+ {
414
+ type: "function",
415
+ name: "ROUTER",
416
+ stateMutability: "view",
417
+ inputs: [],
418
+ outputs: [{ name: "", type: "address" }],
419
+ },
420
+ {
421
+ type: "function",
422
+ name: "wrappedNativeToken",
423
+ stateMutability: "view",
424
+ inputs: [],
425
+ outputs: [{ name: "", type: "address" }],
426
+ },
427
+ {
428
+ type: "function",
429
+ name: "previewSwapAmountOutOf",
430
+ stateMutability: "view",
431
+ inputs: [
432
+ { name: "tokenIn", type: "address" },
433
+ { name: "tokenOut", type: "address" },
434
+ { name: "amount", type: "uint256" },
435
+ { name: "metadata", type: "bytes" },
436
+ ],
437
+ outputs: [{ name: "amountOut", type: "uint256" }],
438
+ },
439
+ {
440
+ type: "function",
441
+ name: "discoverBestPool",
442
+ stateMutability: "view",
443
+ inputs: [
444
+ { name: "normalizedTokenIn", type: "address" },
445
+ { name: "normalizedTokenOut", type: "address" },
446
+ ],
447
+ outputs: [
448
+ {
449
+ name: "",
450
+ type: "tuple",
451
+ components: [
452
+ { name: "isV4", type: "bool" },
453
+ { name: "v3Pool", type: "address" },
454
+ {
455
+ name: "v4Key",
456
+ type: "tuple",
457
+ components: [
458
+ { name: "currency0", type: "address" },
459
+ { name: "currency1", type: "address" },
460
+ { name: "fee", type: "uint24" },
461
+ { name: "tickSpacing", type: "int24" },
462
+ { name: "hooks", type: "address" },
463
+ ],
464
+ },
465
+ ],
466
+ },
467
+ ],
468
+ },
469
+ ];
470
+ const v3PoolFeeAbi = [
471
+ {
472
+ type: "function",
473
+ name: "fee",
474
+ stateMutability: "view",
475
+ inputs: [],
476
+ outputs: [{ name: "", type: "uint24" }],
477
+ },
478
+ ];
479
+ /**
480
+ * Whether a payment to `terminal` reaches the current router: directly,
481
+ * through the gateway, or through the registry's terminal for the project
482
+ * (which may be the gateway). The conversion reads quote that router.
483
+ */
484
+ async function reachesRouter(client, chainId, projectId, terminal) {
485
+ const addressOf = (name) => {
486
+ try {
487
+ return (0, types_js_1.v6Address)(name, chainId).toLowerCase();
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ };
493
+ let reached = terminal.toLowerCase();
494
+ if (reached === addressOf("JBRouterTerminalRegistry")) {
495
+ reached = (await client.readContract({
496
+ address: terminal,
497
+ abi: routerConversionAbi,
498
+ functionName: "terminalOf",
499
+ args: [projectId],
500
+ })).toLowerCase();
501
+ }
502
+ if (reached === addressOf("JBRouterTerminalGateway")) {
503
+ reached = (await client.readContract({
504
+ address: reached,
505
+ abi: routerConversionAbi,
506
+ functionName: "ROUTER",
507
+ })).toLowerCase();
508
+ }
509
+ const router = addressOf("JBRouterTerminal");
510
+ return router !== null && reached === router;
511
+ }
512
+ /**
513
+ * What the router terminal's swap of the whole payment into `tokenOut` is
514
+ * quoted to return (fee and price impact included), and the floor it reverts
515
+ * below, from the pool the router picks. Null when the route is not that one
516
+ * swap, such as when the router cashes a project token out first.
517
+ */
518
+ async function routerConversionOf(client, { chainId, tokenIn, tokenOut, amount, }) {
519
+ const router = (0, types_js_1.v6Address)("JBRouterTerminal", chainId);
520
+ const [sourceProjectId, wrapped] = await Promise.all([
521
+ client.readContract({
522
+ address: (0, types_js_1.v6Address)("JBTokens", chainId),
523
+ abi: routerConversionAbi,
524
+ functionName: "projectIdOf",
525
+ args: [tokenIn],
526
+ }),
527
+ client.readContract({
528
+ address: router,
529
+ abi: routerConversionAbi,
530
+ functionName: "wrappedNativeToken",
531
+ }),
532
+ ]);
533
+ // The router cashes a project token out before any swap.
534
+ if (sourceProjectId !== 0n)
535
+ return null;
536
+ const isNative = (t) => t.toLowerCase() === constants_js_1.NATIVE_TOKEN.toLowerCase() ||
537
+ t.toLowerCase() === viem_1.zeroAddress;
538
+ const wrap = (t) => (isNative(t) ? wrapped : t);
539
+ // The router wraps or unwraps the native token 1:1, without a pool.
540
+ if (wrap(tokenIn).toLowerCase() === wrap(tokenOut).toLowerCase())
541
+ return null;
542
+ const [floor, pool] = await Promise.all([
543
+ client.readContract({
544
+ address: router,
545
+ abi: routerConversionAbi,
546
+ functionName: "previewSwapAmountOutOf",
547
+ args: [tokenIn, tokenOut, amount, "0x"],
548
+ }),
549
+ client.readContract({
550
+ address: router,
551
+ abi: routerConversionAbi,
552
+ functionName: "discoverBestPool",
553
+ args: [wrap(tokenIn), wrap(tokenOut)],
554
+ }),
555
+ ]);
556
+ let expected;
557
+ if (pool.isV4) {
558
+ // V4 pools hold the native token itself, which the router uses for wrapped input.
559
+ const unwrap = (t) => wrap(t).toLowerCase() === wrapped.toLowerCase() ? viem_1.zeroAddress : t;
560
+ const zeroForOne = (0, uniswapV4_js_1.uniswapV4SwapDirection)({
561
+ poolKey: pool.v4Key,
562
+ tokenIn: unwrap(tokenIn),
563
+ tokenOut: unwrap(tokenOut),
564
+ });
565
+ if (zeroForOne === null)
566
+ return null;
567
+ expected = await (0, uniswapV4_js_1.quoteUniswapV4ExactInputSingle)(client, {
568
+ chainId,
569
+ poolKey: pool.v4Key,
570
+ zeroForOne,
571
+ amountIn: amount,
572
+ });
573
+ }
574
+ else {
575
+ expected = await (0, uniswapV3_js_1.quoteUniswapV3ExactInputSingle)(client, {
576
+ chainId,
577
+ tokenIn: wrap(tokenIn),
578
+ tokenOut: wrap(tokenOut),
579
+ fee: await client.readContract({
580
+ address: pool.v3Pool,
581
+ abi: v3PoolFeeAbi,
582
+ functionName: "fee",
583
+ }),
584
+ amountIn: amount,
585
+ });
586
+ }
587
+ return expected >= floor ? { expected, floor } : null;
588
+ }
589
+ /**
590
+ * The destination terminal's preview of a router payment whose conversion
591
+ * returns what it is quoted to, and the router's floor. Null when the route is
592
+ * not one swap of the whole payment.
593
+ */
594
+ async function quotedRoutePreviewOf(client, { chainId, projectId, tokenIn, tokenOut, amount, beneficiary, metadata, }) {
595
+ const conversion = await routerConversionOf(client, {
596
+ chainId,
597
+ tokenIn,
598
+ tokenOut,
599
+ amount,
600
+ });
601
+ if (!conversion)
602
+ return null;
603
+ const destination = await client.readContract({
604
+ address: (0, types_js_1.v6Address)("JBDirectory", chainId),
605
+ abi: routerConversionAbi,
606
+ functionName: "primaryTerminalOf",
607
+ args: [projectId, tokenOut],
608
+ });
609
+ const [, beneficiaryTokenCount, reservedTokenCount, specs] = await client.readContract({
610
+ address: (0, types_js_1.v6Address)("JBRouterTerminal", chainId),
611
+ abi: routerConversionAbi,
612
+ functionName: "previewTerminalPayOf",
613
+ args: [
614
+ destination,
615
+ projectId,
616
+ tokenOut,
617
+ conversion.expected,
618
+ beneficiary,
619
+ metadata,
620
+ ],
621
+ });
622
+ return {
623
+ preview: { beneficiaryTokenCount, reservedTokenCount },
624
+ specs,
625
+ floor: conversion.floor,
626
+ };
627
+ }
628
+ /**
629
+ * The buyback spec the destination returns when the router's conversion
630
+ * returns what it is quoted to, and the router's floor. The hook's input is
631
+ * the conversion less anything taken before it, such as an NFT split, and the
632
+ * hook mints instead of swapping once minting that input reaches a fixed
633
+ * minimum, so the minimum is set from this spec. Null when the route is not
634
+ * one swap of the whole payment.
635
+ */
636
+ async function routedBuybackSpecOf(client, { hook, reservedPercent, ...route }) {
637
+ const routed = await quotedRoutePreviewOf(client, route);
638
+ if (!routed)
639
+ return null;
640
+ const raw = routed.specs.find((s) => s.hook.toLowerCase() === hook.toLowerCase());
641
+ const spec = raw && decodeBuybackPaySpec(raw, reservedPercent);
642
+ return spec ? { spec, floor: routed.floor } : null;
643
+ }
315
644
  /**
316
645
  * Prepare a person's payment with {@link buildUserPayMetadata}: preview it,
317
646
  * and set the minimum the terminal enforces on the beneficiary's balance.
318
647
  *
319
- * When the project's buyback hook has a pool in the paid token, this quotes
320
- * that pool (fee and price impact included) and sends the slippage-floored
321
- * quote as an explicit minimum, so the payer gets the pool's rate whenever it
648
+ * When the project's buyback hook would swap, this quotes its pool (fee and
649
+ * price impact included) and sends the slippage-floored quote as an explicit
650
+ * minimum, so the payer gets the pool's rate whenever it
322
651
  * beats minting. The hook compares that minimum to the swap output plus the
323
652
  * leftover mint: the whole leftover mint when the swapped tokens go through
324
653
  * the reserved split (`skipSplits` false, and always on hook 1.1.1, which
@@ -330,23 +659,40 @@ const poolKeyOfAbi = [
330
659
  * which reaches zero on large buys against a thin pool and sends them to
331
660
  * minting even when the pool pays far more.
332
661
  *
333
- * Otherwise (no pool, a router converting the token first, or a failed quote)
334
- * the minimum is the payer's mint entitlement when the hook would swap, since
335
- * the hook never settles below it, and the terminal's own issuance when it
336
- * would not. If the payment then reverts, {@link decodePayFailure} says why.
662
+ * Behind a router terminal, its registry or its gateway, when the router
663
+ * converts the paid token first with one swap, the pool is the one in the
664
+ * token it converts to, quoted on what that swap is quoted to return. If the
665
+ * conversion returns noticeably less, the hook's minimum reverts the payment.
666
+ * If it returns more, the hook mints once minting reaches the minimum, which
667
+ * happens only when the pool beats minting by less than `slippageBps` plus
668
+ * that gain. The payer still receives at least the minimum, and no fixed
669
+ * minimum avoids this when the pool's edge is inside the conversion's
670
+ * uncertainty. Other routes, such as cashing a project token out first, keep
671
+ * the fallback. When the project has no buyback hook and accepts one token,
672
+ * the minimum is what minting the quoted conversion issues, instead of the
673
+ * router's floor.
674
+ *
675
+ * Otherwise (no pool, or a failed quote) the minimum is the payer's mint
676
+ * entitlement when the hook would swap, since the hook never settles below it,
677
+ * and the terminal's own issuance when it would not. A quote whose reads fail
678
+ * twice returns `quoteFailed`, so the app can say the pool's rate is not
679
+ * protected. If the payment then
680
+ * reverts, {@link decodePayFailure} says why. To let the payer accept less,
681
+ * prepare again with a larger `slippageBps`: the hook's minimum is in the
682
+ * metadata, so lowering `minReturnedTokens` alone does not relax it.
337
683
  *
338
684
  * When the ruleset calls buyback hook 1.4 directly (or through the omnichain
339
685
  * deployer) instead of through the registry, the metadata is keyed to the hook,
340
686
  * which reads nothing else.
341
687
  *
342
688
  * @param args.slippageBps Tolerance on the pool quote and on the fallback
343
- * minimum. Defaults to 100 (1%).
689
+ * minimum. Defaults to {@link PAY_DEFAULT_SLIPPAGE_BPS}.
344
690
  * @param args.skipSplits Whether the beneficiary keeps the swapped tokens
345
691
  * without the reserved split. Defaults to true, as a person's pay card does;
346
692
  * pass false for payments that should honor the project's splits, such as
347
693
  * fees or revenue routed into a project.
348
694
  */
349
- async function prepareUserPay(client, { tiers, slippageBps = 100n, skipSplits = true, ...args }) {
695
+ async function prepareUserPay(client, { tiers, slippageBps = exports.PAY_DEFAULT_SLIPPAGE_BPS, skipSplits = true, ...args }) {
350
696
  const { chainId, projectId, token } = args;
351
697
  let metadata = buildUserPayMetadata({ chainId, tiers, skipSplits });
352
698
  let read = await readPay(client, { ...args, metadata });
@@ -368,32 +714,115 @@ async function prepareUserPay(client, { tiers, slippageBps = 100n, skipSplits =
368
714
  maxBigInt(mintEntitlementOf(buyback, weight) - 2n, 0n)
369
715
  : terminalBeneficiaryTokenCount, slippageBps),
370
716
  };
717
+ // A failed read is retried once: a dropped request must not silently weaken
718
+ // the minimum.
719
+ const withRetry = async (prepare) => {
720
+ try {
721
+ return await prepare();
722
+ }
723
+ catch {
724
+ try {
725
+ return await prepare();
726
+ }
727
+ catch {
728
+ return { ...fallback, quoteFailed: true };
729
+ }
730
+ }
731
+ };
732
+ const isRouter = args.terminal.toLowerCase() !==
733
+ (0, types_js_1.v6Address)("JBMultiTerminal", chainId).toLowerCase();
734
+ if (!buyback && isRouter) {
735
+ // The router previews minting on its conversion's floor, which sits up to
736
+ // its sigmoid slippage below the pool's quote. Require what minting the
737
+ // quoted conversion issues when the router has only that route: the
738
+ // project's one terminal is the multi-terminal, accepting one token, and
739
+ // the payment reaches the router these reads quote.
740
+ // ponytail: one accepted token only; multi-token projects need the
741
+ // router's route pick exposed.
742
+ return withRetry(async () => {
743
+ const [terminals, contexts, viaRouter] = await Promise.all([
744
+ client.readContract({
745
+ address: (0, types_js_1.v6Address)("JBDirectory", chainId),
746
+ abi: routerConversionAbi,
747
+ functionName: "terminalsOf",
748
+ args: [projectId],
749
+ }),
750
+ (0, terminals_js_1.getAccountingContexts)(client, { chainId, projectId }),
751
+ reachesRouter(client, chainId, projectId, args.terminal),
752
+ ]);
753
+ const tokenOut = contexts.length === 1 ? contexts[0].token : null;
754
+ if (!viaRouter ||
755
+ !tokenOut ||
756
+ terminals.length !== 1 ||
757
+ terminals[0].toLowerCase() !==
758
+ (0, types_js_1.v6Address)("JBMultiTerminal", chainId).toLowerCase()) {
759
+ return fallback;
760
+ }
761
+ const routed = await quotedRoutePreviewOf(client, {
762
+ chainId,
763
+ projectId,
764
+ tokenIn: token,
765
+ tokenOut,
766
+ amount: args.amount,
767
+ beneficiary: args.beneficiary,
768
+ metadata,
769
+ });
770
+ if (!routed)
771
+ return fallback;
772
+ return {
773
+ metadata,
774
+ preview: routed.preview,
775
+ minReturnedTokens: (0, cashOut_js_1.slippageFloor)(routed.preview.beneficiaryTokenCount, slippageBps),
776
+ };
777
+ });
778
+ }
371
779
  if (!buyback || buyback.amountToSwapWith === 0n)
372
780
  return fallback;
373
- try {
374
- const poolKey = await client.readContract({
375
- address: buyback.hook,
376
- abi: poolKeyOfAbi,
377
- functionName: "poolKeyOf",
378
- args: [
379
- projectId,
380
- token.toLowerCase() === constants_js_1.NATIVE_TOKEN.toLowerCase()
381
- ? viem_1.zeroAddress
382
- : token,
383
- ],
781
+ return withRetry(async () => {
782
+ const pool = await buybackPoolOf(client, {
783
+ chainId,
784
+ hook: buyback.hook,
785
+ projectId,
786
+ token,
787
+ poolId: buyback.poolId,
384
788
  });
385
- // A router converts the paid token first, so its pool is a different one.
386
- if ((0, uniswapV4_js_1.uniswapV4PoolId)(poolKey) !== buyback.poolId)
789
+ if (!pool)
387
790
  return fallback;
791
+ let swap = buyback;
792
+ if (pool.terminalToken !== token) {
793
+ // A router converts the payment first, and its preview gave the hook the
794
+ // router's slippage floor. A minimum quoted on that would let a normal
795
+ // conversion mint while the pool pays more, so quote what the hook
796
+ // receives when the conversion returns its quote. A conversion well
797
+ // below its quote then fails with a reason.
798
+ const routed = await routedBuybackSpecOf(client, {
799
+ chainId,
800
+ projectId,
801
+ tokenIn: token,
802
+ tokenOut: pool.terminalToken,
803
+ amount: args.amount,
804
+ beneficiary: args.beneficiary,
805
+ metadata,
806
+ hook: buyback.hook,
807
+ reservedPercent: buyback.reservedPercent,
808
+ });
809
+ // The floor preview must be this route's: no more than the floor reached the hook.
810
+ if (!routed ||
811
+ routed.floor < buyback.amountToSwapWith ||
812
+ routed.spec.poolId !== buyback.poolId) {
813
+ return fallback;
814
+ }
815
+ swap = routed.spec;
816
+ }
388
817
  const quote = await (0, uniswapV4_js_1.quoteUniswapV4ExactInputSingle)(client, {
389
818
  chainId,
390
- poolKey,
391
- zeroForOne: !buyback.projectTokenIs0,
392
- amountIn: buyback.amountToSwapWith,
819
+ poolKey: pool.key,
820
+ zeroForOne: !swap.projectTokenIs0,
821
+ amountIn: swap.amountToSwapWith,
393
822
  });
394
823
  const minimumSwapAmountOut = (0, cashOut_js_1.slippageFloor)(quote, slippageBps);
395
824
  // The pool loses to minting: leave the payment on the mint path.
396
- if (minimumSwapAmountOut <= buyback.tokenCountWithoutHook)
825
+ if (minimumSwapAmountOut <= swap.tokenCountWithoutHook)
397
826
  return fallback;
398
827
  const quotedMetadata = buildUserPayMetadata({
399
828
  chainId,
@@ -404,33 +833,46 @@ async function prepareUserPay(client, { tiers, slippageBps = 100n, skipSplits =
404
833
  });
405
834
  // The hook rejects a minimum it cannot route, such as on a pool with no
406
835
  // liquidity in range, and then the payment stays on the mint path.
407
- const [, quotedBeneficiaryCount, quotedReservedCount, quotedSpecs] = await client.readContract({
408
- address: args.terminal,
409
- abi: juicebox_js_1.jbMultiTerminalAbi,
410
- functionName: "previewPayFor",
411
- args: [projectId, token, args.amount, args.beneficiary, quotedMetadata],
412
- });
413
- const swaps = quotedSpecs.some((s) => !s.noop && s.hook.toLowerCase() === buyback.hook.toLowerCase());
414
- if (!swaps)
836
+ let quotedPreview;
837
+ try {
838
+ quotedPreview = await client.readContract({
839
+ address: args.terminal,
840
+ abi: juicebox_js_1.jbMultiTerminalAbi,
841
+ functionName: "previewPayFor",
842
+ args: [projectId, token, args.amount, args.beneficiary, quotedMetadata],
843
+ });
844
+ }
845
+ catch (error) {
846
+ if (rejectsMinimum(error))
847
+ return fallback;
848
+ throw error;
849
+ }
850
+ const [, quotedBeneficiaryCount, quotedReservedCount, quotedSpecs] = quotedPreview;
851
+ // It must still swap, and in the quoted pool: a router may pick another route.
852
+ const quotedSpec = quotedSpecs.find((s) => s.hook.toLowerCase() === buyback.hook.toLowerCase());
853
+ if (!quotedSpec ||
854
+ quotedSpec.noop ||
855
+ decodeBuybackPaySpec(quotedSpec, buyback.reservedPercent)?.poolId !==
856
+ buyback.poolId) {
415
857
  return fallback;
858
+ }
416
859
  // A router folds the hook's estimate into its counts, so only the
417
860
  // multi-terminal's own counts are known to be its issuance.
418
- const issuedByTerminal = args.terminal.toLowerCase() ===
419
- (0, types_js_1.v6Address)("JBMultiTerminal", chainId).toLowerCase()
420
- ? {
861
+ const issuedByTerminal = isRouter
862
+ ? { beneficiaryTokenCount: 0n, reservedTokenCount: 0n }
863
+ : {
421
864
  beneficiaryTokenCount: quotedBeneficiaryCount,
422
865
  reservedTokenCount: quotedReservedCount,
423
- }
424
- : { beneficiaryTokenCount: 0n, reservedTokenCount: 0n };
425
- const directMint = directMintOf(buyback, weight);
866
+ };
867
+ const directMint = directMintOf(swap, weight);
426
868
  // The swapped tokens skip the split, or are reminted through it with the
427
869
  // direct mint. The terminal's own issuance, such as for NFT splits, adds.
428
870
  const settle = (swapTokenCount) => {
429
- const split = buyback.skipSplits
430
- ? splitOf(directMint, buyback.reservedPercent)
431
- : splitOf(swapTokenCount + directMint, buyback.reservedPercent);
871
+ const split = swap.skipSplits
872
+ ? splitOf(directMint, swap.reservedPercent)
873
+ : splitOf(swapTokenCount + directMint, swap.reservedPercent);
432
874
  return {
433
- beneficiaryTokenCount: (buyback.skipSplits ? swapTokenCount : 0n) +
875
+ beneficiaryTokenCount: (swap.skipSplits ? swapTokenCount : 0n) +
434
876
  split.beneficiaryTokenCount +
435
877
  issuedByTerminal.beneficiaryTokenCount,
436
878
  reservedTokenCount: split.reservedTokenCount + issuedByTerminal.reservedTokenCount,
@@ -441,10 +883,7 @@ async function prepareUserPay(client, { tiers, slippageBps = 100n, skipSplits =
441
883
  preview: settle(quote),
442
884
  minReturnedTokens: settle(minimumSwapAmountOut).beneficiaryTokenCount,
443
885
  };
444
- }
445
- catch {
446
- return fallback;
447
- }
886
+ });
448
887
  }
449
888
  /**
450
889
  * Build pay metadata instructing a project's JB721 tiers hook to mint specific tiers.