@logswap/sdk 0.5.35

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 (53) hide show
  1. package/README.md +130 -0
  2. package/dist/actions.d.ts +136 -0
  3. package/dist/actions.js +140 -0
  4. package/dist/admin.d.ts +11 -0
  5. package/dist/admin.js +29 -0
  6. package/dist/claims.d.ts +53 -0
  7. package/dist/claims.js +98 -0
  8. package/dist/client.d.ts +68 -0
  9. package/dist/client.js +68 -0
  10. package/dist/fpool.d.ts +416 -0
  11. package/dist/fpool.js +653 -0
  12. package/dist/generated.d.ts +6522 -0
  13. package/dist/generated.js +3755 -0
  14. package/dist/ids.d.ts +42 -0
  15. package/dist/ids.js +72 -0
  16. package/dist/index.d.ts +23 -0
  17. package/dist/index.js +23 -0
  18. package/dist/indexer.d.ts +91 -0
  19. package/dist/indexer.js +197 -0
  20. package/dist/keys.d.ts +40 -0
  21. package/dist/keys.js +50 -0
  22. package/dist/liquidity.d.ts +223 -0
  23. package/dist/liquidity.js +305 -0
  24. package/dist/onboard.d.ts +56 -0
  25. package/dist/onboard.js +188 -0
  26. package/dist/pools.d.ts +182 -0
  27. package/dist/pools.js +322 -0
  28. package/dist/positions.d.ts +164 -0
  29. package/dist/positions.js +197 -0
  30. package/dist/swap.d.ts +68 -0
  31. package/dist/swap.js +91 -0
  32. package/dist/tokencache.d.ts +76 -0
  33. package/dist/tokencache.js +150 -0
  34. package/dist/write.d.ts +86 -0
  35. package/dist/write.js +106 -0
  36. package/package.json +48 -0
  37. package/src/actions.ts +207 -0
  38. package/src/admin.ts +33 -0
  39. package/src/claims.ts +132 -0
  40. package/src/client.ts +104 -0
  41. package/src/fpool.ts +935 -0
  42. package/src/generated.ts +3762 -0
  43. package/src/ids.ts +78 -0
  44. package/src/index.ts +23 -0
  45. package/src/indexer.ts +252 -0
  46. package/src/keys.ts +73 -0
  47. package/src/liquidity.ts +430 -0
  48. package/src/onboard.ts +228 -0
  49. package/src/pools.ts +396 -0
  50. package/src/positions.ts +276 -0
  51. package/src/swap.ts +140 -0
  52. package/src/tokencache.ts +169 -0
  53. package/src/write.ts +134 -0
