@logswap/sdk 0.5.56 → 0.5.58
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/claims.js +15 -34
- package/dist/client.d.ts +7 -0
- package/dist/client.js +58 -1
- package/dist/fees.d.ts +11 -1
- package/dist/fees.js +34 -8
- package/dist/fpool.js +2 -2
- package/dist/onboard.d.ts +11 -0
- package/dist/onboard.js +23 -9
- package/dist/write.d.ts +13 -1
- package/dist/write.js +26 -1
- package/package.json +1 -1
- package/src/claims.ts +14 -36
- package/src/client.ts +60 -1
- package/src/fees.ts +32 -6
- package/src/fpool.ts +2 -2
- package/src/onboard.ts +25 -8
- package/src/write.ts +27 -2
package/dist/claims.js
CHANGED
|
@@ -59,40 +59,21 @@ export async function isRouterApproved(c, owner) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
export async function checkReadiness(c, key, owner, need = {}) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
]
|
|
74
|
-
const [operator, baseAllow, quoteAllow] = await Promise.all([
|
|
62
|
+
// A token is ready when BOTH legs of its pull path are: the ERC-20 allowance to whoever pulls
|
|
63
|
+
// (Permit2 when wired) and, with Permit2, its unexpired allowance to the router. This used to
|
|
64
|
+
// read the ERC-20 allowance to the ROUTER, which on a Permit2 deployment is never granted — so
|
|
65
|
+
// it said "approve" forever, and approving the router changed nothing.
|
|
66
|
+
const { checkTokenReadiness } = await import("./onboard.js");
|
|
67
|
+
const ready = async (token, amount) => {
|
|
68
|
+
if (amount === undefined)
|
|
69
|
+
return true;
|
|
70
|
+
const r = await checkTokenReadiness(c, token, owner, amount);
|
|
71
|
+
return !r.needsTokenApproval && !r.needsPermit2Approval;
|
|
72
|
+
};
|
|
73
|
+
const [operator, baseOk, quoteOk] = await Promise.all([
|
|
75
74
|
need.positions ? isRouterApproved(c, owner) : Promise.resolve(true),
|
|
76
|
-
need.base
|
|
77
|
-
|
|
78
|
-
address: key.base,
|
|
79
|
-
abi: erc20,
|
|
80
|
-
functionName: "allowance",
|
|
81
|
-
args: [owner, c.addresses.router],
|
|
82
|
-
})
|
|
83
|
-
: Promise.resolve(0n),
|
|
84
|
-
need.quote
|
|
85
|
-
? c.public.readContract({
|
|
86
|
-
address: key.quote,
|
|
87
|
-
abi: erc20,
|
|
88
|
-
functionName: "allowance",
|
|
89
|
-
args: [owner, c.addresses.router],
|
|
90
|
-
})
|
|
91
|
-
: Promise.resolve(0n),
|
|
75
|
+
ready(key.base, need.base),
|
|
76
|
+
ready(key.quote, need.quote),
|
|
92
77
|
]);
|
|
93
|
-
return {
|
|
94
|
-
needsOperator: !operator,
|
|
95
|
-
needsBaseAllowance: need.base !== undefined && baseAllow < need.base,
|
|
96
|
-
needsQuoteAllowance: need.quote !== undefined && quoteAllow < need.quote,
|
|
97
|
-
};
|
|
78
|
+
return { needsOperator: !operator, needsBaseAllowance: !baseOk, needsQuoteAllowance: !quoteOk };
|
|
98
79
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -85,3 +85,10 @@ export declare const EXPECTED_VERSION = 4n;
|
|
|
85
85
|
*/
|
|
86
86
|
export declare const EXPECTED_F_VERSION = 18n;
|
|
87
87
|
export declare function assertVersion(c: LogswapClient, expected?: bigint): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* The versions say the managers are the right code; this says the rest of the addresses belong
|
|
90
|
+
* to THEM. A stale router or lens from a previous deployment passes every version check and then
|
|
91
|
+
* fails in the worst way: approvals go to a router that pulls for another manager, or every pull
|
|
92
|
+
* reverts because the router's Permit2 is not the one the user approved.
|
|
93
|
+
*/
|
|
94
|
+
export declare function assertWiring(c: LogswapClient): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -8,7 +8,39 @@
|
|
|
8
8
|
* the protocol, not configuration that grows.
|
|
9
9
|
*/
|
|
10
10
|
export function createLogswapClient(args) {
|
|
11
|
-
return { public: args.public, wallet: args.wallet, addresses: args.addresses };
|
|
11
|
+
return { public: args.public, wallet: args.wallet && guardChain(args.wallet, args.public), addresses: args.addresses };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The wallet, with every signing call first checking that it is on the chain the reads come from.
|
|
15
|
+
*
|
|
16
|
+
* An injected wallet switched to another network would otherwise estimate and simulate against
|
|
17
|
+
* the public client's chain and then sign and send on its own — with deterministic deploy
|
|
18
|
+
* addresses, possibly into a different contract. Checked at each call, not once: a user can
|
|
19
|
+
* switch networks mid-session.
|
|
20
|
+
*/
|
|
21
|
+
function guardChain(wallet, pub) {
|
|
22
|
+
if (typeof wallet.getChainId !== "function")
|
|
23
|
+
return wallet; // a test double
|
|
24
|
+
let readChain = pub.chain?.id;
|
|
25
|
+
const check = async () => {
|
|
26
|
+
readChain ??= await pub.getChainId();
|
|
27
|
+
const walletChain = await wallet.getChainId();
|
|
28
|
+
if (walletChain !== readChain) {
|
|
29
|
+
throw new Error(`logswap: the wallet is on chain ${walletChain} but this app reads chain ${readChain} — switch the wallet's network`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const guarded = new Set(["writeContract", "sendTransaction", "signTypedData"]);
|
|
33
|
+
return new Proxy(wallet, {
|
|
34
|
+
get(target, prop, receiver) {
|
|
35
|
+
const v = Reflect.get(target, prop, receiver);
|
|
36
|
+
if (!guarded.has(prop) || typeof v !== "function")
|
|
37
|
+
return v;
|
|
38
|
+
return async (...a) => {
|
|
39
|
+
await check();
|
|
40
|
+
return v.apply(target, a);
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
12
44
|
}
|
|
13
45
|
/**
|
|
14
46
|
* Assert the deployment matches the ABI this build was generated against.
|
|
@@ -114,4 +146,29 @@ export async function assertVersion(c, expected = EXPECTED_VERSION) {
|
|
|
114
146
|
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`);
|
|
115
147
|
}
|
|
116
148
|
}
|
|
149
|
+
await assertWiring(c);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The versions say the managers are the right code; this says the rest of the addresses belong
|
|
153
|
+
* to THEM. A stale router or lens from a previous deployment passes every version check and then
|
|
154
|
+
* fails in the worst way: approvals go to a router that pulls for another manager, or every pull
|
|
155
|
+
* reverts because the router's Permit2 is not the one the user approved.
|
|
156
|
+
*/
|
|
157
|
+
export async function assertWiring(c) {
|
|
158
|
+
const { logswapRouterAbi, logswapLensAbi } = await import("./generated.js");
|
|
159
|
+
const rd = (address, abi, functionName) => c.public.readContract({ address, abi, functionName });
|
|
160
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
161
|
+
const want = [
|
|
162
|
+
["router.mgr()", rd(c.addresses.router, logswapRouterAbi, "mgr"), c.addresses.cPoolManager],
|
|
163
|
+
["router.permit2()", rd(c.addresses.router, logswapRouterAbi, "permit2"), c.addresses.permit2 ?? ZERO],
|
|
164
|
+
["lens.mgr()", rd(c.addresses.lens, logswapLensAbi, "mgr"), c.addresses.cPoolManager],
|
|
165
|
+
];
|
|
166
|
+
if (c.addresses.fPoolManager) {
|
|
167
|
+
want.push(["router.fmgr()", rd(c.addresses.router, logswapRouterAbi, "fmgr"), c.addresses.fPoolManager]);
|
|
168
|
+
want.push(["lens.fmgr()", rd(c.addresses.lens, logswapLensAbi, "fmgr"), c.addresses.fPoolManager]);
|
|
169
|
+
}
|
|
170
|
+
const got = await Promise.all(want.map((w) => w[1]));
|
|
171
|
+
const bad = want.flatMap(([what, , expected], i) => got[i].toLowerCase() === expected.toLowerCase() ? [] : [`${what} is ${got[i]}, expected ${expected}`]);
|
|
172
|
+
if (bad.length)
|
|
173
|
+
throw new Error(`logswap: the deployment's addresses do not belong together — ${bad.join("; ")}`);
|
|
117
174
|
}
|
package/dist/fees.d.ts
CHANGED
|
@@ -30,7 +30,17 @@ export interface SwapFeeWindow {
|
|
|
30
30
|
swapFees: bigint;
|
|
31
31
|
swaps: number;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The first block with timestamp ≥ `t` in `[lo, hi]`; `hi` when none is earlier.
|
|
35
|
+
*
|
|
36
|
+
* A PARALLEL search: each round reads {@link PROBES} evenly spaced blocks at once and keeps the
|
|
37
|
+
* sixteenth of the bracket that holds the answer, so 64k blocks take 4 round trips. Bisection read
|
|
38
|
+
* one block per round trip — ~16 sequential `getBlock`s, seconds apiece against a distant node,
|
|
39
|
+
* before a fee figure could render. Interpolating on timestamps was tried and did no better: a
|
|
40
|
+
* devnet mines on demand plus a heartbeat, so its blocks are far from evenly spaced in time.
|
|
41
|
+
* Exact for monotonic timestamps; a chain restored from a snapshot can carry a genesis stamped
|
|
42
|
+
* after its later blocks, so callers bound `lo` by the block they care about (a pool's own).
|
|
43
|
+
*/
|
|
34
44
|
export declare function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint, hi: bigint): Promise<bigint>;
|
|
35
45
|
/**
|
|
36
46
|
* Sum the swap fees of market `m` over a window: from `since` (unix seconds) or `fromBlock`, to
|
package/dist/fees.js
CHANGED
|
@@ -17,16 +17,42 @@ const YEAR_SECONDS = 31_536_000;
|
|
|
17
17
|
export function swapFeeOf(e, phi) {
|
|
18
18
|
return e.baseIn ? (e.amountOut * phi) / (WAD - phi) : (e.amountIn * phi) / (WAD + phi);
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** Probes per round of {@link blockAtOrAfter}: 15 splits the bracket 16 ways. */
|
|
21
|
+
const PROBES = 15n;
|
|
22
|
+
/**
|
|
23
|
+
* The first block with timestamp ≥ `t` in `[lo, hi]`; `hi` when none is earlier.
|
|
24
|
+
*
|
|
25
|
+
* A PARALLEL search: each round reads {@link PROBES} evenly spaced blocks at once and keeps the
|
|
26
|
+
* sixteenth of the bracket that holds the answer, so 64k blocks take 4 round trips. Bisection read
|
|
27
|
+
* one block per round trip — ~16 sequential `getBlock`s, seconds apiece against a distant node,
|
|
28
|
+
* before a fee figure could render. Interpolating on timestamps was tried and did no better: a
|
|
29
|
+
* devnet mines on demand plus a heartbeat, so its blocks are far from evenly spaced in time.
|
|
30
|
+
* Exact for monotonic timestamps; a chain restored from a snapshot can carry a genesis stamped
|
|
31
|
+
* after its later blocks, so callers bound `lo` by the block they care about (a pool's own).
|
|
32
|
+
*/
|
|
21
33
|
export async function blockAtOrAfter(c, t, lo, hi) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
if (lo >= hi)
|
|
35
|
+
return hi;
|
|
36
|
+
const stamp = async (n) => (await c.public.getBlock({ blockNumber: n })).timestamp;
|
|
37
|
+
if ((await stamp(lo)) >= t)
|
|
38
|
+
return lo;
|
|
39
|
+
// invariant from here: ts(lo) < t, and the answer is in (lo, hi]
|
|
40
|
+
while (hi - lo > 1n) {
|
|
41
|
+
const width = hi - lo;
|
|
42
|
+
const probes = [...new Set(Array.from({ length: Number(PROBES) }, (_, i) => lo + (width * BigInt(i + 1)) / (PROBES + 1n)))].filter((n) => n > lo && n < hi);
|
|
43
|
+
if (probes.length === 0)
|
|
44
|
+
probes.push(lo + width / 2n);
|
|
45
|
+
const stamps = await Promise.all(probes.map(stamp));
|
|
46
|
+
const first = stamps.findIndex((ts) => ts >= t);
|
|
47
|
+
if (first === -1)
|
|
48
|
+
lo = probes[probes.length - 1];
|
|
49
|
+
else {
|
|
50
|
+
hi = probes[first];
|
|
51
|
+
if (first > 0)
|
|
52
|
+
lo = probes[first - 1];
|
|
53
|
+
}
|
|
28
54
|
}
|
|
29
|
-
return
|
|
55
|
+
return hi;
|
|
30
56
|
}
|
|
31
57
|
/**
|
|
32
58
|
* Sum the swap fees of market `m` over a window: from `since` (unix seconds) or `fromBlock`, to
|
package/dist/fpool.js
CHANGED
|
@@ -45,7 +45,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
45
45
|
* in for the base — a call through it to any of the four policies decodes the base's functions.
|
|
46
46
|
*/
|
|
47
47
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
48
|
-
import { signerFor } from "./write.js";
|
|
48
|
+
import { fallBackUnlessRejected, signerFor } from "./write.js";
|
|
49
49
|
const WAD = 10n ** 18n;
|
|
50
50
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
51
51
|
/** The optional questions a policy declared at birth, as bits of `Pool.asks`. */
|
|
@@ -435,7 +435,7 @@ pullTokens = []) {
|
|
|
435
435
|
let sendArgs = args;
|
|
436
436
|
if (pullTokens.length) {
|
|
437
437
|
const { permit2SigCalls } = await import("./onboard.js");
|
|
438
|
-
const sigCalls = await permit2SigCalls(c, pullTokens).catch(
|
|
438
|
+
const sigCalls = await permit2SigCalls(c, pullTokens).catch(fallBackUnlessRejected);
|
|
439
439
|
if (sigCalls.length) {
|
|
440
440
|
const { encodeFunctionData } = await import("viem");
|
|
441
441
|
const inner = encodeFunctionData({ abi: logswapRouterAbi, functionName, args });
|
package/dist/onboard.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ import type { LogswapClient } from "./client.js";
|
|
|
18
18
|
export declare const MAX_UINT256: bigint;
|
|
19
19
|
export declare const MAX_UINT160: bigint;
|
|
20
20
|
export declare const MAX_UINT48: bigint;
|
|
21
|
+
/**
|
|
22
|
+
* How long a Permit2 allowance to the router lives: 30 days, as Uniswap's own interface does. It
|
|
23
|
+
* used to be `MAX_UINT48` — never — so a router bug or a compromised router could pull every
|
|
24
|
+
* approved token from every user indefinitely. A lapsed allowance is re-granted by one more
|
|
25
|
+
* signature, riding inside the next write ({@link permit2SigCalls}).
|
|
26
|
+
*/
|
|
27
|
+
export declare const PERMIT2_EXPIRY_SECONDS: bigint;
|
|
28
|
+
/** The expiry to grant now. */
|
|
29
|
+
export declare const permit2Expiry: () => bigint;
|
|
30
|
+
/** Whether a Permit2 allowance (amount, expiration) covers `need` for long enough to land. */
|
|
31
|
+
export declare const permit2Covers: (amount: bigint, expiration: number, need: bigint) => boolean;
|
|
21
32
|
/** Is this deployment's router wired to Permit2? Decides which approval path applies. */
|
|
22
33
|
export declare function usesPermit2(c: LogswapClient): boolean;
|
|
23
34
|
/** The contract that will actually pull tokens: Permit2 when wired, otherwise the router. */
|
package/dist/onboard.js
CHANGED
|
@@ -18,6 +18,20 @@ import { requireWallet } from "./write.js";
|
|
|
18
18
|
export const MAX_UINT256 = (1n << 256n) - 1n;
|
|
19
19
|
export const MAX_UINT160 = (1n << 160n) - 1n;
|
|
20
20
|
export const MAX_UINT48 = (1n << 48n) - 1n;
|
|
21
|
+
/**
|
|
22
|
+
* How long a Permit2 allowance to the router lives: 30 days, as Uniswap's own interface does. It
|
|
23
|
+
* used to be `MAX_UINT48` — never — so a router bug or a compromised router could pull every
|
|
24
|
+
* approved token from every user indefinitely. A lapsed allowance is re-granted by one more
|
|
25
|
+
* signature, riding inside the next write ({@link permit2SigCalls}).
|
|
26
|
+
*/
|
|
27
|
+
export const PERMIT2_EXPIRY_SECONDS = 30n * 24n * 3600n;
|
|
28
|
+
/** An allowance that lapses within this window is treated as already lapsed: it could die in flight. */
|
|
29
|
+
const EXPIRY_MARGIN_SECONDS = 3600;
|
|
30
|
+
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
|
31
|
+
/** The expiry to grant now. */
|
|
32
|
+
export const permit2Expiry = () => BigInt(nowSeconds()) + PERMIT2_EXPIRY_SECONDS;
|
|
33
|
+
/** Whether a Permit2 allowance (amount, expiration) covers `need` for long enough to land. */
|
|
34
|
+
export const permit2Covers = (amount, expiration, need) => amount >= need && expiration > nowSeconds() + EXPIRY_MARGIN_SECONDS;
|
|
21
35
|
const ERC20 = parseAbi([
|
|
22
36
|
"function approve(address spender, uint256 amount) returns (bool)",
|
|
23
37
|
"function allowance(address owner, address spender) view returns (uint256)",
|
|
@@ -54,7 +68,7 @@ export async function approveToken(c, token, amount = MAX_UINT256) {
|
|
|
54
68
|
* router, bundled through `multicall`) in production; this transaction form is what a test or a
|
|
55
69
|
* simple UI wants.
|
|
56
70
|
*/
|
|
57
|
-
export async function approvePermit2ForRouter(c, token, amount = MAX_UINT160, expiration =
|
|
71
|
+
export async function approvePermit2ForRouter(c, token, amount = MAX_UINT160, expiration = permit2Expiry()) {
|
|
58
72
|
if (!usesPermit2(c))
|
|
59
73
|
return null;
|
|
60
74
|
const { account } = requireWallet(c);
|
|
@@ -89,13 +103,13 @@ export async function onboardToken(c, token, need) {
|
|
|
89
103
|
sent.push(h);
|
|
90
104
|
}
|
|
91
105
|
if (usesPermit2(c)) {
|
|
92
|
-
const [amt] = (await c.public.readContract({
|
|
106
|
+
const [amt, expiration] = (await c.public.readContract({
|
|
93
107
|
address: c.addresses.permit2,
|
|
94
108
|
abi: PERMIT2,
|
|
95
109
|
functionName: "allowance",
|
|
96
110
|
args: [address, token, c.addresses.router],
|
|
97
111
|
}));
|
|
98
|
-
if (amt
|
|
112
|
+
if (!permit2Covers(amt, expiration, need)) {
|
|
99
113
|
const h = await approvePermit2ForRouter(c, token);
|
|
100
114
|
if (h) {
|
|
101
115
|
await c.public.waitForTransactionReceipt({ hash: h });
|
|
@@ -114,13 +128,13 @@ export async function checkTokenReadiness(c, token, owner, need) {
|
|
|
114
128
|
}));
|
|
115
129
|
let needsPermit2Approval = false;
|
|
116
130
|
if (usesPermit2(c)) {
|
|
117
|
-
const [amt] = (await c.public.readContract({
|
|
131
|
+
const [amt, expiration] = (await c.public.readContract({
|
|
118
132
|
address: c.addresses.permit2,
|
|
119
133
|
abi: PERMIT2,
|
|
120
134
|
functionName: "allowance",
|
|
121
135
|
args: [owner, token, c.addresses.router],
|
|
122
136
|
}));
|
|
123
|
-
needsPermit2Approval = amt
|
|
137
|
+
needsPermit2Approval = !permit2Covers(amt, expiration, need);
|
|
124
138
|
}
|
|
125
139
|
return { usesPermit2: usesPermit2(c), needsTokenApproval: allowance < need, needsPermit2Approval };
|
|
126
140
|
}
|
|
@@ -156,16 +170,16 @@ export async function permit2SigCalls(c, tokens) {
|
|
|
156
170
|
const chainId = await c.public.getChainId();
|
|
157
171
|
const calls = [];
|
|
158
172
|
for (const token of [...new Set(tokens.map((t) => t.toLowerCase()))]) {
|
|
159
|
-
const [amt, , nonce] = (await c.public.readContract({
|
|
173
|
+
const [amt, expiration, nonce] = (await c.public.readContract({
|
|
160
174
|
address: c.addresses.permit2,
|
|
161
175
|
abi: PERMIT2,
|
|
162
176
|
functionName: "allowance",
|
|
163
177
|
args: [owner, token, c.addresses.router],
|
|
164
178
|
}));
|
|
165
|
-
if (amt
|
|
166
|
-
continue; // effectively
|
|
179
|
+
if (permit2Covers(amt, expiration, 1n << 120n))
|
|
180
|
+
continue; // effectively unlimited, and not lapsing
|
|
167
181
|
const message = {
|
|
168
|
-
details: { token, amount: MAX_UINT160, expiration: Number(
|
|
182
|
+
details: { token, amount: MAX_UINT160, expiration: Number(permit2Expiry()), nonce },
|
|
169
183
|
spender: c.addresses.router,
|
|
170
184
|
sigDeadline: BigInt(Math.floor(Date.now() / 1000)) + 1800n,
|
|
171
185
|
};
|
package/dist/write.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* raw `minOut`/`maxIn` the contract wants. `withSlippage()` derives one from a quote when that is
|
|
13
13
|
* what you have — but the bound is what ships.
|
|
14
14
|
*/
|
|
15
|
-
import type { Address, Hash } from "viem";
|
|
15
|
+
import type { Address, Hash, Hex } from "viem";
|
|
16
16
|
import type { LogswapClient } from "./client.js";
|
|
17
17
|
/** Disables the deadline check. Deliberate opt-out, never a default. */
|
|
18
18
|
export declare const NEVER: bigint;
|
|
@@ -73,6 +73,18 @@ export declare function sendRouterWrite(c: LogswapClient, args: {
|
|
|
73
73
|
args: readonly unknown[];
|
|
74
74
|
bufferPct?: bigint;
|
|
75
75
|
}): Promise<Hash>;
|
|
76
|
+
/**
|
|
77
|
+
* Whether an error is the user saying no in their wallet (EIP-1193 code 4001, viem's
|
|
78
|
+
* `UserRejectedRequestError`), however deep viem nested it.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isUserRejection(e: unknown): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The permit signatures are an optimisation: when they cannot be produced (a wallet without
|
|
83
|
+
* `signTypedData`, a read that failed) the write goes out plain and relies on a standing
|
|
84
|
+
* allowance. A REJECTED signature is not that — it was swallowed too, and the user who had just
|
|
85
|
+
* said no was shown a transaction prompt, which then reverted with a Permit2 error that hid why.
|
|
86
|
+
*/
|
|
87
|
+
export declare function fallBackUnlessRejected(e: unknown): Hex[];
|
|
76
88
|
/** Simulate a router write and return its decoded result, without sending. */
|
|
77
89
|
export declare function simulateRouterWrite<T>(c: LogswapClient, args: {
|
|
78
90
|
abi: readonly unknown[];
|
package/dist/write.js
CHANGED
|
@@ -92,6 +92,31 @@ export async function sendRouterWrite(c, args) {
|
|
|
92
92
|
const gas = (estimate * (100n + (args.bufferPct ?? 30n))) / 100n;
|
|
93
93
|
return c.wallet.writeContract({ ...params, gas });
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether an error is the user saying no in their wallet (EIP-1193 code 4001, viem's
|
|
97
|
+
* `UserRejectedRequestError`), however deep viem nested it.
|
|
98
|
+
*/
|
|
99
|
+
export function isUserRejection(e) {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (let x = e; x && typeof x === "object" && !seen.has(x); x = x.cause) {
|
|
102
|
+
seen.add(x);
|
|
103
|
+
const o = x;
|
|
104
|
+
if (o.name === "UserRejectedRequestError" || o.code === 4001)
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The permit signatures are an optimisation: when they cannot be produced (a wallet without
|
|
111
|
+
* `signTypedData`, a read that failed) the write goes out plain and relies on a standing
|
|
112
|
+
* allowance. A REJECTED signature is not that — it was swallowed too, and the user who had just
|
|
113
|
+
* said no was shown a transaction prompt, which then reverted with a Permit2 error that hid why.
|
|
114
|
+
*/
|
|
115
|
+
export function fallBackUnlessRejected(e) {
|
|
116
|
+
if (isUserRejection(e))
|
|
117
|
+
throw e;
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
95
120
|
/** Simulate a router write and return its decoded result, without sending. */
|
|
96
121
|
export async function simulateRouterWrite(c, args) {
|
|
97
122
|
const account = args.account ?? c.wallet?.account?.address;
|
|
@@ -112,7 +137,7 @@ export async function simulateRouterWrite(c, args) {
|
|
|
112
137
|
*/
|
|
113
138
|
export async function sendRouterWriteWithPermits(c, args, pullTokens) {
|
|
114
139
|
const { permit2SigCalls } = await import("./onboard.js");
|
|
115
|
-
const sigCalls = await permit2SigCalls(c, pullTokens).catch(
|
|
140
|
+
const sigCalls = await permit2SigCalls(c, pullTokens).catch(fallBackUnlessRejected);
|
|
116
141
|
if (sigCalls.length === 0)
|
|
117
142
|
return sendRouterWrite(c, args);
|
|
118
143
|
const { encodeFunctionData } = await import("viem");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logswap/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.58",
|
|
4
4
|
"description": "TypeScript SDK for Logswap, the liquidity primitive introducing the logarithmic invariant — typed wrappers over the CPoolManager / FPoolManager / Router / Lens surface.",
|
|
5
5
|
"license": "GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
package/src/claims.ts
CHANGED
|
@@ -91,42 +91,20 @@ export async function checkReadiness(
|
|
|
91
91
|
owner: Address,
|
|
92
92
|
need: { base?: bigint; quote?: bigint; positions?: boolean } = {},
|
|
93
93
|
): Promise<Readiness> {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
] as const;
|
|
106
|
-
|
|
107
|
-
const [operator, baseAllow, quoteAllow] = await Promise.all([
|
|
94
|
+
// A token is ready when BOTH legs of its pull path are: the ERC-20 allowance to whoever pulls
|
|
95
|
+
// (Permit2 when wired) and, with Permit2, its unexpired allowance to the router. This used to
|
|
96
|
+
// read the ERC-20 allowance to the ROUTER, which on a Permit2 deployment is never granted — so
|
|
97
|
+
// it said "approve" forever, and approving the router changed nothing.
|
|
98
|
+
const { checkTokenReadiness } = await import("./onboard.js");
|
|
99
|
+
const ready = async (token: Address, amount?: bigint) => {
|
|
100
|
+
if (amount === undefined) return true;
|
|
101
|
+
const r = await checkTokenReadiness(c, token, owner, amount);
|
|
102
|
+
return !r.needsTokenApproval && !r.needsPermit2Approval;
|
|
103
|
+
};
|
|
104
|
+
const [operator, baseOk, quoteOk] = await Promise.all([
|
|
108
105
|
need.positions ? isRouterApproved(c, owner) : Promise.resolve(true),
|
|
109
|
-
need.base
|
|
110
|
-
|
|
111
|
-
address: key.base,
|
|
112
|
-
abi: erc20,
|
|
113
|
-
functionName: "allowance",
|
|
114
|
-
args: [owner, c.addresses.router],
|
|
115
|
-
}) as Promise<bigint>)
|
|
116
|
-
: Promise.resolve(0n),
|
|
117
|
-
need.quote
|
|
118
|
-
? (c.public.readContract({
|
|
119
|
-
address: key.quote,
|
|
120
|
-
abi: erc20,
|
|
121
|
-
functionName: "allowance",
|
|
122
|
-
args: [owner, c.addresses.router],
|
|
123
|
-
}) as Promise<bigint>)
|
|
124
|
-
: Promise.resolve(0n),
|
|
106
|
+
ready(key.base, need.base),
|
|
107
|
+
ready(key.quote, need.quote),
|
|
125
108
|
]);
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
needsOperator: !operator,
|
|
129
|
-
needsBaseAllowance: need.base !== undefined && baseAllow < need.base,
|
|
130
|
-
needsQuoteAllowance: need.quote !== undefined && quoteAllow < need.quote,
|
|
131
|
-
};
|
|
109
|
+
return { needsOperator: !operator, needsBaseAllowance: !baseOk, needsQuoteAllowance: !quoteOk };
|
|
132
110
|
}
|
package/src/client.ts
CHANGED
|
@@ -52,7 +52,38 @@ export function createLogswapClient(args: {
|
|
|
52
52
|
wallet?: WalletClient;
|
|
53
53
|
addresses: LogswapAddresses;
|
|
54
54
|
}): LogswapClient {
|
|
55
|
-
return { public: args.public, wallet: args.wallet, addresses: args.addresses };
|
|
55
|
+
return { public: args.public, wallet: args.wallet && guardChain(args.wallet, args.public), addresses: args.addresses };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The wallet, with every signing call first checking that it is on the chain the reads come from.
|
|
60
|
+
*
|
|
61
|
+
* An injected wallet switched to another network would otherwise estimate and simulate against
|
|
62
|
+
* the public client's chain and then sign and send on its own — with deterministic deploy
|
|
63
|
+
* addresses, possibly into a different contract. Checked at each call, not once: a user can
|
|
64
|
+
* switch networks mid-session.
|
|
65
|
+
*/
|
|
66
|
+
function guardChain(wallet: WalletClient, pub: PublicClient): WalletClient {
|
|
67
|
+
if (typeof wallet.getChainId !== "function") return wallet; // a test double
|
|
68
|
+
let readChain: number | undefined = pub.chain?.id;
|
|
69
|
+
const check = async () => {
|
|
70
|
+
readChain ??= await pub.getChainId();
|
|
71
|
+
const walletChain = await wallet.getChainId();
|
|
72
|
+
if (walletChain !== readChain) {
|
|
73
|
+
throw new Error(`logswap: the wallet is on chain ${walletChain} but this app reads chain ${readChain} — switch the wallet's network`);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const guarded = new Set<PropertyKey>(["writeContract", "sendTransaction", "signTypedData"]);
|
|
77
|
+
return new Proxy(wallet, {
|
|
78
|
+
get(target, prop, receiver) {
|
|
79
|
+
const v = Reflect.get(target, prop, receiver);
|
|
80
|
+
if (!guarded.has(prop) || typeof v !== "function") return v;
|
|
81
|
+
return async (...a: unknown[]) => {
|
|
82
|
+
await check();
|
|
83
|
+
return (v as (...x: unknown[]) => unknown).apply(target, a);
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
/**
|
|
@@ -164,4 +195,32 @@ export async function assertVersion(c: LogswapClient, expected = EXPECTED_VERSIO
|
|
|
164
195
|
);
|
|
165
196
|
}
|
|
166
197
|
}
|
|
198
|
+
await assertWiring(c);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The versions say the managers are the right code; this says the rest of the addresses belong
|
|
203
|
+
* to THEM. A stale router or lens from a previous deployment passes every version check and then
|
|
204
|
+
* fails in the worst way: approvals go to a router that pulls for another manager, or every pull
|
|
205
|
+
* reverts because the router's Permit2 is not the one the user approved.
|
|
206
|
+
*/
|
|
207
|
+
export async function assertWiring(c: LogswapClient): Promise<void> {
|
|
208
|
+
const { logswapRouterAbi, logswapLensAbi } = await import("./generated.js");
|
|
209
|
+
const rd = (address: Address, abi: readonly unknown[], functionName: string) =>
|
|
210
|
+
c.public.readContract({ address, abi, functionName } as never) as Promise<Address>;
|
|
211
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
212
|
+
const want: Array<[string, Promise<Address>, Address]> = [
|
|
213
|
+
["router.mgr()", rd(c.addresses.router, logswapRouterAbi, "mgr"), c.addresses.cPoolManager],
|
|
214
|
+
["router.permit2()", rd(c.addresses.router, logswapRouterAbi, "permit2"), c.addresses.permit2 ?? ZERO],
|
|
215
|
+
["lens.mgr()", rd(c.addresses.lens, logswapLensAbi, "mgr"), c.addresses.cPoolManager],
|
|
216
|
+
];
|
|
217
|
+
if (c.addresses.fPoolManager) {
|
|
218
|
+
want.push(["router.fmgr()", rd(c.addresses.router, logswapRouterAbi, "fmgr"), c.addresses.fPoolManager]);
|
|
219
|
+
want.push(["lens.fmgr()", rd(c.addresses.lens, logswapLensAbi, "fmgr"), c.addresses.fPoolManager]);
|
|
220
|
+
}
|
|
221
|
+
const got = await Promise.all(want.map((w) => w[1]));
|
|
222
|
+
const bad = want.flatMap(([what, , expected], i) =>
|
|
223
|
+
got[i]!.toLowerCase() === expected.toLowerCase() ? [] : [`${what} is ${got[i]}, expected ${expected}`],
|
|
224
|
+
);
|
|
225
|
+
if (bad.length) throw new Error(`logswap: the deployment's addresses do not belong together — ${bad.join("; ")}`);
|
|
167
226
|
}
|
package/src/fees.ts
CHANGED
|
@@ -36,14 +36,40 @@ export interface SwapFeeWindow {
|
|
|
36
36
|
swaps: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/** Probes per round of {@link blockAtOrAfter}: 15 splits the bracket 16 ways. */
|
|
40
|
+
const PROBES = 15n;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The first block with timestamp ≥ `t` in `[lo, hi]`; `hi` when none is earlier.
|
|
44
|
+
*
|
|
45
|
+
* A PARALLEL search: each round reads {@link PROBES} evenly spaced blocks at once and keeps the
|
|
46
|
+
* sixteenth of the bracket that holds the answer, so 64k blocks take 4 round trips. Bisection read
|
|
47
|
+
* one block per round trip — ~16 sequential `getBlock`s, seconds apiece against a distant node,
|
|
48
|
+
* before a fee figure could render. Interpolating on timestamps was tried and did no better: a
|
|
49
|
+
* devnet mines on demand plus a heartbeat, so its blocks are far from evenly spaced in time.
|
|
50
|
+
* Exact for monotonic timestamps; a chain restored from a snapshot can carry a genesis stamped
|
|
51
|
+
* after its later blocks, so callers bound `lo` by the block they care about (a pool's own).
|
|
52
|
+
*/
|
|
40
53
|
export async function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint, hi: bigint): Promise<bigint> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
if (lo >= hi) return hi;
|
|
55
|
+
const stamp = async (n: bigint) => (await c.public.getBlock({ blockNumber: n })).timestamp;
|
|
56
|
+
if ((await stamp(lo)) >= t) return lo;
|
|
57
|
+
// invariant from here: ts(lo) < t, and the answer is in (lo, hi]
|
|
58
|
+
while (hi - lo > 1n) {
|
|
59
|
+
const width = hi - lo;
|
|
60
|
+
const probes = [...new Set(Array.from({ length: Number(PROBES) }, (_, i) => lo + (width * BigInt(i + 1)) / (PROBES + 1n)))].filter(
|
|
61
|
+
(n) => n > lo && n < hi,
|
|
62
|
+
);
|
|
63
|
+
if (probes.length === 0) probes.push(lo + width / 2n);
|
|
64
|
+
const stamps = await Promise.all(probes.map(stamp));
|
|
65
|
+
const first = stamps.findIndex((ts) => ts >= t);
|
|
66
|
+
if (first === -1) lo = probes[probes.length - 1]!;
|
|
67
|
+
else {
|
|
68
|
+
hi = probes[first]!;
|
|
69
|
+
if (first > 0) lo = probes[first - 1]!;
|
|
70
|
+
}
|
|
45
71
|
}
|
|
46
|
-
return
|
|
72
|
+
return hi;
|
|
47
73
|
}
|
|
48
74
|
|
|
49
75
|
/**
|
package/src/fpool.ts
CHANGED
|
@@ -48,7 +48,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
48
48
|
*/
|
|
49
49
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
50
50
|
import type { LogswapClient } from "./client.js";
|
|
51
|
-
import { signerFor } from "./write.js";
|
|
51
|
+
import { fallBackUnlessRejected, signerFor } from "./write.js";
|
|
52
52
|
|
|
53
53
|
const WAD = 10n ** 18n;
|
|
54
54
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
|
|
@@ -676,7 +676,7 @@ async function writeRouter(
|
|
|
676
676
|
let sendArgs: unknown[] = args;
|
|
677
677
|
if (pullTokens.length) {
|
|
678
678
|
const { permit2SigCalls } = await import("./onboard.js");
|
|
679
|
-
const sigCalls = await permit2SigCalls(c, pullTokens).catch(
|
|
679
|
+
const sigCalls = await permit2SigCalls(c, pullTokens).catch(fallBackUnlessRejected);
|
|
680
680
|
if (sigCalls.length) {
|
|
681
681
|
const { encodeFunctionData } = await import("viem");
|
|
682
682
|
const inner = encodeFunctionData({ abi: logswapRouterAbi, functionName, args } as never);
|
package/src/onboard.ts
CHANGED
|
@@ -22,6 +22,23 @@ export const MAX_UINT256 = (1n << 256n) - 1n;
|
|
|
22
22
|
export const MAX_UINT160 = (1n << 160n) - 1n;
|
|
23
23
|
export const MAX_UINT48 = (1n << 48n) - 1n;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* How long a Permit2 allowance to the router lives: 30 days, as Uniswap's own interface does. It
|
|
27
|
+
* used to be `MAX_UINT48` — never — so a router bug or a compromised router could pull every
|
|
28
|
+
* approved token from every user indefinitely. A lapsed allowance is re-granted by one more
|
|
29
|
+
* signature, riding inside the next write ({@link permit2SigCalls}).
|
|
30
|
+
*/
|
|
31
|
+
export const PERMIT2_EXPIRY_SECONDS = 30n * 24n * 3600n;
|
|
32
|
+
/** An allowance that lapses within this window is treated as already lapsed: it could die in flight. */
|
|
33
|
+
const EXPIRY_MARGIN_SECONDS = 3600;
|
|
34
|
+
|
|
35
|
+
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
|
36
|
+
/** The expiry to grant now. */
|
|
37
|
+
export const permit2Expiry = (): bigint => BigInt(nowSeconds()) + PERMIT2_EXPIRY_SECONDS;
|
|
38
|
+
/** Whether a Permit2 allowance (amount, expiration) covers `need` for long enough to land. */
|
|
39
|
+
export const permit2Covers = (amount: bigint, expiration: number, need: bigint): boolean =>
|
|
40
|
+
amount >= need && expiration > nowSeconds() + EXPIRY_MARGIN_SECONDS;
|
|
41
|
+
|
|
25
42
|
const ERC20 = parseAbi([
|
|
26
43
|
"function approve(address spender, uint256 amount) returns (bool)",
|
|
27
44
|
"function allowance(address owner, address spender) view returns (uint256)",
|
|
@@ -71,7 +88,7 @@ export async function approvePermit2ForRouter(
|
|
|
71
88
|
c: LogswapClient,
|
|
72
89
|
token: Address,
|
|
73
90
|
amount: bigint = MAX_UINT160,
|
|
74
|
-
expiration: bigint =
|
|
91
|
+
expiration: bigint = permit2Expiry(),
|
|
75
92
|
): Promise<Hash | null> {
|
|
76
93
|
if (!usesPermit2(c)) return null;
|
|
77
94
|
const { account } = requireWallet(c);
|
|
@@ -109,13 +126,13 @@ export async function onboardToken(c: LogswapClient, token: Address, need: bigin
|
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
if (usesPermit2(c)) {
|
|
112
|
-
const [amt] = (await c.public.readContract({
|
|
129
|
+
const [amt, expiration] = (await c.public.readContract({
|
|
113
130
|
address: c.addresses.permit2!,
|
|
114
131
|
abi: PERMIT2,
|
|
115
132
|
functionName: "allowance",
|
|
116
133
|
args: [address, token, c.addresses.router],
|
|
117
134
|
})) as readonly [bigint, number, number];
|
|
118
|
-
if (amt
|
|
135
|
+
if (!permit2Covers(amt, expiration, need)) {
|
|
119
136
|
const h = await approvePermit2ForRouter(c, token);
|
|
120
137
|
if (h) {
|
|
121
138
|
await c.public.waitForTransactionReceipt({ hash: h });
|
|
@@ -150,13 +167,13 @@ export async function checkTokenReadiness(
|
|
|
150
167
|
|
|
151
168
|
let needsPermit2Approval = false;
|
|
152
169
|
if (usesPermit2(c)) {
|
|
153
|
-
const [amt] = (await c.public.readContract({
|
|
170
|
+
const [amt, expiration] = (await c.public.readContract({
|
|
154
171
|
address: c.addresses.permit2!,
|
|
155
172
|
abi: PERMIT2,
|
|
156
173
|
functionName: "allowance",
|
|
157
174
|
args: [owner, token, c.addresses.router],
|
|
158
175
|
})) as readonly [bigint, number, number];
|
|
159
|
-
needsPermit2Approval = amt
|
|
176
|
+
needsPermit2Approval = !permit2Covers(amt, expiration, need);
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
return { usesPermit2: usesPermit2(c), needsTokenApproval: allowance < need, needsPermit2Approval };
|
|
@@ -195,15 +212,15 @@ export async function permit2SigCalls(c: LogswapClient, tokens: Address[]): Prom
|
|
|
195
212
|
const chainId = await c.public.getChainId();
|
|
196
213
|
const calls: Hex[] = [];
|
|
197
214
|
for (const token of [...new Set(tokens.map((t) => t.toLowerCase() as Address))]) {
|
|
198
|
-
const [amt, , nonce] = (await c.public.readContract({
|
|
215
|
+
const [amt, expiration, nonce] = (await c.public.readContract({
|
|
199
216
|
address: c.addresses.permit2!,
|
|
200
217
|
abi: PERMIT2,
|
|
201
218
|
functionName: "allowance",
|
|
202
219
|
args: [owner, token, c.addresses.router],
|
|
203
220
|
})) as readonly [bigint, number, number];
|
|
204
|
-
if (amt
|
|
221
|
+
if (permit2Covers(amt, expiration, 1n << 120n)) continue; // effectively unlimited, and not lapsing
|
|
205
222
|
const message = {
|
|
206
|
-
details: { token, amount: MAX_UINT160, expiration: Number(
|
|
223
|
+
details: { token, amount: MAX_UINT160, expiration: Number(permit2Expiry()), nonce },
|
|
207
224
|
spender: c.addresses.router,
|
|
208
225
|
sigDeadline: BigInt(Math.floor(Date.now() / 1000)) + 1800n,
|
|
209
226
|
};
|
package/src/write.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* what you have — but the bound is what ships.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type { Address, Hash } from "viem";
|
|
16
|
+
import type { Address, Hash, Hex } from "viem";
|
|
17
17
|
import type { LogswapClient } from "./client.js";
|
|
18
18
|
|
|
19
19
|
/** Disables the deadline check. Deliberate opt-out, never a default. */
|
|
@@ -114,6 +114,31 @@ export async function sendRouterWrite(
|
|
|
114
114
|
return c.wallet!.writeContract({ ...(params as object), gas } as never);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Whether an error is the user saying no in their wallet (EIP-1193 code 4001, viem's
|
|
119
|
+
* `UserRejectedRequestError`), however deep viem nested it.
|
|
120
|
+
*/
|
|
121
|
+
export function isUserRejection(e: unknown): boolean {
|
|
122
|
+
const seen = new Set<unknown>();
|
|
123
|
+
for (let x: unknown = e; x && typeof x === "object" && !seen.has(x); x = (x as { cause?: unknown }).cause) {
|
|
124
|
+
seen.add(x);
|
|
125
|
+
const o = x as { name?: string; code?: number };
|
|
126
|
+
if (o.name === "UserRejectedRequestError" || o.code === 4001) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The permit signatures are an optimisation: when they cannot be produced (a wallet without
|
|
133
|
+
* `signTypedData`, a read that failed) the write goes out plain and relies on a standing
|
|
134
|
+
* allowance. A REJECTED signature is not that — it was swallowed too, and the user who had just
|
|
135
|
+
* said no was shown a transaction prompt, which then reverted with a Permit2 error that hid why.
|
|
136
|
+
*/
|
|
137
|
+
export function fallBackUnlessRejected(e: unknown): Hex[] {
|
|
138
|
+
if (isUserRejection(e)) throw e;
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/** Simulate a router write and return its decoded result, without sending. */
|
|
118
143
|
export async function simulateRouterWrite<T>(
|
|
119
144
|
c: LogswapClient,
|
|
@@ -142,7 +167,7 @@ export async function sendRouterWriteWithPermits(
|
|
|
142
167
|
pullTokens: Address[],
|
|
143
168
|
): Promise<Hash> {
|
|
144
169
|
const { permit2SigCalls } = await import("./onboard.js");
|
|
145
|
-
const sigCalls = await permit2SigCalls(c, pullTokens).catch(
|
|
170
|
+
const sigCalls = await permit2SigCalls(c, pullTokens).catch(fallBackUnlessRejected);
|
|
146
171
|
if (sigCalls.length === 0) return sendRouterWrite(c, args);
|
|
147
172
|
const { encodeFunctionData } = await import("viem");
|
|
148
173
|
const inner = encodeFunctionData({ abi: args.abi, functionName: args.functionName, args: args.args } as never);
|