@logswap/sdk 0.5.52 → 0.5.54

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/liquidity.ts CHANGED
@@ -46,8 +46,10 @@ import {
46
46
  resolveRecipient,
47
47
  sendRouterWrite,
48
48
  sendRouterWriteWithPermits,
49
+ withSlippage,
49
50
  type WriteOptions,
50
51
  } from "./write.js";
52
+ import { quoteExactIn, quoteExactOut } from "./swap.js";
51
53
 
52
54
  /* mint ====================================================================== */
53
55
 
@@ -58,8 +60,14 @@ export interface MintArgs extends WriteOptions {
58
60
  cap?: bigint;
59
61
  /** Exposure to add. */
60
62
  L: bigint;
61
- maxBaseIn?: bigint;
62
- maxQuoteIn?: bigint;
63
+ /**
64
+ * The most the mint may pull, per token. REQUIRED: the pulled amounts depend on spot when the
65
+ * transaction lands, so an unbounded mint can be sandwiched into depositing far more than the
66
+ * preview showed. Derive them from the preview with {@link withSlippage} (`"input"`); pass
67
+ * {@link UNBOUNDED} only where the price cannot move under you.
68
+ */
69
+ maxBaseIn: bigint;
70
+ maxQuoteIn: bigint;
63
71
  }
64
72
 
65
73
  /**
@@ -73,7 +81,6 @@ export interface MintArgs extends WriteOptions {
73
81
  * costs zero quote too — that is what makes single-asset entry possible in both directions.
74
82
  */
75
83
  export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