@@ -0,0 +1,430 @@
1
+ /**
2
+ * liquidity.ts — providing, editing and exiting a C position.
3
+ *
4
+ * A class is (ξ, μ, L): floor, cap, exposure. One primitive sets all three — `update` — and every
5
+ * edit is a constraint on it. The vocabulary follows the geometry (decisions 009): **the verb
6
+ * names the axis, the direction is a signed rung count**, and only two direction-words survive,
7
+ * because each names a wallet flow the user must see.
8
+ *
9
+ * update(ξ', μ', L') the primitive
10
+ * ├─ move(ξ', μ') L pinned
11
+ * │ ├─ floor(±n) μ pinned · ↑ harvest (receive quote) · ↓ deepen (pay quote)
12
+ * │ ├─ cap(±n) ξ pinned · no direction words
13
+ * │ └─ shift(±n) width pinned — the translation
14
+ * ├─ resize(L') ξ, μ pinned; L' = 0 is the exit
15
+ * └─ reprice(ξ') R pinned: L' = L·e^{ξ' − ξ}, dormant only
16
+ * mint · burn · claim
17
+ *
18
+ * Every edit is ONE action of the router's batch (decisions 010, `actions.ts`), which is what
19
+ * lets it settle in kind, or in base, or in quote: `settleIn` appends the one swap that nets the
20
+ * other side, and the batch moves a single token. `previewEdit` is the lens's `previewUpdate` —
21
+ * the exact flows, mint fee and swept fees included — and is what an editor shows before its one
22
+ * apply.
23
+ */
24
+
25
+ import type { Address, Hash } from "viem";
26
+ import { logswapLensAbi, logswapRouterAbi } from "./generated.js";
27
+ import type { LogswapClient } from "./client.js";
28
+ import type { PoolKey } from "./keys.js";
29
+ import { NO_CAP } from "./ids.js";
30
+ import { describeId, getPositionClass } from "./positions.js";
31
+ import {
32
+ execute,
33
+ OPEN_DELTA,
34
+ settleInKind,
35
+ settleOnly,
36
+ swapExactInAction,
37
+ swapExactOutAction,
38
+ updateAction,
39
+ type Action,
40
+ type PairBounds,
41
+ type Settle,
42
+ } from "./actions.js";
43
+ import {
44
+ requireWallet,
45
+ resolveDeadline,
46
+ resolveRecipient,
47
+ sendRouterWrite,
48
+ sendRouterWriteWithPermits,
49
+ type WriteOptions,
50
+ } from "./write.js";
51
+
52
+ /* mint ====================================================================== */
53
+
54
+ export interface MintArgs extends WriteOptions {
55
+ key: PoolKey;
56
+ /** Log-price floor. Need not be on the grid — see the blending note below. */
57
+ targetFloor: bigint;
58
+ cap?: bigint;
59
+ /** Exposure to add. */
60
+ L: bigint;
61
+ maxBaseIn?: bigint;
62
+ maxQuoteIn?: bigint;
63
+ }
64
+
65
+ /**
66
+ * Mint at `targetFloor`, blending across the two adjacent rungs when it is off-grid.
67
+ *
68
+ * This is why the floor ladder can be coarse without the UX being coarse: a blend of two adjacent
69
+ * classes IS exactly one position at the weighted-mean floor, so a continuous floor slider works
70
+ * over a discrete ladder. The router settles once per token, not once per rung.
71
+ *
72
+ * **A mint at floor == spot costs zero quote** ($Q(\xi) = 0$), and one above spot is dormant and
73
+ * costs zero quote too — that is what makes single-asset entry possible in both directions.
74
+ */
75
+ export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
76
+ const MAX = (1n << 256n) - 1n;
77
+ return sendRouterWriteWithPermits(c, {
78
+ abi: logswapRouterAbi,
79
+ functionName: "mintBlended",
80
+ args: [
81
+ a.key,
82
+ a.targetFloor,
83
+ a.cap ?? NO_CAP,
84
+ a.L,
85
+ a.maxBaseIn ?? MAX,
86
+ a.maxQuoteIn ?? MAX,
87
+ resolveRecipient(c, a),
88
+ resolveDeadline(a),
89
+ ],
90
+ }, [a.key.base, a.key.quote]);
91
+ }
92
+
93
+ export interface CreateMarketArgs extends WriteOptions {
94
+ key: PoolKey;
95
+ /** Launch log-price. Bounded by the manager to ±46 (the price domain, decisions 012). */
96
+ x0: bigint;
97
+ /** The backstop's floor — the first mint's rung, permanent for the pool's life. */
98
+ targetFloor: bigint;
99
+ /** The founding exposure; must exceed the manager's `minBackstopL` lock. */
100
+ L: bigint;
101
+ maxBaseIn?: bigint;
102
+ maxQuoteIn?: bigint;
103
+ }
104
+
105
+ /**
106
+ * Create a market ATOMICALLY: `initialize` at `x0` plus the backstop-establishing first mint in
107
+ * one transaction — the canonical creation path (decisions 013, security-review H-02). The
108
+ * two-step path let an attacker seat the PERMANENT backstop at a hostile rung between an honest
109
+ * `initialize` and its first mint; here either both land or neither, and a front-run of the whole
110
+ * call reverts it without moving a wei. No cap: the first mint IS the backstop and must be
111
+ * uncapped.
112
+ */
113
+ export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promise<Hash> {
114
+ const MAX = (1n << 256n) - 1n;
115
+ return sendRouterWriteWithPermits(c, {
116
+ abi: logswapRouterAbi,
117
+ functionName: "createMarket",
118
+ args: [
119
+ a.key,
120
+ a.x0,
121
+ a.targetFloor,
122
+ a.L,
123
+ a.maxBaseIn ?? MAX,
124
+ a.maxQuoteIn ?? MAX,
125
+ resolveRecipient(c, a),
126
+ resolveDeadline(a),
127
+ ],
128
+ }, [a.key.base, a.key.quote]);
129
+ }
130
+
131
+ export interface ZapArgs extends WriteOptions {
132
+ key: PoolKey;
133
+ /** Fund entirely from base (true) or entirely from quote (false). */
134
+ fundWithBase: boolean;
135
+ amountIn: bigint;
136
+ targetFloor: bigint;
137
+ cap?: bigint;
138
+ L: bigint;
139
+ maxIn?: bigint;
140
+ }
141
+
142
+ /**
143
+ * Fund a mint from a single asset: the router swaps the imbalanced part through the curve, then
144
+ * mints, all inside one lock — the two-action batch `[swapExactIn, mint]`.
145
+ *
146
+ * Routing the imbalanced part through the CURVE is the point, not an implementation detail. A
147
+ * pro-rata deposit of one asset would be a zero-slippage, zero-fee swap at stale spot — a subsidy
148
+ * paid by the incumbent LPs (`logswap-docs` §*Quote-only capital*). Paying the curve removes it.
149
+ */
150
+ export async function zapIn(c: LogswapClient, a: ZapArgs): Promise<Hash> {
151
+ return sendRouterWriteWithPermits(c, {
152
+ abi: logswapRouterAbi,
153
+ functionName: "zapIn",
154
+ args: [
155
+ a.key,
156
+ a.fundWithBase,
157
+ a.amountIn,
158
+ a.targetFloor,
159
+ a.cap ?? NO_CAP,
160
+ a.L,
161
+ a.maxIn ?? (1n << 256n) - 1n,
162
+ resolveRecipient(c, a),
163
+ resolveDeadline(a),
164
+ ],
165
+ }, [a.fundWithBase ? a.key.base : a.key.quote]);
166
+ }
167
+
168
+ /* the primitive, and its named entry ======================================== */
169
+
170
+ export interface UpdateArgs extends WriteOptions {
171
+ key: PoolKey;
172
+ fromId: bigint;
173
+ shares: bigint;
174
+ toFloor: bigint;
175
+ toCap?: bigint;
176
+ /** New exposure. `0n` exits the position entirely. */
177
+ newL: bigint;
178
+ maxBaseIn?: bigint;
179
+ maxQuoteIn?: bigint;
180
+ }
181
+
182
+ /** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
183
+ export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
184
+ const MAX = (1n << 256n) - 1n;
185
+ return sendRouterWriteWithPermits(c, {
186
+ abi: logswapRouterAbi,
187
+ functionName: "update",
188
+ args: [
189
+ a.key,
190
+ a.fromId,
191
+ a.shares,
192
+ a.toFloor,
193
+ a.toCap ?? NO_CAP,
194
+ a.newL,
195
+ a.maxBaseIn ?? MAX,
196
+ a.maxQuoteIn ?? MAX,
197
+ resolveRecipient(c, a),
198
+ resolveDeadline(a),
199
+ ],
200
+ }, [a.key.base, a.key.quote]);
201
+ }
202
+
203
+ export interface MoveArgs extends WriteOptions {
204
+ key: PoolKey;
205
+ fromId: bigint;
206
+ shares: bigint;
207
+ toFloor: bigint;
208
+ toCap?: bigint;
209
+ maxBaseIn?: bigint;
210
+ maxQuoteIn?: bigint;
211
+ }
212
+
213
+ /**
214
+ * Reposition at constant exposure — the L-pinned parent of `floor`, `cap` and `shift`; in kind.
215
+ *
216
+ * **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
217
+ * priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
218
+ * The defaults below are unbounded for callers who genuinely want that; pass `maxBaseIn` /
219
+ * `maxQuoteIn` from a preview for anything user-facing.
220
+ */
221
+ export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
222
+ const MAX = (1n << 256n) - 1n;
223
+ return sendRouterWrite(c, {
224
+ abi: logswapRouterAbi,
225
+ functionName: "move",
226
+ args: [
227
+ a.key,
228
+ a.fromId,
229
+ a.shares,
230
+ a.toFloor,
231
+ a.toCap ?? NO_CAP,
232
+ a.maxBaseIn ?? MAX,
233
+ a.maxQuoteIn ?? MAX,
234
+ resolveRecipient(c, a),
235
+ resolveDeadline(a),
236
+ ],
237
+ });
238
+ }
239
+
240
+ /* the edit tree ============================================================= */
241
+
242
+ /** Which token the edit settles in. `kind` moves both legs; `base` / `quote` nets the other side through the curve. */
243
+ export type SettleIn = "kind" | "base" | "quote";
244
+
245
+ export interface EditArgs extends WriteOptions, PairBounds {
246
+ key: PoolKey;
247
+ fromId: bigint;
248
+ /** The shares to edit — all of a holding, or part of it. */
249
+ shares: bigint;
250
+ /** Default `kind`. */
251
+ settleIn?: SettleIn;
252
+ }
253
+
254
+ export interface EditPreview {
255
+ /** Signed, + = the caller deposits; the mint fee is inside `dQuote`. */
256
+ dBase: bigint;
257
+ dQuote: bigint;
258
+ /** Accrued fees the edit sweeps, delivered in the same settlement. */
259
+ fees: bigint;
260
+ }
261
+
262
+ /** The exact flows of an edit before it is sent — the lens's `previewUpdate`. */
263
+ export async function previewEdit(
264
+ c: LogswapClient,
265
+ a: { key: PoolKey; fromId: bigint; holder?: Address; shares: bigint; toFloor: bigint; toCap?: bigint; newL: bigint },
266
+ ): Promise<EditPreview> {
267
+ const holder = a.holder ?? requireWallet(c).address;
268
+ const r = (await c.public.readContract({
269
+ address: c.addresses.lens,
270
+ abi: logswapLensAbi,
271
+ functionName: "previewUpdate",
272
+ args: [a.key, a.fromId, holder, a.shares, a.toFloor, a.toCap ?? NO_CAP, a.newL],
273
+ })) as readonly [bigint, bigint, bigint];
274
+ return { dBase: r[0], dQuote: r[1], fees: r[2] };
275
+ }
276
+
277
+ /**
278
+ * The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
279
+ * nets the other. The other side's flow is read off the preview: released → sold with an exact-in
280
+ * of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
281
+ * other token to zero both ways, so a stale preview fails loudly rather than moving it.
282
+ */
283
+ export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; toCap?: bigint; newL: bigint }): Promise<Hash> {
284
+ const { key } = a;
285
+ const recipient = resolveRecipient(c, a);
286
+ const actions: Action[] = [
287
+ updateAction({ key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL }),
288
+ ];
289
+ const settleIn = a.settleIn ?? "kind";
290
+ let settle: Settle;
291
+ if (settleIn === "kind") {
292
+ settle = settleInKind(key, recipient, a);
293
+ } else {
294
+ const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
295
+ // the swept fees ride INSIDE dQuote (the manager burns by owner and nets them; decisions 018),
296
+ // so quote's open delta after the update is dQuote itself — `fees` is reported for display only
297
+ const otherFlow = settleIn === "base" ? p.dQuote : p.dBase; // + = owed, − = released
298
+ const otherIsBase = settleIn === "quote";
299
+ if (otherFlow < 0n) actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
300
+ else if (otherFlow > 0n) actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
301
+ settle = settleOnly(key, recipient, settleIn, {
302
+ maxIn: settleIn === "base" ? a.maxBaseIn : a.maxQuoteIn,
303
+ minOut: settleIn === "base" ? a.minBaseOut : a.minQuoteOut,
304
+ });
305
+ }
306
+ return execute(c, { actions, settle, deadline: a.deadline, recipient });
307
+ }
308
+
309
+ /** The class behind `shares` of `id`: its floor, cap, and the exposure those shares carry. */
310
+ async function classOf(c: LogswapClient, key: PoolKey, id: bigint, shares: bigint) {
311
+ const [d, cls] = await Promise.all([describeId(c, id, key.tickSpacing), getPositionClass(c, id)]);
312
+ if (d.poolId === `0x${"0".repeat(64)}`) throw new Error(`logswap: id ${id} was never minted`);
313
+ const L = cls.shares === 0n ? 0n : (cls.L * shares) / cls.shares;
314
+ return { floor: d.floor!, cap: d.cap!, capped: d.capped, L };
315
+ }
316
+
317
+ /** Move the floor by `rungs` (signed), cap and L pinned. Positive = harvest, negative = deepen. */
318
+ export async function floor(c: LogswapClient, a: EditArgs & { rungs: bigint }): Promise<Hash> {
319
+ const k = await classOf(c, a.key, a.fromId, a.shares);
320
+ return edit(c, { ...a, toFloor: k.floor + a.rungs * a.key.tickSpacing, toCap: k.capped ? k.cap : undefined, newL: k.L });
321
+ }
322
+
323
+ /**
324
+ * Raise the floor by `rungs` (default 1) and withdraw PURE QUOTE — the base leg untouched.
325
+ *
326
+ * A burn is in kind: it returns base and quote pro rata, so taking α of the proceeds also takes α
327
+ * of the unsold inventory off the market. Raising the floor instead releases
328
+ *
329
+ * ΔQ = L (ξ' − ξ), ΔR = 0
330
+ *
331
+ * because $R = Le^{-x}$ depends on neither the floor nor the withdrawal. No base moves, the price
332
+ * does not move, and depth is unchanged. **The cost, which a UI must show:** the position stops
333
+ * bidding below ξ'. `settleIn: "base"` swaps the proceeds into base in the same batch.
334
+ */
335
+ export async function harvest(c: LogswapClient, a: EditArgs & { rungs?: bigint }): Promise<Hash> {
336
+ const n = a.rungs ?? 1n;
337
+ if (n <= 0n) throw new Error("logswap: harvest raises the floor — rungs must be positive; to lower it, deepen()");
338
+ return floor(c, { ...a, rungs: n });
339
+ }
340
+
341
+ /**
342
+ * Lower the floor by `rungs` (default 1), paying L·Δξ of quote for the deeper bid — harvest's
343
+ * mirror. `settleIn: "base"` pays it in base instead (an exact-out of the quote owed).
344
+ */
345
+ export async function deepen(c: LogswapClient, a: EditArgs & { rungs?: bigint }): Promise<Hash> {
346
+ const n = a.rungs ?? 1n;
347
+ if (n <= 0n) throw new Error("logswap: deepen lowers the floor — rungs must be positive; to raise it, harvest()");
348
+ return floor(c, { ...a, rungs: -n });
349
+ }
350
+
351
+ /** Move the cap by `rungs` (signed), floor and L pinned. Base flows while the class converts; quote once capped out. */
352
+ export async function cap(c: LogswapClient, a: EditArgs & { rungs: bigint }): Promise<Hash> {
353
+ const k = await classOf(c, a.key, a.fromId, a.shares);
354
+ if (!k.capped) throw new Error("logswap: cap() needs a capped class — an uncapped one has no cap to move");
355
+ const toCap = k.cap + a.rungs * a.key.tickSpacing;
356
+ if (toCap <= k.floor) throw new Error("logswap: the cap cannot sit at or below the floor");
357
+ return edit(c, { ...a, toFloor: k.floor, toCap, newL: k.L });
358
+ }
359
+
360
+ /** Translate the class by `rungs` (signed): floor and cap together, width and L pinned. */
361
+ export async function shift(c: LogswapClient, a: EditArgs & { rungs: bigint }): Promise<Hash> {
362
+ const k = await classOf(c, a.key, a.fromId, a.shares);
363
+ const d = a.rungs * a.key.tickSpacing;
364
+ return edit(c, { ...a, toFloor: k.floor + d, toCap: k.capped ? k.cap + d : undefined, newL: k.L });
365
+ }
366
+
367
+ /** Set the exposure to `newL`, floor and cap pinned; both legs pro rata. The mint fee bites on an increase only. */
368
+ export async function resize(c: LogswapClient, a: EditArgs & { newL: bigint }): Promise<Hash> {
369
+ const k = await classOf(c, a.key, a.fromId, a.shares);
370
+ return edit(c, { ...a, toFloor: k.floor, toCap: k.capped ? k.cap : undefined, newL: a.newL });
371
+ }
372
+
373
+ /** Exit `shares` — `resize(0)`. `settleIn: "quote"` (or `"base"`) leaves with one asset. */
374
+ export async function exit(c: LogswapClient, a: EditArgs): Promise<Hash> {
375
+ const k = await classOf(c, a.key, a.fromId, a.shares);
376
+ return edit(c, { ...a, toFloor: k.floor, toCap: k.capped ? k.cap : undefined, newL: 0n });
377
+ }
378
+
379
+ /**
380
+ * Re-express a DORMANT class's base at a new trigger price: the base is pinned, so
381
+ * L' = L·e^{ξ' − ξ} and nothing moves but the trigger. `toFloor` must stay above spot.
382
+ *
383
+ * The exponential is evaluated in double precision (~1e-16 relative), which is far inside the
384
+ * wei-rounding of the class it produces; an exact L' is not what a trigger edit is about.
385
+ */
386
+ export async function reprice(c: LogswapClient, a: EditArgs & { toFloor: bigint }): Promise<Hash> {
387
+ const k = await classOf(c, a.key, a.fromId, a.shares);
388
+ const ratio = Math.exp(Number(a.toFloor - k.floor) / 1e18);
389
+ const newL = (k.L * BigInt(Math.round(ratio * 1e18))) / 10n ** 18n;
390
+ return edit(c, { ...a, toFloor: a.toFloor, toCap: k.capped ? k.cap : undefined, newL });
391
+ }
392
+
393
+ /** Quote released by a harvest, exactly: ΔQ = L(ξ' − ξ). Base is untouched. */
394
+ export function harvestProceeds(L: bigint, fromFloor: bigint, toFloor: bigint): bigint {
395
+ if (toFloor <= fromFloor) return 0n;
396
+ return (L * (toFloor - fromFloor)) / 10n ** 18n;
397
+ }
398
+
399
+ /* burn ====================================================================== */
400
+
401
+ export interface BurnArgs extends WriteOptions {
402
+ key: PoolKey;
403
+ ids: readonly bigint[];
404
+ shares: readonly bigint[];
405
+ minBaseOut?: bigint;
406
+ minQuoteOut?: bigint;
407
+ }
408
+
409
+ /**
410
+ * Exit, in kind, across one or more classes. Accrued fees are swept into the same settlement.
411
+ *
412
+ * In kind means both legs, pro rata. To take proceeds while leaving inventory in the market, that
413
+ * is {@link harvest}; to leave with one asset, {@link exit} with `settleIn`.
414
+ */
415
+ export async function burn(c: LogswapClient, a: BurnArgs): Promise<Hash> {
416
+ if (a.ids.length !== a.shares.length) throw new Error("logswap: ids and shares length mismatch");
417
+ return sendRouterWrite(c, {
418
+ abi: logswapRouterAbi,
419
+ functionName: "burn",
420
+ args: [
421
+ a.key,
422
+ a.ids,
423
+ a.shares,
424
+ a.minBaseOut ?? 0n,
425
+ a.minQuoteOut ?? 0n,
426
+ resolveRecipient(c, a),
427
+ resolveDeadline(a),
428
+ ],
429
+ });
430
+ }
package/src/onboard.ts ADDED
@@ -0,0 +1,228 @@
1
+ /**
2
+ * onboard.ts — getting a wallet to the point where router writes actually work.
3
+ *
4
+ * There are **three separate approvals** with three separate failure modes, and a UI that surfaces
5
+ * them late produces failed transactions instead of prompts:
6
+ *
7
+ * 1. ERC-20 → Permit2 once per token, forever. Only when the router is Permit2-wired.
8
+ * 2. Permit2 → router a standing allowance, by transaction or by signature.
9
+ * 3. 6909 operator grant once per wallet, covers every market. Needed by burn/update/move.
10
+ *
11
+ * **The trap this module exists for:** when the router is deployed with Permit2, approving the
12
+ * ERC-20 to the *router* is not enough and not used — the pull goes through Permit2, and the
13
+ * failure is `NotAllowed()` from Permit2 rather than anything naming an allowance. Approving the
14
+ * router directly looks correct and does nothing.
15
+ */
16
+
17
+ import { parseAbi, zeroAddress, type Address, type Hash, type Hex } from "viem";
18
+ import type { LogswapClient } from "./client.js";
19
+ import { requireWallet } from "./write.js";
20
+
21
+ export const MAX_UINT256 = (1n << 256n) - 1n;
22
+ export const MAX_UINT160 = (1n << 160n) - 1n;
23
+ export const MAX_UINT48 = (1n << 48n) - 1n;
24
+
25
+ const ERC20 = parseAbi([
26
+ "function approve(address spender, uint256 amount) returns (bool)",
27
+ "function allowance(address owner, address spender) view returns (uint256)",
28
+ ]);
29
+
30
+ const PERMIT2 = parseAbi([
31
+ "function approve(address token, address spender, uint160 amount, uint48 expiration)",
32
+ "function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)",
33
+ "function transferFrom(address from, address to, uint160 amount, address token)",
34
+ ]);
35
+
36
+ /** Is this deployment's router wired to Permit2? Decides which approval path applies. */
37
+ export function usesPermit2(c: LogswapClient): boolean {
38
+ return !!c.addresses.permit2 && c.addresses.permit2 !== zeroAddress;
39
+ }
40
+
41
+ /** The contract that will actually pull tokens: Permit2 when wired, otherwise the router. */
42
+ export function spenderFor(c: LogswapClient): Address {
43
+ return usesPermit2(c) ? c.addresses.permit2! : c.addresses.router;
44
+ }
45
+
46
+ /** Step 1 — approve the *puller* on an ERC-20. Once per token. */
47
+ export async function approveToken(
48
+ c: LogswapClient,
49
+ token: Address,
50
+ amount: bigint = MAX_UINT256,
51
+ ): Promise<Hash> {
52
+ const { account } = requireWallet(c);
53
+ return c.wallet!.writeContract({
54
+ address: token,
55
+ abi: ERC20,
56
+ functionName: "approve",
57
+ args: [spenderFor(c), amount],
58
+ account,
59
+ chain: c.wallet!.chain,
60
+ } as never);
61
+ }
62
+
63
+ /**
64
+ * Step 2 — the standing Permit2 allowance for the router.
65
+ *
66
+ * No-op when the deployment has no Permit2. Prefer the signature flow (`applyPermit2` on the
67
+ * router, bundled through `multicall`) in production; this transaction form is what a test or a
68
+ * simple UI wants.
69
+ */
70
+ export async function approvePermit2ForRouter(
71
+ c: LogswapClient,
72
+ token: Address,
73
+ amount: bigint = MAX_UINT160,
74
+ expiration: bigint = MAX_UINT48,
75
+ ): Promise<Hash | null> {
76
+ if (!usesPermit2(c)) return null;
77
+ const { account } = requireWallet(c);
78
+ return c.wallet!.writeContract({
79
+ address: c.addresses.permit2!,
80
+ abi: PERMIT2,
81
+ functionName: "approve",
82
+ args: [token, c.addresses.router, amount, Number(expiration)],
83
+ account,
84
+ chain: c.wallet!.chain,
85
+ } as never);
86
+ }
87
+
88
+ /**
89
+ * Both token-side steps in order, skipping any already satisfied.
90
+ *
91
+ * Returns the hashes actually sent, so a UI can report "2 approvals" honestly rather than always
92
+ * prompting twice.
93
+ */
94
+ export async function onboardToken(c: LogswapClient, token: Address, need: bigint): Promise<Hash[]> {
95
+ // The address, not the account object: these two are READS. Signing paths take the object.
96
+ const { address } = requireWallet(c);
97
+ const sent: Hash[] = [];
98
+
99
+ const allowance = (await c.public.readContract({
100
+ address: token,
101
+ abi: ERC20,
102
+ functionName: "allowance",
103
+ args: [address, spenderFor(c)],
104
+ })) as bigint;
105
+ if (allowance < need) {
106
+ const h = await approveToken(c, token);
107
+ await c.public.waitForTransactionReceipt({ hash: h });
108
+ sent.push(h);
109
+ }
110
+
111
+ if (usesPermit2(c)) {
112
+ const [amt] = (await c.public.readContract({
113
+ address: c.addresses.permit2!,
114
+ abi: PERMIT2,
115
+ functionName: "allowance",
116
+ args: [address, token, c.addresses.router],
117
+ })) as readonly [bigint, number, number];
118
+ if (amt < need) {
119
+ const h = await approvePermit2ForRouter(c, token);
120
+ if (h) {
121
+ await c.public.waitForTransactionReceipt({ hash: h });
122
+ sent.push(h);
123
+ }
124
+ }
125
+ }
126
+ return sent;
127
+ }
128
+
129
+ /** What still stands between this wallet and a working write. */
130
+ export interface TokenReadiness {
131
+ usesPermit2: boolean;
132
+ /** ERC-20 allowance to whoever pulls (Permit2 when wired, else the router). */
133
+ needsTokenApproval: boolean;
134
+ /** Permit2's standing allowance for the router. Always false when Permit2 is not wired. */
135
+ needsPermit2Approval: boolean;
136
+ }
137
+
138
+ export async function checkTokenReadiness(
139
+ c: LogswapClient,
140
+ token: Address,
141
+ owner: Address,
142
+ need: bigint,
143
+ ): Promise<TokenReadiness> {
144
+ const allowance = (await c.public.readContract({
145
+ address: token,
146
+ abi: ERC20,
147
+ functionName: "allowance",
148
+ args: [owner, spenderFor(c)],
149
+ })) as bigint;
150
+
151
+ let needsPermit2Approval = false;
152
+ if (usesPermit2(c)) {
153
+ const [amt] = (await c.public.readContract({
154
+ address: c.addresses.permit2!,
155
+ abi: PERMIT2,
156
+ functionName: "allowance",
157
+ args: [owner, token, c.addresses.router],
158
+ })) as readonly [bigint, number, number];
159
+ needsPermit2Approval = amt < need;
160
+ }
161
+
162
+ return { usesPermit2: usesPermit2(c), needsTokenApproval: allowance < need, needsPermit2Approval };
163
+ }
164
+
165
+ // ─── the signature path: allowances that ride inside the action's own transaction ─────────────
166
+ //
167
+ // The rationalized flow. A token's ONLY unavoidable transaction is its one-time ERC-20 approval
168
+ // to Permit2; the Permit2→router allowance is set by an EIP-712 SIGNATURE (free, instant, no
169
+ // gas) submitted as an `applyPermit2` call INSIDE the same multicall as the action. Minting on a
170
+ // fresh token is then one transaction, not three — the pattern the router's `applyPermit2` +
171
+ // `multicall` were built for, and the one canonical Permit2 UX uses.
172
+
173
+ const PERMIT2_TYPES = {
174
+ PermitDetails: [
175
+ { name: "token", type: "address" },
176
+ { name: "amount", type: "uint160" },
177
+ { name: "expiration", type: "uint48" },
178
+ { name: "nonce", type: "uint48" },
179
+ ],
180
+ PermitSingle: [
181
+ { name: "details", type: "PermitDetails" },
182
+ { name: "spender", type: "address" },
183
+ { name: "sigDeadline", type: "uint256" },
184
+ ],
185
+ } as const;
186
+
187
+ /**
188
+ * For each token whose Permit2→router allowance is missing: read the nonce, have the wallet SIGN
189
+ * a PermitSingle (a signature prompt, not a transaction), and return the encoded `applyPermit2`
190
+ * calls ready to ride in a router multicall. Tokens already allowed produce nothing.
191
+ */
192
+ export async function permit2SigCalls(c: LogswapClient, tokens: Address[]): Promise<Hex[]> {
193
+ if (!usesPermit2(c)) return [];
194
+ const { account, address: owner } = requireWallet(c);
195
+ const chainId = await c.public.getChainId();
196
+ const calls: Hex[] = [];
197
+ for (const token of [...new Set(tokens.map((t) => t.toLowerCase() as Address))]) {
198
+ const [amt, , nonce] = (await c.public.readContract({
199
+ address: c.addresses.permit2!,
200
+ abi: PERMIT2,
201
+ functionName: "allowance",
202
+ args: [owner, token, c.addresses.router],
203
+ })) as readonly [bigint, number, number];
204
+ if (amt >= 1n << 120n) continue; // effectively infinite already
205
+ const message = {
206
+ details: { token, amount: MAX_UINT160, expiration: Number(MAX_UINT48), nonce },
207
+ spender: c.addresses.router,
208
+ sigDeadline: BigInt(Math.floor(Date.now() / 1000)) + 1800n,
209
+ };
210
+ const signature = await c.wallet!.signTypedData({
211
+ account,
212
+ domain: { name: "Permit2", chainId, verifyingContract: c.addresses.permit2! },
213
+ types: PERMIT2_TYPES,
214
+ primaryType: "PermitSingle",
215
+ message,
216
+ } as never);
217
+ const { encodeFunctionData } = await import("viem");
218
+ const { logswapRouterAbi } = await import("./generated.js");
219
+ calls.push(
220
+ encodeFunctionData({
221
+ abi: logswapRouterAbi,
222
+ functionName: "applyPermit2",
223
+ args: [owner, message, signature],
224
+ } as never),
225
+ );
226
+ }
227
+ return calls;
228
+ }