@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/dist/fpool.d.ts +42 -7
- package/dist/fpool.js +52 -14
- package/dist/indexer.d.ts +15 -0
- package/dist/indexer.js +57 -34
- package/dist/liquidity.d.ts +43 -15
- package/dist/liquidity.js +46 -25
- package/dist/pools.d.ts +2 -0
- package/dist/pools.js +6 -14
- package/dist/write.d.ts +8 -0
- package/dist/write.js +16 -1
- package/package.json +1 -1
- package/src/fpool.ts +60 -15
- package/src/indexer.ts +63 -41
- package/src/liquidity.ts +82 -37
- package/src/pools.ts +8 -13
- package/src/write.ts +17 -1
package/dist/liquidity.js
CHANGED
|
@@ -25,7 +25,8 @@ import { logswapLensAbi, logswapRouterAbi } from "./generated.js";
|
|
|
25
25
|
import { NO_CAP } from "./ids.js";
|
|
26
26
|
import { describeId, getPositionClass } from "./positions.js";
|
|
27
27
|
import { execute, OPEN_DELTA, settleInKind, settleOnly, swapExactInAction, swapExactOutAction, updateAction, } from "./actions.js";
|
|
28
|
-
import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite, sendRouterWriteWithPermits, } from "./write.js";
|
|
28
|
+
import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite, sendRouterWriteWithPermits, withSlippage, } from "./write.js";
|
|
29
|
+
import { quoteExactIn, quoteExactOut } from "./swap.js";
|
|
29
30
|
/**
|
|
30
31
|
* Mint at `targetFloor`, blending across the two adjacent rungs when it is off-grid.
|
|
31
32
|
*
|
|
@@ -37,7 +38,6 @@ import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite, send
|
|
|
37
38
|
* costs zero quote too — that is what makes single-asset entry possible in both directions.
|
|
38
39
|
*/
|
|
39
40
|
export async function mint(c, a) {
|
|
40
|
-
const MAX = (1n << 256n) - 1n;
|
|
41
41
|
return sendRouterWriteWithPermits(c, {
|
|
42
42
|
abi: logswapRouterAbi,
|
|
43
43
|
functionName: "mintBlended",
|
|
@@ -46,8 +46,8 @@ export async function mint(c, a) {
|
|
|
46
46
|
a.targetFloor,
|
|
47
47
|
a.cap ?? NO_CAP,
|
|
48
48
|
a.L,
|
|
49
|
-
a.maxBaseIn
|
|
50
|
-
a.maxQuoteIn
|
|
49
|
+
a.maxBaseIn,
|
|
50
|
+
a.maxQuoteIn,
|
|
51
51
|
resolveRecipient(c, a),
|
|
52
52
|
resolveDeadline(a),
|
|
53
53
|
],
|
|
@@ -62,7 +62,6 @@ export async function mint(c, a) {
|
|
|
62
62
|
* uncapped.
|
|
63
63
|
*/
|
|
64
64
|
export async function createMarket(c, a) {
|
|
65
|
-
const MAX = (1n << 256n) - 1n;
|
|
66
65
|
return sendRouterWriteWithPermits(c, {
|
|
67
66
|
abi: logswapRouterAbi,
|
|
68
67
|
functionName: "createMarket",
|
|
@@ -71,8 +70,8 @@ export async function createMarket(c, a) {
|
|
|
71
70
|
a.x0,
|
|
72
71
|
a.targetFloor,
|
|
73
72
|
a.L,
|
|
74
|
-
a.maxBaseIn
|
|
75
|
-
a.maxQuoteIn
|
|
73
|
+
a.maxBaseIn,
|
|
74
|
+
a.maxQuoteIn,
|
|
76
75
|
resolveRecipient(c, a),
|
|
77
76
|
resolveDeadline(a),
|
|
78
77
|
],
|
|
@@ -94,11 +93,11 @@ export async function zapIn(c, a) {
|
|
|
94
93
|
a.key,
|
|
95
94
|
a.fundWithBase,
|
|
96
95
|
a.amountIn,
|
|
97
|
-
a.minSwapOut
|
|
96
|
+
a.minSwapOut,
|
|
98
97
|
a.targetFloor,
|
|
99
98
|
a.cap ?? NO_CAP,
|
|
100
99
|
a.L,
|
|
101
|
-
a.maxIn
|
|
100
|
+
a.maxIn,
|
|
102
101
|
resolveRecipient(c, a),
|
|
103
102
|
resolveDeadline(a),
|
|
104
103
|
],
|
|
@@ -106,7 +105,6 @@ export async function zapIn(c, a) {
|
|
|
106
105
|
}
|
|
107
106
|
/** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
|
|
108
107
|
export async function update(c, a) {
|
|
109
|
-
const MAX = (1n << 256n) - 1n;
|
|
110
108
|
return sendRouterWriteWithPermits(c, {
|
|
111
109
|
abi: logswapRouterAbi,
|
|
112
110
|
functionName: "update",
|
|
@@ -117,8 +115,8 @@ export async function update(c, a) {
|
|
|
117
115
|
a.toFloor,
|
|
118
116
|
a.toCap ?? NO_CAP,
|
|
119
117
|
a.newL,
|
|
120
|
-
a.maxBaseIn
|
|
121
|
-
a.maxQuoteIn
|
|
118
|
+
a.maxBaseIn,
|
|
119
|
+
a.maxQuoteIn,
|
|
122
120
|
resolveRecipient(c, a),
|
|
123
121
|
resolveDeadline(a),
|
|
124
122
|
],
|
|
@@ -129,11 +127,9 @@ export async function update(c, a) {
|
|
|
129
127
|
*
|
|
130
128
|
* **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
|
|
131
129
|
* priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
|
|
132
|
-
* The
|
|
133
|
-
* `maxQuoteIn` from a preview for anything user-facing.
|
|
130
|
+
* The bounds are required; {@link UNBOUNDED} is an explicit, visible opt-out.
|
|
134
131
|
*/
|
|
135
132
|
export async function move(c, a) {
|
|
136
|
-
const MAX = (1n << 256n) - 1n;
|
|
137
133
|
return sendRouterWrite(c, {
|
|
138
134
|
abi: logswapRouterAbi,
|
|
139
135
|
functionName: "move",
|
|
@@ -143,8 +139,8 @@ export async function move(c, a) {
|
|
|
143
139
|
a.shares,
|
|
144
140
|
a.toFloor,
|
|
145
141
|
a.toCap ?? NO_CAP,
|
|
146
|
-
a.maxBaseIn
|
|
147
|
-
a.maxQuoteIn
|
|
142
|
+
a.maxBaseIn,
|
|
143
|
+
a.maxQuoteIn,
|
|
148
144
|
resolveRecipient(c, a),
|
|
149
145
|
resolveDeadline(a),
|
|
150
146
|
],
|
|
@@ -161,11 +157,26 @@ export async function previewEdit(c, a) {
|
|
|
161
157
|
}));
|
|
162
158
|
return { dBase: r[0], dQuote: r[1], fees: r[2] };
|
|
163
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Bounds for a pair of signed flows (+ = the caller pays, − = the caller receives), at `bps` of
|
|
162
|
+
* tolerance: a payment is capped from above, a receipt floored from below, and a token that does
|
|
163
|
+
* not move in that direction is bound to zero.
|
|
164
|
+
*/
|
|
165
|
+
export function boundsFromFlows(dBase, dQuote, bps) {
|
|
166
|
+
const inb = (d) => (d > 0n ? withSlippage(d, bps, "input") : 0n);
|
|
167
|
+
const outb = (d) => (d < 0n ? withSlippage(-d, bps, "output") : 0n);
|
|
168
|
+
return { maxBaseIn: inb(dBase), maxQuoteIn: inb(dQuote), minBaseOut: outb(dBase), minQuoteOut: outb(dQuote) };
|
|
169
|
+
}
|
|
164
170
|
/**
|
|
165
171
|
* The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
|
|
166
172
|
* nets the other. The other side's flow is read off the preview: released → sold with an exact-in
|
|
167
173
|
* of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
|
|
168
174
|
* other token to zero both ways, so a stale preview fails loudly rather than moving it.
|
|
175
|
+
*
|
|
176
|
+
* **Bounded by default.** The preview gives the edit's exact flows at today's state; `slippageBps`
|
|
177
|
+
* turns them into the settlement's bounds. With `settleIn` one token, the netting swap is quoted
|
|
178
|
+
* on today's curve, before the update changes the active liquidity — close for a small edit, and
|
|
179
|
+
* for a large one a bound that reverts rather than one that lets a sandwich through.
|
|
169
180
|
*/
|
|
170
181
|
export async function edit(c, a) {
|
|
171
182
|
const { key } = a;
|
|
@@ -174,27 +185,37 @@ export async function edit(c, a) {
|
|
|
174
185
|
updateAction({ key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL }),
|
|
175
186
|
];
|
|
176
187
|
const settleIn = a.settleIn ?? "kind";
|
|
188
|
+
const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
|
|
177
189
|
let settle;
|
|
178
190
|
if (settleIn === "kind") {
|
|
179
|
-
settle = settleInKind(key, recipient, a);
|
|
191
|
+
settle = settleInKind(key, recipient, { ...boundsFromFlows(p.dBase, p.dQuote, a.slippageBps), ...defined(a) });
|
|
180
192
|
}
|
|
181
193
|
else {
|
|
182
|
-
const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
|
|
183
194
|
// the swept fees ride INSIDE dQuote (the manager burns by owner and nets them; decisions 018),
|
|
184
195
|
// so quote's open delta after the update is dQuote itself — `fees` is reported for display only
|
|
185
196
|
const otherFlow = settleIn === "base" ? p.dQuote : p.dBase; // + = owed, − = released
|
|
186
197
|
const otherIsBase = settleIn === "quote";
|
|
187
|
-
|
|
198
|
+
let own = settleIn === "base" ? p.dBase : p.dQuote;
|
|
199
|
+
if (otherFlow < 0n) {
|
|
188
200
|
actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
|
|
189
|
-
|
|
201
|
+
own -= await quoteExactIn(c, key, otherIsBase, -otherFlow);
|
|
202
|
+
}
|
|
203
|
+
else if (otherFlow > 0n) {
|
|
190
204
|
actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
|
|
205
|
+
own += await quoteExactOut(c, key, otherIsBase, otherFlow);
|
|
206
|
+
}
|
|
207
|
+
const b = settleIn === "base" ? boundsFromFlows(own, 0n, a.slippageBps) : boundsFromFlows(0n, own, a.slippageBps);
|
|
191
208
|
settle = settleOnly(key, recipient, settleIn, {
|
|
192
|
-
maxIn: settleIn === "base" ? a.maxBaseIn : a.maxQuoteIn,
|
|
193
|
-
minOut: settleIn === "base" ? a.minBaseOut : a.minQuoteOut,
|
|
209
|
+
maxIn: settleIn === "base" ? (a.maxBaseIn ?? b.maxBaseIn) : (a.maxQuoteIn ?? b.maxQuoteIn),
|
|
210
|
+
minOut: settleIn === "base" ? (a.minBaseOut ?? b.minBaseOut) : (a.minQuoteOut ?? b.minQuoteOut),
|
|
194
211
|
});
|
|
195
212
|
}
|
|
196
213
|
return execute(c, { actions, settle, deadline: a.deadline, recipient });
|
|
197
214
|
}
|
|
215
|
+
/** The explicit bounds a caller passed, without the undefined ones (so they do not erase derived ones). */
|
|
216
|
+
function defined(b) {
|
|
217
|
+
return Object.fromEntries(["maxBaseIn", "maxQuoteIn", "minBaseOut", "minQuoteOut"].filter((k) => b[k] !== undefined).map((k) => [k, b[k]]));
|
|
218
|
+
}
|
|
198
219
|
/** The class behind `shares` of `id`: its floor, cap, and the exposure those shares carry. */
|
|
199
220
|
async function classOf(c, key, id, shares) {
|
|
200
221
|
const [d, cls] = await Promise.all([describeId(c, id, key.tickSpacing), getPositionClass(c, id)]);
|
|
@@ -297,8 +318,8 @@ export async function burn(c, a) {
|
|
|
297
318
|
a.key,
|
|
298
319
|
a.ids,
|
|
299
320
|
a.shares,
|
|
300
|
-
a.minBaseOut
|
|
301
|
-
a.minQuoteOut
|
|
321
|
+
a.minBaseOut,
|
|
322
|
+
a.minQuoteOut,
|
|
302
323
|
resolveRecipient(c, a),
|
|
303
324
|
resolveDeadline(a),
|
|
304
325
|
],
|
package/dist/pools.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface DiscoveredMarket {
|
|
|
73
73
|
x0: bigint;
|
|
74
74
|
blockNumber: bigint;
|
|
75
75
|
transactionHash: Hex;
|
|
76
|
+
/** The manager that emitted the `Initialize` — set when the row came from the indexer. */
|
|
77
|
+
manager?: Address;
|
|
76
78
|
}
|
|
77
79
|
/**
|
|
78
80
|
* Enumerate markets from `Initialize` logs.
|
package/dist/pools.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { formatUnits } from "viem";
|
|
15
15
|
import { logswapLensAbi, cPoolManagerAbi } from "./generated.js";
|
|
16
|
+
import { signerFor } from "./write.js";
|
|
16
17
|
import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
|
|
17
18
|
import { poolId } from "./keys.js";
|
|
18
19
|
/** Read a market's full state by key. */
|
|
@@ -137,19 +138,10 @@ export async function isValidFloor(c, key, floor) {
|
|
|
137
138
|
* magnitude while the contract behaves perfectly — which reads as "the app is broken".
|
|
138
139
|
*/
|
|
139
140
|
export async function marketDecimals(c, key) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
return Number(await c.public.readContract({ address: token, abi, functionName: "decimals" }));
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return 18; // same fallback the manager uses when the token does not implement it
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
const [base, quote] = await Promise.all([read(key.base), read(key.quote)]);
|
|
152
|
-
return { base, quote };
|
|
141
|
+
// It used to fall back to 18 on ANY failure, a dropped request included — a 6-decimal quote read
|
|
142
|
+
// as 18 is every amount 10^12 off. `marketTokens` retries and then refuses; so does this.
|
|
143
|
+
const { base, quote } = await marketTokens(c, key);
|
|
144
|
+
return { base: base.decimals, quote: quote.decimals };
|
|
153
145
|
}
|
|
154
146
|
/**
|
|
155
147
|
* Both sides of a market, for labelling and scaling.
|
|
@@ -318,7 +310,7 @@ export async function initializeMarket(c, a) {
|
|
|
318
310
|
abi: cPoolManagerAbi,
|
|
319
311
|
functionName: "initialize",
|
|
320
312
|
args: [a.key, a.x0],
|
|
321
|
-
account: a.account,
|
|
313
|
+
account: signerFor(c, a.account),
|
|
322
314
|
});
|
|
323
315
|
return c.wallet.writeContract(request);
|
|
324
316
|
}
|
package/dist/write.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ export declare function resolveDeadline(o: WriteOptions | undefined, now?: numbe
|
|
|
38
38
|
* until someone uses a key the node does not have — a bot, a script, CI, or a real user's wallet.
|
|
39
39
|
* Passing the object lets viem pick the correct path for both kinds.
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* The wallet's account OBJECT for a write the caller named by address — see {@link requireWallet}
|
|
43
|
+
* for why the object and never the address. The F helpers take `account: Address` in their
|
|
44
|
+
* arguments; passed straight to viem it became a JSON-RPC account and the NODE was asked to sign,
|
|
45
|
+
* which works only on a dev node holding the key (every agent on the devnet did exactly that).
|
|
46
|
+
* A named account that is not the wallet's is refused: nothing else could sign for it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function signerFor(c: LogswapClient, address: Address): NonNullable<LogswapClient["wallet"]>["account"];
|
|
41
49
|
export declare function requireWallet(c: LogswapClient): {
|
|
42
50
|
account: NonNullable<LogswapClient["wallet"]>["account"];
|
|
43
51
|
address: Address;
|
package/dist/write.js
CHANGED
|
@@ -32,6 +32,20 @@ export function resolveDeadline(o, now = Date.now()) {
|
|
|
32
32
|
* until someone uses a key the node does not have — a bot, a script, CI, or a real user's wallet.
|
|
33
33
|
* Passing the object lets viem pick the correct path for both kinds.
|
|
34
34
|
*/
|
|
35
|
+
/**
|
|
36
|
+
* The wallet's account OBJECT for a write the caller named by address — see {@link requireWallet}
|
|
37
|
+
* for why the object and never the address. The F helpers take `account: Address` in their
|
|
38
|
+
* arguments; passed straight to viem it became a JSON-RPC account and the NODE was asked to sign,
|
|
39
|
+
* which works only on a dev node holding the key (every agent on the devnet did exactly that).
|
|
40
|
+
* A named account that is not the wallet's is refused: nothing else could sign for it.
|
|
41
|
+
*/
|
|
42
|
+
export function signerFor(c, address) {
|
|
43
|
+
const { account } = requireWallet(c);
|
|
44
|
+
if (account.address.toLowerCase() !== address.toLowerCase()) {
|
|
45
|
+
throw new Error(`logswap: the write names ${address} but the wallet signs as ${account.address}`);
|
|
46
|
+
}
|
|
47
|
+
return account;
|
|
48
|
+
}
|
|
35
49
|
export function requireWallet(c) {
|
|
36
50
|
const account = c.wallet?.account;
|
|
37
51
|
if (!c.wallet || !account) {
|
|
@@ -53,7 +67,8 @@ export function withSlippage(quoted, bps, side) {
|
|
|
53
67
|
if (bps < 0 || bps > 10_000)
|
|
54
68
|
throw new Error(`logswap: slippage bps out of range: ${bps}`);
|
|
55
69
|
const b = BigInt(Math.round(bps));
|
|
56
|
-
|
|
70
|
+
// an input bound rounds UP: flooring it would put a wei-exact quote at bps = 0 one wei short
|
|
71
|
+
return side === "output" ? (quoted * (10000n - b)) / 10000n : (quoted * (10000n + b) + 9999n) / 10000n;
|
|
57
72
|
}
|
|
58
73
|
/**
|
|
59
74
|
* Send a router write with a gas buffer over an explicit estimate.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logswap/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.54",
|
|
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/fpool.ts
CHANGED
|
@@ -48,6 +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
52
|
|
|
52
53
|
const WAD = 10n ** 18n;
|
|
53
54
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
|
|
@@ -359,7 +360,12 @@ export interface FPoolSwapArgs {
|
|
|
359
360
|
/** Leg index into `bases`. */
|
|
360
361
|
j: number;
|
|
361
362
|
amountIn: bigint;
|
|
362
|
-
|
|
363
|
+
/**
|
|
364
|
+
* The least the swap must return. Required: an F swap is exact-in, and without a floor on the
|
|
365
|
+
* output a front-run takes the difference. {@link fPoolQuoteSwap} is exact; bound it with
|
|
366
|
+
* {@link withSlippage} (`"output"`).
|
|
367
|
+
*/
|
|
368
|
+
minOut: bigint;
|
|
363
369
|
/** Who receives the out leg. Defaults to the account. */
|
|
364
370
|
to?: Address;
|
|
365
371
|
/** Absolute unix seconds. Defaulted to a bounded window, as every router write is. */
|
|
@@ -379,7 +385,7 @@ async function fSwap(c: LogswapClient, a: FPoolSwapArgs, kind: FPoolQuoteKind, k
|
|
|
379
385
|
return writeRouter(
|
|
380
386
|
c,
|
|
381
387
|
"swapExactIn",
|
|
382
|
-
[a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut
|
|
388
|
+
[a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut, a.to ?? a.account, a.deadline ?? defaultDeadline()],
|
|
383
389
|
a.account,
|
|
384
390
|
[pull],
|
|
385
391
|
);
|
|
@@ -407,10 +413,11 @@ export async function fPoolSwapBaseForBase(c: LogswapClient, a: FPoolSwapArgs &
|
|
|
407
413
|
/** Deposit dL/L of every reserve, receive shares ∝ dL. θ is untouched by construction. */
|
|
408
414
|
export async function fPoolMint(
|
|
409
415
|
c: LogswapClient,
|
|
410
|
-
|
|
416
|
+
/** `maxQuoteIn` is required: the quote leg is Q·dL/L at landing, and Q moves with every swap. */
|
|
417
|
+
a: { poolId: Hex; dL: bigint; maxQuoteIn: bigint; account: Address; to?: Address },
|
|
411
418
|
) {
|
|
412
419
|
// shares go to `to` (the account by default); on a gated pool `to` must be allowed
|
|
413
|
-
return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn
|
|
420
|
+
return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn], a.account);
|
|
414
421
|
}
|
|
415
422
|
|
|
416
423
|
/**
|
|
@@ -420,9 +427,27 @@ export async function fPoolMint(
|
|
|
420
427
|
*/
|
|
421
428
|
export async function fPoolBurn(
|
|
422
429
|
c: LogswapClient,
|
|
423
|
-
|
|
430
|
+
/** `minQuoteOut` is required: the quote slice is Q·shares/S at landing. See {@link fPoolPreviewBurn}. */
|
|
431
|
+
a: { poolId: Hex; shares: bigint; minQuoteOut: bigint; account: Address; owner?: Address },
|
|
424
432
|
) {
|
|
425
|
-
return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut
|
|
433
|
+
return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut], a.account);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* What burning `shares` returns at today's state — the manager's own arithmetic: every reserve and
|
|
438
|
+
* the quote pro rata, rounded down (FPoolManager.burn). Exact until the next swap or mint.
|
|
439
|
+
*/
|
|
440
|
+
export function fPoolPreviewBurn(st: Pick<FPoolState, "Q" | "reserves" | "totalSupply">, shares: bigint): { quoteOut: bigint; basesOut: bigint[] } {
|
|
441
|
+
if (st.totalSupply === 0n) return { quoteOut: 0n, basesOut: st.reserves.map(() => 0n) };
|
|
442
|
+
return { quoteOut: (st.Q * shares) / st.totalSupply, basesOut: st.reserves.map((r) => (r * shares) / st.totalSupply) };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The quote {@link fPoolMint} pulls for `dL` at today's state: Q·dL/L, rounded up (the pool never
|
|
447
|
+
* under-collects), before any mint fee — so bound it with {@link withSlippage} (`"input"`).
|
|
448
|
+
*/
|
|
449
|
+
export function fPoolPreviewMintQuote(st: Pick<FPoolState, "Q" | "L">, dL: bigint): bigint {
|
|
450
|
+
return st.L === 0n ? 0n : (st.Q * dL + st.L - 1n) / st.L;
|
|
426
451
|
}
|
|
427
452
|
|
|
428
453
|
// ─── zaps (on the router — one router, one Permit2 spender) ───────────────────
|
|
@@ -475,17 +500,37 @@ export async function fPoolZapIn(
|
|
|
475
500
|
*/
|
|
476
501
|
export async function fPoolZapOut(
|
|
477
502
|
c: LogswapClient,
|
|
478
|
-
|
|
503
|
+
/** `minOut` is required: the sale routes every leg through the curve. See {@link fPoolPreviewZapOut}. */
|
|
504
|
+
a: { poolId: Hex; shares: bigint; tokenOut: Address; minOut: bigint; to?: Address; account: Address; deadline?: bigint },
|
|
479
505
|
) {
|
|
480
506
|
const to = a.to ?? a.account;
|
|
481
507
|
return writeRouter(
|
|
482
508
|
c,
|
|
483
509
|
"zapOut",
|
|
484
|
-
[a.poolId, a.shares, a.tokenOut, a.minOut
|
|
510
|
+
[a.poolId, a.shares, a.tokenOut, a.minOut, to, a.deadline ?? defaultDeadline()],
|
|
485
511
|
a.account,
|
|
486
512
|
);
|
|
487
513
|
}
|
|
488
514
|
|
|
515
|
+
/**
|
|
516
|
+
* What {@link fPoolZapOut} returns at today's state: the router's own `zapOut`, simulated as the
|
|
517
|
+
* holder with no bound. Exact, but it needs what the real call needs — the shares, and the router
|
|
518
|
+
* as the holder's ERC-6909 operator — so it throws where the zap-out itself would revert.
|
|
519
|
+
*/
|
|
520
|
+
export async function fPoolPreviewZapOut(
|
|
521
|
+
c: LogswapClient,
|
|
522
|
+
a: { poolId: Hex; shares: bigint; tokenOut: Address; account: Address },
|
|
523
|
+
): Promise<bigint> {
|
|
524
|
+
const { result } = await c.public.simulateContract({
|
|
525
|
+
address: c.addresses.router,
|
|
526
|
+
abi: logswapRouterAbi,
|
|
527
|
+
functionName: "zapOut",
|
|
528
|
+
args: [a.poolId, a.shares, a.tokenOut, 0n, a.account, defaultDeadline()],
|
|
529
|
+
account: a.account,
|
|
530
|
+
} as never);
|
|
531
|
+
return result as bigint;
|
|
532
|
+
}
|
|
533
|
+
|
|
489
534
|
/**
|
|
490
535
|
* Roll a retired generation's bundle into the pool's live one (decisions 025, 032), on the
|
|
491
536
|
* router under one lock: redeem `shares` of generation `gen` in place — the legs, the quote and
|
|
@@ -600,7 +645,7 @@ async function writeFPool(c: LogswapClient, poolId: Hex, functionName: string, a
|
|
|
600
645
|
// one place all seven helpers share, is what keeps a caller from ever omitting it. The `as
|
|
601
646
|
// never` casts blind tsc to arg counts, so this file's encode test is the real guard.
|
|
602
647
|
args: [poolId, ...args],
|
|
603
|
-
account,
|
|
648
|
+
account: signerFor(c, account),
|
|
604
649
|
} as never);
|
|
605
650
|
return wallet.writeContract(request as never);
|
|
606
651
|
}
|
|
@@ -613,7 +658,7 @@ async function writeFPool0(c: LogswapClient, functionName: string, args: unknown
|
|
|
613
658
|
abi: fPoolManagerAbi,
|
|
614
659
|
functionName,
|
|
615
660
|
args,
|
|
616
|
-
account,
|
|
661
|
+
account: signerFor(c, account),
|
|
617
662
|
} as never);
|
|
618
663
|
return wallet.writeContract(request as never);
|
|
619
664
|
}
|
|
@@ -644,7 +689,7 @@ async function writeRouter(
|
|
|
644
689
|
abi: logswapRouterAbi,
|
|
645
690
|
functionName: fn,
|
|
646
691
|
args: sendArgs,
|
|
647
|
-
account,
|
|
692
|
+
account: signerFor(c, account),
|
|
648
693
|
} as never);
|
|
649
694
|
return wallet.writeContract(request as never);
|
|
650
695
|
}
|
|
@@ -705,7 +750,7 @@ export async function approveRouterForFPool(c: LogswapClient, account: Address)
|
|
|
705
750
|
abi: fPoolManagerAbi,
|
|
706
751
|
functionName: "setOperator",
|
|
707
752
|
args: [c.addresses.router, true],
|
|
708
|
-
account,
|
|
753
|
+
account: signerFor(c, account),
|
|
709
754
|
} as never);
|
|
710
755
|
return wallet.writeContract(request as never);
|
|
711
756
|
}
|
|
@@ -1225,7 +1270,7 @@ async function writePolicy(c: LogswapClient, abi: readonly unknown[], a: { poolI
|
|
|
1225
1270
|
abi,
|
|
1226
1271
|
functionName,
|
|
1227
1272
|
args: [a.poolId, ...args],
|
|
1228
|
-
account: a.account,
|
|
1273
|
+
account: signerFor(c, a.account),
|
|
1229
1274
|
} as never);
|
|
1230
1275
|
return wallet.writeContract(request as never);
|
|
1231
1276
|
}
|
|
@@ -1317,7 +1362,7 @@ export async function fPoolSetSponsorName(c: LogswapClient, a: { who: Address; n
|
|
|
1317
1362
|
abi: policyRegistryAbi,
|
|
1318
1363
|
functionName: "setSponsorName",
|
|
1319
1364
|
args: [a.who, textToBytes32(a.name)],
|
|
1320
|
-
account: a.account,
|
|
1365
|
+
account: signerFor(c, a.account),
|
|
1321
1366
|
} as never);
|
|
1322
1367
|
return wallet.writeContract(request as never);
|
|
1323
1368
|
}
|
|
@@ -1392,7 +1437,7 @@ export async function fPoolTransferShares(
|
|
|
1392
1437
|
abi: fPoolManagerAbi,
|
|
1393
1438
|
functionName: "transfer",
|
|
1394
1439
|
args: [a.to, fPoolShareId(a.poolId, gen), a.shares],
|
|
1395
|
-
account: a.account,
|
|
1440
|
+
account: signerFor(c, a.account),
|
|
1396
1441
|
} as never);
|
|
1397
1442
|
return wallet.writeContract(request as never);
|
|
1398
1443
|
}
|
package/src/indexer.ts
CHANGED
|
@@ -34,25 +34,70 @@ export function bytea(hex: Hex): string {
|
|
|
34
34
|
return `\\x${hex.replace(/^0x/, "")}`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** How long an indexer call may take before the caller falls back (to the chain, or an empty view). */
|
|
38
|
+
export const INDEXER_TIMEOUT_MS = 8_000;
|
|
39
|
+
|
|
37
40
|
export async function gql<T>(
|
|
38
41
|
url: string,
|
|
39
42
|
query: string,
|
|
40
43
|
variables?: Record<string, unknown>,
|
|
41
44
|
signal?: AbortSignal,
|
|
42
45
|
): Promise<T> {
|
|
46
|
+
// A hung indexer must not hang its caller: every consumer has a fallback, and it only runs when
|
|
47
|
+
// this call ends. The caller's signal, if any, still wins.
|
|
43
48
|
const res = await fetch(url, {
|
|
44
49
|
method: "POST",
|
|
45
50
|
headers: { "content-type": "application/json" },
|
|
46
51
|
body: JSON.stringify({ query, variables }),
|
|
47
|
-
signal,
|
|
52
|
+
signal: signal ?? AbortSignal.timeout(INDEXER_TIMEOUT_MS),
|
|
48
53
|
});
|
|
49
54
|
if (!res.ok) throw new Error(`indexer: HTTP ${res.status} from ${url}`);
|
|
55
|
+
// an Access login page or a proxy's error page answers 200 with HTML; say so, rather than
|
|
56
|
+
// "Unexpected token <"
|
|
57
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
58
|
+
if (!ct.includes("json")) throw new Error(`indexer: ${url} answered ${ct || "no content-type"}, not JSON (a login page?)`);
|
|
50
59
|
const body = (await res.json()) as { data?: T; errors?: IndexerError[] };
|
|
51
60
|
if (body.errors?.length) throw new Error(`indexer: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
52
61
|
if (!body.data) throw new Error("indexer: empty response");
|
|
53
62
|
return body.data;
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
const PAGE = 1000;
|
|
66
|
+
const MAX_PAGES = 100;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Every row of a connection, page by page. A single `first: 1000` silently drops row 1001 onward —
|
|
70
|
+
* a pool's 1001st position class, a deployment's 1001st market — with nothing to say so. Pages by
|
|
71
|
+
* cursor in primary-key order, so rows inserted while paging cannot shift the window.
|
|
72
|
+
*/
|
|
73
|
+
export async function gqlAll<N>(
|
|
74
|
+
url: string,
|
|
75
|
+
field: string,
|
|
76
|
+
selection: string,
|
|
77
|
+
opts: { vars?: Record<string, { type: string; value: unknown }>; condition?: string; signal?: AbortSignal } = {},
|
|
78
|
+
): Promise<N[]> {
|
|
79
|
+
const vars = opts.vars ?? {};
|
|
80
|
+
const decl = Object.entries(vars).map(([k, v]) => `, $${k}: ${v.type}`).join("");
|
|
81
|
+
const cond = opts.condition ? `, condition: ${opts.condition}` : "";
|
|
82
|
+
const q = `query Page($after: Cursor${decl}) {
|
|
83
|
+
${field}(first: ${PAGE}, after: $after, orderBy: PRIMARY_KEY_ASC${cond}) {
|
|
84
|
+
pageInfo { hasNextPage endCursor } nodes { ${selection} }
|
|
85
|
+
}
|
|
86
|
+
}`;
|
|
87
|
+
const values = Object.fromEntries(Object.entries(vars).map(([k, v]) => [k, v.value]));
|
|
88
|
+
const out: N[] = [];
|
|
89
|
+
let after: string | null = null;
|
|
90
|
+
for (let i = 0; i < MAX_PAGES; i++) {
|
|
91
|
+
type Conn = { pageInfo: { hasNextPage: boolean; endCursor: string | null }; nodes: N[] };
|
|
92
|
+
const d: Record<string, Conn> = await gql<Record<string, Conn>>(url, q, { ...values, after }, opts.signal);
|
|
93
|
+
const page: Conn = d[field]!;
|
|
94
|
+
out.push(...page.nodes);
|
|
95
|
+
if (!page.pageInfo.hasNextPage || !page.pageInfo.endCursor) return out;
|
|
96
|
+
after = page.pageInfo.endCursor;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`indexer: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
99
|
+
}
|
|
100
|
+
|
|
56
101
|
/** A position class as the chain registered it: the floor/cap geometry, fixed for the id's life. */
|
|
57
102
|
export interface RegisteredId {
|
|
58
103
|
id: bigint;
|
|
@@ -63,16 +108,12 @@ export interface RegisteredId {
|
|
|
63
108
|
|
|
64
109
|
/** Every position class ever registered in a pool. Historical — some may since have burnt to zero. */
|
|
65
110
|
export async function registeredIds(url: string, poolId: Hex, signal?: AbortSignal): Promise<RegisteredId[]> {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const d = await gql<{ allIdRegistereds: { nodes: { id: string; floor: string; cap: string; capped: boolean }[] } }>(
|
|
70
|
-
url,
|
|
71
|
-
q,
|
|
72
|
-
{ pool: bytea(poolId) },
|
|
111
|
+
const nodes = await gqlAll<{ id: string; floor: string; cap: string; capped: boolean }>(url, "allIdRegistereds", "id floor cap capped", {
|
|
112
|
+
vars: { pool: { type: "String!", value: bytea(poolId) } },
|
|
113
|
+
condition: "{poolId: $pool}",
|
|
73
114
|
signal,
|
|
74
|
-
);
|
|
75
|
-
return
|
|
115
|
+
});
|
|
116
|
+
return nodes.map((n) => ({
|
|
76
117
|
id: BigInt(n.id),
|
|
77
118
|
floor: BigInt(n.floor),
|
|
78
119
|
cap: BigInt(n.cap),
|
|
@@ -142,15 +183,12 @@ export async function indexerHealthy(url: string, signal?: AbortSignal): Promise
|
|
|
142
183
|
* everywhere else, so a wrong id fails loudly instead of addressing the wrong pool.
|
|
143
184
|
*/
|
|
144
185
|
export async function discoverMarketsIndexed(url: string, signal?: AbortSignal): Promise<DiscoveredMarket[]> {
|
|
145
|
-
const q = `{ allInitializes(first: 1000) { nodes {
|
|
146
|
-
poolId base quote tickSpacing phi x0 blockNumber txHash
|
|
147
|
-
} } }`;
|
|
148
186
|
type Row = {
|
|
149
187
|
poolId: Hex; base: Hex; quote: Hex; tickSpacing: string; phi: string;
|
|
150
|
-
x0: string; blockNumber: string; txHash: Hex;
|
|
188
|
+
x0: string; blockNumber: string; txHash: Hex; contractAddress: Hex;
|
|
151
189
|
};
|
|
152
|
-
const
|
|
153
|
-
return
|
|
190
|
+
const nodes = await gqlAll<Row>(url, "allInitializes", "poolId base quote tickSpacing phi x0 blockNumber txHash contractAddress", { signal });
|
|
191
|
+
return nodes.map((n) => {
|
|
154
192
|
const key: PoolKey = {
|
|
155
193
|
base: n.base as Address,
|
|
156
194
|
quote: n.quote as Address,
|
|
@@ -163,6 +201,7 @@ export async function discoverMarketsIndexed(url: string, signal?: AbortSignal):
|
|
|
163
201
|
x0: BigInt(n.x0),
|
|
164
202
|
blockNumber: BigInt(n.blockNumber),
|
|
165
203
|
transactionHash: n.txHash,
|
|
204
|
+
manager: n.contractAddress,
|
|
166
205
|
};
|
|
167
206
|
});
|
|
168
207
|
}
|
|
@@ -181,31 +220,14 @@ export async function discoverMarketsBest(
|
|
|
181
220
|
if (url) {
|
|
182
221
|
try {
|
|
183
222
|
const markets = await discoverMarketsIndexed(url, signal);
|
|
184
|
-
// The indexer is a CACHE and the chain is the truth. After a redeploy
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
markets.map(async (m) => {
|
|
193
|
-
try {
|
|
194
|
-
await c.public.readContract({
|
|
195
|
-
address: c.addresses.cPoolManager,
|
|
196
|
-
abi: cPoolManagerAbi,
|
|
197
|
-
functionName: "phiOf",
|
|
198
|
-
args: [m.key],
|
|
199
|
-
} as never);
|
|
200
|
-
return m;
|
|
201
|
-
} catch {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
}),
|
|
205
|
-
)
|
|
206
|
-
).filter((m): m is DiscoveredMarket => m !== null);
|
|
207
|
-
if (alive.length) return { markets: alive, source: "indexer" };
|
|
208
|
-
}
|
|
223
|
+
// The indexer is a CACHE and the chain is the truth. After a redeploy it keeps serving the
|
|
224
|
+
// previous deployment's markets, and calling the new manager with those keys makes every
|
|
225
|
+
// read revert, refresh-proof. Each row names the manager that emitted it, so keep only the
|
|
226
|
+
// rows of the manager this client is pointed at — no read per row. None left means the
|
|
227
|
+
// indexer has not seen this deployment: the chain scan is the answer.
|
|
228
|
+
const mine = c.addresses.cPoolManager.toLowerCase();
|
|
229
|
+
const alive = markets.filter((m) => m.manager?.toLowerCase() === mine);
|
|
230
|
+
if (alive.length) return { markets: alive, source: "indexer" };
|
|
209
231
|
} catch {
|
|
210
232
|
/* fall through — a missing indexer must not cost you the market list */
|
|
211
233
|
}
|