@logswap/sdk 0.5.55 → 0.5.57

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/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 = MAX_UINT48,
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 < need) {
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 < need;
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 >= 1n << 120n) continue; // effectively infinite already
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(MAX_UINT48), nonce },
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/pools.ts CHANGED
@@ -31,7 +31,7 @@ export interface PoolState {
31
31
  reserveQuote: bigint;
32
32
  /** Fee-per-unit-L accumulator: $\phi \cdot \mathrm{TV}(\log p)$. */
33
33
  F: bigint;
34
- /** Quadratic variation of log-price, $\Sigma = \sum_{\text{swaps}} (\Delta x_{\text{swap}})^2$ — one term per swap (decisions 042). */
34
+ /** Quadratic variation of log-price, $\Sigma = \sum_{\text{swaps}} (\Delta x_{\text{swap}})^2$ — one term per swap. */
35
35
  bigSigma: bigint;
36
36
  currentK: bigint;
37
37
  protocolOwed: bigint;
@@ -52,7 +52,7 @@ export async function getPool(c: LogswapClient, key: PoolKey): Promise<PoolState
52
52
 
53
53
  /**
54
54
  * The market's fee: the key's `phi` with `0n` resolved to `DEFAULT_FEE`. Constant for the market's
55
- * life (C version 4, decisions 042 — `phiEff` and the variance kernel before). Reverts for a key
55
+ * life (C version 4, — `phiEff` and the variance kernel before). Reverts for a key
56
56
  * nobody initialized, which `discoverMarkets` relies on to drop a stale indexer's rows.
57
57
  */
58
58
  export async function phiOf(c: LogswapClient, key: PoolKey): Promise<bigint> {
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);