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