76
- const MAX = (1n << 256n) - 1n;
77
84
  return sendRouterWriteWithPermits(c, {
78
85
  abi: logswapRouterAbi,
79
86
  functionName: "mintBlended",
@@ -82,8 +89,8 @@ export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
82
89
  a.targetFloor,
83
90
  a.cap ?? NO_CAP,
84
91
  a.L,
85
- a.maxBaseIn ?? MAX,
86
- a.maxQuoteIn ?? MAX,
92
+ a.maxBaseIn,
93
+ a.maxQuoteIn,
87
94
  resolveRecipient(c, a),
88
95
  resolveDeadline(a),
89
96
  ],
@@ -98,8 +105,9 @@ export interface CreateMarketArgs extends WriteOptions {
98
105
  targetFloor: bigint;
99
106
  /** The founding exposure; must exceed the manager's `minBackstopL` lock. */
100
107
  L: bigint;
101
- maxBaseIn?: bigint;
102
- maxQuoteIn?: bigint;
108
+ /** Required, as on {@link mint}. Creation is atomic at `x0`, so the flows are fixed by the arguments; {@link UNBOUNDED} is safe here only for that reason. */
109
+ maxBaseIn: bigint;
110
+ maxQuoteIn: bigint;
103
111
  }
104
112
 
105
113
  /**
@@ -111,7 +119,6 @@ export interface CreateMarketArgs extends WriteOptions {
111
119
  * uncapped.
112
120
  */
113
121
  export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promise<Hash> {
114
- const MAX = (1n << 256n) - 1n;
115
122
  return sendRouterWriteWithPermits(c, {
116
123
  abi: logswapRouterAbi,
117
124
  functionName: "createMarket",
@@ -120,8 +127,8 @@ export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promi
120
127
  a.x0,
121
128
  a.targetFloor,
122
129
  a.L,
123
- a.maxBaseIn ?? MAX,
124
- a.maxQuoteIn ?? MAX,
130
+ a.maxBaseIn,
131
+ a.maxQuoteIn,
125
132
  resolveRecipient(c, a),
126
133
  resolveDeadline(a),
127
134
  ],
@@ -133,12 +140,13 @@ export interface ZapArgs extends WriteOptions {
133
140
  /** Fund entirely from base (true) or entirely from quote (false). */
134
141
  fundWithBase: boolean;
135
142
  amountIn: bigint;
136
- /** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. 0 = unbounded. */
137
- minSwapOut?: bigint;
143
+ /** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. Required. */
144
+ minSwapOut: bigint;
138
145
  targetFloor: bigint;
139
146
  cap?: bigint;
140
147
  L: bigint;
141
- maxIn?: bigint;
148
+ /** The most of the funding token the zap may pull. Required. */
149
+ maxIn: bigint;
142
150
  }
143
151
 
144
152
  /**
@@ -157,11 +165,11 @@ export async function zapIn(c: LogswapClient, a: ZapArgs): Promise<Hash> {
157
165
  a.key,
158
166
  a.fundWithBase,
159
167
  a.amountIn,
160
- a.minSwapOut ?? 0n,
168
+ a.minSwapOut,
161
169
  a.targetFloor,
162
170
  a.cap ?? NO_CAP,
163
171
  a.L,
164
- a.maxIn ?? (1n << 256n) - 1n,
172
+ a.maxIn,
165
173
  resolveRecipient(c, a),
166
174
  resolveDeadline(a),
167
175
  ],
@@ -178,13 +186,13 @@ export interface UpdateArgs extends WriteOptions {
178
186
  toCap?: bigint;
179
187
  /** New exposure. `0n` exits the position entirely. */
180
188
  newL: bigint;
181
- maxBaseIn?: bigint;
182
- maxQuoteIn?: bigint;
189
+ /** Required: see {@link MintArgs}. {@link previewEdit} gives the exact flows to bound. */
190
+ maxBaseIn: bigint;
191
+ maxQuoteIn: bigint;
183
192
  }
184
193
 
185
194
  /** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
186
195
  export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
187
- const MAX = (1n << 256n) - 1n;
188
196
  return sendRouterWriteWithPermits(c, {
189
197
  abi: logswapRouterAbi,
190
198
  functionName: "update",
@@ -195,8 +203,8 @@ export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
195
203
  a.toFloor,
196
204
  a.toCap ?? NO_CAP,
197
205
  a.newL,
198
- a.maxBaseIn ?? MAX,
199
- a.maxQuoteIn ?? MAX,
206
+ a.maxBaseIn,
207
+ a.maxQuoteIn,
200
208
  resolveRecipient(c, a),
201
209
  resolveDeadline(a),
202
210
  ],
@@ -209,8 +217,9 @@ export interface MoveArgs extends WriteOptions {
209
217
  shares: bigint;
210
218
  toFloor: bigint;
211
219
  toCap?: bigint;
212
- maxBaseIn?: bigint;
213
- maxQuoteIn?: bigint;
220
+ /** Required: see {@link MintArgs}. */
221
+ maxBaseIn: bigint;
222
+ maxQuoteIn: bigint;
214
223
  }
215
224
 
216
225
  /**
@@ -218,11 +227,9 @@ export interface MoveArgs extends WriteOptions {
218
227
  *
219
228
  * **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
220
229
  * priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
221
- * The defaults below are unbounded for callers who genuinely want that; pass `maxBaseIn` /
222
- * `maxQuoteIn` from a preview for anything user-facing.
230
+ * The bounds are required; {@link UNBOUNDED} is an explicit, visible opt-out.
223
231
  */
224
232
  export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
225
- const MAX = (1n << 256n) - 1n;
226
233
  return sendRouterWrite(c, {
227
234
  abi: logswapRouterAbi,
228
235
  functionName: "move",
@@ -232,8 +239,8 @@ export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
232
239
  a.shares,
233
240
  a.toFloor,
234
241
  a.toCap ?? NO_CAP,
235
- a.maxBaseIn ?? MAX,
236
- a.maxQuoteIn ?? MAX,
242
+ a.maxBaseIn,
243
+ a.maxQuoteIn,
237
244
  resolveRecipient(c, a),
238
245
  resolveDeadline(a),
239
246
  ],
@@ -252,6 +259,13 @@ export interface EditArgs extends WriteOptions, PairBounds {
252
259
  shares: bigint;
253
260
  /** Default `kind`. */
254
261
  settleIn?: SettleIn;
262
+ /**
263
+ * Tolerance in basis points, applied to the flows the edit is expected to move (the lens's
264
+ * exact preview, plus a quote of the netting swap when `settleIn` is one token). Required: an
265
+ * edit's flows depend on spot when it lands. An explicit bound in `PairBounds` overrides the one
266
+ * derived for that token.
267
+ */
268
+ slippageBps: number;
255
269
  }
256
270
 
257
271
  export interface EditPreview {
@@ -277,11 +291,27 @@ export async function previewEdit(
277
291
  return { dBase: r[0], dQuote: r[1], fees: r[2] };
278
292
  }
279
293
 
294
+ /**
295
+ * Bounds for a pair of signed flows (+ = the caller pays, − = the caller receives), at `bps` of
296
+ * tolerance: a payment is capped from above, a receipt floored from below, and a token that does
297
+ * not move in that direction is bound to zero.
298
+ */
299
+ export function boundsFromFlows(dBase: bigint, dQuote: bigint, bps: number): Required<PairBounds> {
300
+ const inb = (d: bigint) => (d > 0n ? withSlippage(d, bps, "input") : 0n);
301
+ const outb = (d: bigint) => (d < 0n ? withSlippage(-d, bps, "output") : 0n);
302
+ return { maxBaseIn: inb(dBase), maxQuoteIn: inb(dQuote), minBaseOut: outb(dBase), minQuoteOut: outb(dQuote) };
303
+ }
304
+
280
305
  /**
281
306
  * The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
282
307
  * nets the other. The other side's flow is read off the preview: released → sold with an exact-in
283
308
  * of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
284
309
  * other token to zero both ways, so a stale preview fails loudly rather than moving it.
310
+ *
311
+ * **Bounded by default.** The preview gives the edit's exact flows at today's state; `slippageBps`
312
+ * turns them into the settlement's bounds. With `settleIn` one token, the netting swap is quoted
313
+ * on today's curve, before the update changes the active liquidity — close for a small edit, and
314
+ * for a large one a bound that reverts rather than one that lets a sandwich through.
285
315
  */
286
316
  export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; toCap?: bigint; newL: bigint }): Promise<Hash> {
287
317
  const { key } = a;
@@ -290,25 +320,39 @@ export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; to
290
320
  updateAction({ key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL }),
291
321
  ];
292
322
  const settleIn = a.settleIn ?? "kind";
323
+ const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
293
324
  let settle: Settle;
294
325
  if (settleIn === "kind") {
295
- settle = settleInKind(key, recipient, a);
326
+ settle = settleInKind(key, recipient, { ...boundsFromFlows(p.dBase, p.dQuote, a.slippageBps), ...defined(a) });
296
327
  } else {
297
- const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
298
328
  // the swept fees ride INSIDE dQuote (the manager burns by owner and nets them; decisions 018),
299
329
  // so quote's open delta after the update is dQuote itself — `fees` is reported for display only
300
330
  const otherFlow = settleIn === "base" ? p.dQuote : p.dBase; // + = owed, − = released
301
331
  const otherIsBase = settleIn === "quote";
302
- if (otherFlow < 0n) actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
303
- else if (otherFlow > 0n) actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
332
+ let own = settleIn === "base" ? p.dBase : p.dQuote;
333
+ if (otherFlow < 0n) {
334
+ actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
335
+ own -= await quoteExactIn(c, key, otherIsBase, -otherFlow);
336
+ } else if (otherFlow > 0n) {
337
+ actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
338
+ own += await quoteExactOut(c, key, otherIsBase, otherFlow);
339
+ }
340
+ const b = settleIn === "base" ? boundsFromFlows(own, 0n, a.slippageBps) : boundsFromFlows(0n, own, a.slippageBps);
304
341
  settle = settleOnly(key, recipient, settleIn, {
305
- maxIn: settleIn === "base" ? a.maxBaseIn : a.maxQuoteIn,
306
- minOut: settleIn === "base" ? a.minBaseOut : a.minQuoteOut,
342
+ maxIn: settleIn === "base" ? (a.maxBaseIn ?? b.maxBaseIn) : (a.maxQuoteIn ?? b.maxQuoteIn),
343
+ minOut: settleIn === "base" ? (a.minBaseOut ?? b.minBaseOut) : (a.minQuoteOut ?? b.minQuoteOut),
307
344
  });
308
345
  }
309
346
  return execute(c, { actions, settle, deadline: a.deadline, recipient });
310
347
  }
311
348
 
349
+ /** The explicit bounds a caller passed, without the undefined ones (so they do not erase derived ones). */
350
+ function defined(b: PairBounds): PairBounds {
351
+ return Object.fromEntries(
352
+ (["maxBaseIn", "maxQuoteIn", "minBaseOut", "minQuoteOut"] as const).filter((k) => b[k] !== undefined).map((k) => [k, b[k]]),
353
+ );
354
+ }
355
+
312
356
  /** The class behind `shares` of `id`: its floor, cap, and the exposure those shares carry. */
313
357
  async function classOf(c: LogswapClient, key: PoolKey, id: bigint, shares: bigint) {
314
358
  const [d, cls] = await Promise.all([describeId(c, id, key.tickSpacing), getPositionClass(c, id)]);
@@ -405,8 +449,9 @@ export interface BurnArgs extends WriteOptions {
405
449
  key: PoolKey;
406
450
  ids: readonly bigint[];
407
451
  shares: readonly bigint[];
408
- minBaseOut?: bigint;
409
- minQuoteOut?: bigint;
452
+ /** The least of each token the burn must return. Required: a burn's legs depend on spot. */
453
+ minBaseOut: bigint;
454
+ minQuoteOut: bigint;
410
455
  }
411
456
 
412
457
  /**
@@ -424,8 +469,8 @@ export async function burn(c: LogswapClient, a: BurnArgs): Promise<Hash> {
424
469
  a.key,
425
470
  a.ids,
426
471
  a.shares,
427
- a.minBaseOut ?? 0n,
428
- a.minQuoteOut ?? 0n,
472
+ a.minBaseOut,
473
+ a.minQuoteOut,
429
474
  resolveRecipient(c, a),
430
475
  resolveDeadline(a),
431
476
  ],
package/src/pools.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  import { formatUnits, type Address, type Hex } from "viem";
16
16
  import { logswapLensAbi, cPoolManagerAbi } from "./generated.js";
17
17
  import type { LogswapClient } from "./client.js";
18
+ import { signerFor } from "./write.js";
18
19
  import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
19
20
  import { poolId, type PoolKey } from "./keys.js";
20
21
 
@@ -104,6 +105,8 @@ export interface DiscoveredMarket {
104
105
  x0: bigint;
105
106
  blockNumber: bigint;
106
107
  transactionHash: Hex;
108
+ /** The manager that emitted the `Initialize` — set when the row came from the indexer. */
109
+ manager?: Address;
107
110
  }
108
111
 
109
112
  /**
@@ -188,18 +191,10 @@ export async function marketDecimals(
188
191
  c: LogswapClient,
189
192
  key: PoolKey,
190
193
  ): Promise<{ base: number; quote: number }> {
191
- const abi = [
192
- { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
193
- ] as const;
194
- const read = async (token: Address): Promise<number> => {
195
- try {
196
- return Number(await c.public.readContract({ address: token, abi, functionName: "decimals" }));
197
- } catch {
198
- return 18; // same fallback the manager uses when the token does not implement it
199
- }
200
- };
201
- const [base, quote] = await Promise.all([read(key.base), read(key.quote)]);
202
- return { base, quote };
194
+ // It used to fall back to 18 on ANY failure, a dropped request included — a 6-decimal quote read
195
+ // as 18 is every amount 10^12 off. `marketTokens` retries and then refuses; so does this.
196
+ const { base, quote } = await marketTokens(c, key);
197
+ return { base: base.decimals, quote: quote.decimals };
203
198
  }
204
199
 
205
200
  /** A token as a UI needs it: address, symbol and decimals, all read from the chain. */
@@ -391,7 +386,7 @@ export async function initializeMarket(c: LogswapClient, a: { key: PoolKey; x0:
391
386
  abi: cPoolManagerAbi,
392
387
  functionName: "initialize",
393
388
  args: [a.key, a.x0],
394
- account: a.account,
389
+ account: signerFor(c, a.account),
395
390
  } as never);
396
391
  return c.wallet!.writeContract(request as never);
397
392
  }
package/src/write.ts CHANGED
@@ -46,6 +46,21 @@ export function resolveDeadline(o: WriteOptions | undefined, now = Date.now()):
46
46
  * until someone uses a key the node does not have — a bot, a script, CI, or a real user's wallet.
47
47
  * Passing the object lets viem pick the correct path for both kinds.
48
48
  */
49
+ /**
50
+ * The wallet's account OBJECT for a write the caller named by address — see {@link requireWallet}
51
+ * for why the object and never the address. The F helpers take `account: Address` in their
52
+ * arguments; passed straight to viem it became a JSON-RPC account and the NODE was asked to sign,
53
+ * which works only on a dev node holding the key (every agent on the devnet did exactly that).
54
+ * A named account that is not the wallet's is refused: nothing else could sign for it.
55
+ */
56
+ export function signerFor(c: LogswapClient, address: Address): NonNullable<LogswapClient["wallet"]>["account"] {
57
+ const { account } = requireWallet(c);
58
+ if (account!.address.toLowerCase() !== address.toLowerCase()) {
59
+ throw new Error(`logswap: the write names ${address} but the wallet signs as ${account!.address}`);
60
+ }
61
+ return account;
62
+ }
63
+
49
64
  export function requireWallet(c: LogswapClient): { account: NonNullable<LogswapClient["wallet"]>["account"]; address: Address } {
50
65
  const account = c.wallet?.account;
51
66
  if (!c.wallet || !account) {
@@ -68,7 +83,8 @@ export function resolveRecipient(c: LogswapClient, o?: WriteOptions): Address {
68
83
  export function withSlippage(quoted: bigint, bps: number, side: "output" | "input"): bigint {
69
84
  if (bps < 0 || bps > 10_000) throw new Error(`logswap: slippage bps out of range: ${bps}`);
70
85
  const b = BigInt(Math.round(bps));
71
- return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b)) / 10_000n;
86
+ // an input bound rounds UP: flooring it would put a wei-exact quote at bps = 0 one wei short
87
+ return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b) + 9_999n) / 10_000n;
72
88
  }
73
89
 
74
90
  /**