@logswap/sdk 0.5.73 → 0.5.75
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/README.md +40 -0
- package/dist/admin.d.ts +195 -0
- package/dist/admin.js +91 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +12 -2
- package/dist/fees.d.ts +2 -1
- package/dist/fees.js +2 -1
- package/dist/fpool.d.ts +181 -2
- package/dist/fpool.js +5 -9
- package/dist/generated.d.ts +1113 -214
- package/dist/generated.js +736 -101
- package/dist/history.d.ts +82 -2
- package/dist/history.js +108 -16
- package/dist/indexer.js +1 -6
- package/dist/keys.d.ts +8 -6
- package/dist/keys.js +6 -5
- package/dist/liquidity.d.ts +8 -1
- package/dist/liquidity.js +5 -2
- package/dist/pools.d.ts +19 -10
- package/dist/pools.js +33 -25
- package/dist/positions.d.ts +23 -9
- package/dist/positions.js +39 -17
- package/package.json +1 -1
- package/src/admin.ts +123 -1
- package/src/client.ts +14 -2
- package/src/fees.ts +4 -3
- package/src/fpool.ts +16 -12
- package/src/generated.ts +736 -101
- package/src/history.ts +150 -17
- package/src/indexer.ts +1 -6
- package/src/keys.ts +8 -6
- package/src/liquidity.ts +10 -2
- package/src/pools.ts +45 -35
- package/src/positions.ts +43 -19
package/src/history.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* the decoding (the inputs' types — numbers as bigint, addresses checksummed, arrays from JSON).
|
|
14
14
|
* With no indexer URL, or when the query fails, it asks the node and says so in `source`.
|
|
15
15
|
*/
|
|
16
|
-
import { getAddress, type AbiEvent, type Address, type Hex } from "viem";
|
|
16
|
+
import { getAddress, parseAbiItem, type AbiEvent, type Address, type Hex } from "viem";
|
|
17
17
|
import type { LogswapClient } from "./client.js";
|
|
18
|
+
import type { FPoolKind } from "./fpool.js";
|
|
18
19
|
import { bytea, gql } from "./indexer.js";
|
|
19
20
|
import { blockAtOrAfter } from "./fees.js";
|
|
20
21
|
|
|
@@ -32,7 +33,14 @@ export interface HistoryLog<A = Record<string, unknown>> {
|
|
|
32
33
|
export interface EventLogsQuery {
|
|
33
34
|
/** The rindexer GraphQL endpoint; absent means the chain. */
|
|
34
35
|
indexer?: string;
|
|
35
|
-
|
|
36
|
+
/** The emitter, or several: the four policies share one table per event (indexer phase 1). */
|
|
37
|
+
address: Address | readonly Address[];
|
|
38
|
+
/**
|
|
39
|
+
* The emitter's name in the indexer's config, for an event two indexed contracts share — only
|
|
40
|
+
* needed when the address is not in the client's artifact (a policy the registry approved later):
|
|
41
|
+
* see {@link indexedContractOf}.
|
|
42
|
+
*/
|
|
43
|
+
contract?: string;
|
|
36
44
|
event: AbiEvent;
|
|
37
45
|
/** Indexed arguments to match, as `getLogs` takes them; an array matches any of its values. */
|
|
38
46
|
args?: Record<string, unknown>;
|
|
@@ -70,29 +78,52 @@ export async function getEventLogs<A = Record<string, unknown>>(
|
|
|
70
78
|
|
|
71
79
|
// ─── the indexer path ────────────────────────────────────────────────────────
|
|
72
80
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
81
|
+
/**
|
|
82
|
+
* What the rindexer deployment calls each contract — the prefix of a table two contracts share
|
|
83
|
+
* (logswap-be `devnet/indexer/rindexer.yaml`). The four policies are one factory-tracked contract,
|
|
84
|
+
* `Policy`, except `Listed`, whose two signatures are two contracts (`PublicPolicy`, `PrivatePolicy`).
|
|
85
|
+
*/
|
|
86
|
+
function contractName(c: LogswapClient, address: Address, event: string): string | null {
|
|
75
87
|
const a = address.toLowerCase();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
88
|
+
const is = (x?: Address) => x !== undefined && a === x.toLowerCase();
|
|
89
|
+
const { addresses: d } = c;
|
|
90
|
+
if (is(d.cPoolManager)) return "LogswapManager";
|
|
91
|
+
if (is(d.fPoolManager)) return "FPoolManager";
|
|
92
|
+
if (is(d.router)) return "LogswapRouter";
|
|
93
|
+
if (is(d.policyRegistry)) return "PolicyRegistry";
|
|
94
|
+
if (is(d.tokenFactory)) return "TokenFactory";
|
|
95
|
+
if (is(d.publicPolicy)) return indexedContractOf("public", event);
|
|
96
|
+
if (is(d.privatePolicy)) return indexedContractOf("private", event);
|
|
97
|
+
if (is(d.polPolicy)) return indexedContractOf("pol", event);
|
|
98
|
+
if (is(d.padPolicy)) return indexedContractOf("pad", event);
|
|
79
99
|
return null;
|
|
80
100
|
}
|
|
81
101
|
|
|
102
|
+
/**
|
|
103
|
+
* A policy's name in the indexer for one of its events: `Policy`, or the kind's own contract for the
|
|
104
|
+
* two-signature `Listed`. Pass it as {@link EventLogsQuery.contract} for a policy outside the artifact.
|
|
105
|
+
*/
|
|
106
|
+
export function indexedContractOf(kind: FPoolKind, event: string): string {
|
|
107
|
+
if (event === "Listed") return kind === "private" ? "PrivatePolicy" : "PublicPolicy";
|
|
108
|
+
return "Policy";
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
/** Events whose name two indexed contracts share: postgraphile prefixes their tables. */
|
|
83
|
-
const SHARED = new Set(["Transfer"]);
|
|
112
|
+
const SHARED = new Set(["Transfer", "Approval", "OperatorSet", "CollectorProposed", "CollectorAccepted", "Approved", "Listed"]);
|
|
84
113
|
|
|
85
114
|
const plural = (n: string) => (/(s|x|ch|sh)$/.test(n) ? `${n}es` : `${n}s`);
|
|
86
115
|
/** A column's GraphQL name: postgraphile camelCases the snake_case rindexer writes (`F` → `f`). */
|
|
87
116
|
const column = (name: string) => name[0]!.toLowerCase() + name.slice(1);
|
|
88
117
|
|
|
89
|
-
export function tableFor(c: LogswapClient, address: Address, event: string): { field: string; type: string } {
|
|
90
|
-
const prefix = SHARED.has(event) ? contractName(c, address) : "";
|
|
118
|
+
export function tableFor(c: LogswapClient, address: Address, event: string, contract?: string): { field: string; type: string } {
|
|
119
|
+
const prefix = SHARED.has(event) ? (contract ?? contractName(c, address, event)) : "";
|
|
91
120
|
if (prefix === null) throw new Error(`history: no indexed contract at ${address}`);
|
|
92
121
|
const type = `${prefix}${event}`;
|
|
93
122
|
return { field: `all${plural(type)}`, type };
|
|
94
123
|
}
|
|
95
124
|
|
|
125
|
+
const emitters = (q: { address: Address | readonly Address[] }): Address[] => (Array.isArray(q.address) ? [...q.address] : [q.address as Address]);
|
|
126
|
+
|
|
96
127
|
function decode(type: string, v: unknown): unknown {
|
|
97
128
|
if (v === null || v === undefined) return v;
|
|
98
129
|
if (type.endsWith("[]")) return (v as unknown[]).map((x) => decode(type.slice(0, -2), x));
|
|
@@ -107,7 +138,9 @@ const MAX_PAGES = 200;
|
|
|
107
138
|
const PARALLEL_PAGES = 8;
|
|
108
139
|
|
|
109
140
|
async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
110
|
-
const
|
|
141
|
+
const all = emitters(q);
|
|
142
|
+
if (all.length === 0) return [];
|
|
143
|
+
const { field, type } = tableFor(c, all[0]!, q.event.name, q.contract);
|
|
111
144
|
const inputs = q.event.inputs;
|
|
112
145
|
const condition: Record<string, unknown> = {};
|
|
113
146
|
const filter: Record<string, unknown> = {};
|
|
@@ -164,11 +197,11 @@ async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<Hist
|
|
|
164
197
|
}
|
|
165
198
|
}
|
|
166
199
|
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
167
|
-
const want =
|
|
200
|
+
const want = new Map(all.map((a) => [a.toLowerCase(), a]));
|
|
168
201
|
return rows
|
|
169
|
-
.filter((r) => String(r.contractAddress).toLowerCase()
|
|
202
|
+
.filter((r) => want.has(String(r.contractAddress).toLowerCase()))
|
|
170
203
|
.map((r) => ({
|
|
171
|
-
address:
|
|
204
|
+
address: want.get(String(r.contractAddress).toLowerCase())!,
|
|
172
205
|
args: Object.fromEntries(inputs.map((i) => [i.name!, decode(i.type, r[column(i.name!)])])) as A,
|
|
173
206
|
blockNumber: BigInt(r.blockNumber as string),
|
|
174
207
|
blockTimestamp: Math.floor(Date.parse(r.blockTimestamp as string) / 1000),
|
|
@@ -185,10 +218,13 @@ export const LATEST_SCAN_BLOCKS = 5_000n;
|
|
|
185
218
|
|
|
186
219
|
async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
187
220
|
let fromBlock = q.fromBlock ?? 0n;
|
|
188
|
-
const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
|
|
221
|
+
const toBlock = q.toBlock ?? (await c.public.getBlockNumber({ cacheTime: 0 })); // uncached: viem caches the head, and a scan bounded by a stale one misses the block just mined
|
|
189
222
|
if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
190
223
|
if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS) fromBlock = toBlock - LATEST_SCAN_BLOCKS;
|
|
191
|
-
|
|
224
|
+
const all = emitters(q);
|
|
225
|
+
if (all.length === 0) return [];
|
|
226
|
+
let logs = (await c.public.getLogs({ address: Array.isArray(q.address) ? all : q.address, event: q.event, args: q.args, fromBlock, toBlock } as never)) as unknown as Array<{
|
|
227
|
+
address?: Address;
|
|
192
228
|
args: A;
|
|
193
229
|
blockNumber: bigint;
|
|
194
230
|
transactionHash: Hex;
|
|
@@ -200,10 +236,107 @@ async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<Histor
|
|
|
200
236
|
}
|
|
201
237
|
if (q.latest !== undefined) logs = logs.slice(-q.latest);
|
|
202
238
|
return logs.map((l) => ({
|
|
203
|
-
address:
|
|
239
|
+
address: l.address ? getAddress(l.address) : all[0]!,
|
|
204
240
|
args: l.args,
|
|
205
241
|
blockNumber: l.blockNumber,
|
|
206
242
|
transactionHash: l.transactionHash,
|
|
207
243
|
logIndex: l.logIndex,
|
|
208
244
|
}));
|
|
209
245
|
}
|
|
246
|
+
|
|
247
|
+
// ─── several events, and the token factory ────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/** A row of {@link getEventsLogs}: one of the asked events, named. */
|
|
250
|
+
export interface NamedHistoryLog extends HistoryLog {
|
|
251
|
+
eventName: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Several events of one contract (or of several emitters sharing them), merged oldest first and
|
|
256
|
+
* tagged with `eventName` — the shape of viem's multi-event `getLogs`, which is what the chain path
|
|
257
|
+
* runs. One indexer table per event; if any of them cannot answer, the whole read goes to the node,
|
|
258
|
+
* so a history is never half the indexer's and half the node's.
|
|
259
|
+
*/
|
|
260
|
+
export async function getEventsLogs(
|
|
261
|
+
c: LogswapClient,
|
|
262
|
+
q: Omit<EventLogsQuery, "event" | "args" | "latest" | "contract" | "transactionHashes"> & {
|
|
263
|
+
events: readonly AbiEvent[];
|
|
264
|
+
/** The emitters' policy kind, to name a shared event's table for a policy outside the artifact. */
|
|
265
|
+
kind?: FPoolKind;
|
|
266
|
+
/** The emitter's name in the indexer (e.g. `PolicyRegistry`), for a contract outside the artifact; `kind` wins for a policy. */
|
|
267
|
+
contract?: string;
|
|
268
|
+
},
|
|
269
|
+
): Promise<{ logs: NamedHistoryLog[]; source: "indexer" | "chain" }> {
|
|
270
|
+
const order = (a: HistoryLog, b: HistoryLog) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1);
|
|
271
|
+
if (q.indexer && q.events.length > 0) {
|
|
272
|
+
try {
|
|
273
|
+
const got = await Promise.all(
|
|
274
|
+
q.events.map(async (event) => {
|
|
275
|
+
const contract = q.kind ? indexedContractOf(q.kind, event.name) : q.contract;
|
|
276
|
+
return (await fromIndexer<Record<string, unknown>>(c, { ...q, event, contract })).map((l) => ({ ...l, eventName: event.name }));
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
return { logs: got.flat().sort(order), source: "indexer" };
|
|
280
|
+
} catch {
|
|
281
|
+
/* the indexer is a cache: the node holds the same events */
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const all = emitters(q);
|
|
285
|
+
if (all.length === 0 || q.events.length === 0) return { logs: [], source: "chain" };
|
|
286
|
+
let fromBlock = q.fromBlock ?? 0n;
|
|
287
|
+
const toBlock = q.toBlock ?? (await c.public.getBlockNumber({ cacheTime: 0 })); // uncached: viem caches the head, and a scan bounded by a stale one misses the block just mined
|
|
288
|
+
if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
289
|
+
const logs = (await c.public.getLogs({ address: Array.isArray(q.address) ? all : q.address, events: q.events, fromBlock, toBlock } as never)) as unknown as Array<{
|
|
290
|
+
eventName: string;
|
|
291
|
+
address?: Address;
|
|
292
|
+
args: Record<string, unknown>;
|
|
293
|
+
blockNumber: bigint;
|
|
294
|
+
transactionHash: Hex;
|
|
295
|
+
logIndex: number;
|
|
296
|
+
}>;
|
|
297
|
+
return {
|
|
298
|
+
logs: logs
|
|
299
|
+
.map((l) => ({ eventName: l.eventName, address: l.address ? getAddress(l.address) : all[0]!, args: l.args, blockNumber: l.blockNumber, transactionHash: l.transactionHash, logIndex: l.logIndex }))
|
|
300
|
+
.sort(order),
|
|
301
|
+
source: "chain",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The devnet's token factory event (`src/mocks/TokenFactory.sol`): provenance for every launchable token. */
|
|
306
|
+
export const TOKEN_CREATED = parseAbiItem("event TokenCreated(address indexed creator, address indexed token, string name, string symbol, uint256 supply)");
|
|
307
|
+
|
|
308
|
+
export interface CreatedToken {
|
|
309
|
+
creator: Address;
|
|
310
|
+
token: Address;
|
|
311
|
+
name: string;
|
|
312
|
+
symbol: string;
|
|
313
|
+
supply: bigint;
|
|
314
|
+
blockNumber: bigint;
|
|
315
|
+
/** Unix seconds; set when the indexer answered. */
|
|
316
|
+
blockTimestamp?: number;
|
|
317
|
+
transactionHash: Hex;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Every token the deployment's factory created, oldest first — the token list's provenance (a token
|
|
322
|
+
* named like a factory token but deployed elsewhere is not in it). Optionally one creator's.
|
|
323
|
+
*/
|
|
324
|
+
export async function tokensCreated(
|
|
325
|
+
c: LogswapClient,
|
|
326
|
+
q: { indexer?: string; creator?: Address; fromBlock?: bigint; signal?: AbortSignal },
|
|
327
|
+
): Promise<{ tokens: CreatedToken[]; source: "indexer" | "chain" }> {
|
|
328
|
+
const factory = c.addresses.tokenFactory;
|
|
329
|
+
if (!factory) throw new Error("logswap: tokensCreated needs addresses.tokenFactory");
|
|
330
|
+
const { logs, source } = await getEventLogs<{ creator: Address; token: Address; name: string; symbol: string; supply: bigint }>(c, {
|
|
331
|
+
indexer: q.indexer,
|
|
332
|
+
address: factory,
|
|
333
|
+
event: TOKEN_CREATED,
|
|
334
|
+
args: q.creator ? { creator: q.creator } : undefined,
|
|
335
|
+
fromBlock: q.fromBlock,
|
|
336
|
+
signal: q.signal,
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
tokens: logs.map((l) => ({ ...l.args, blockNumber: l.blockNumber, ...(l.blockTimestamp !== undefined ? { blockTimestamp: l.blockTimestamp } : {}), transactionHash: l.transactionHash })),
|
|
340
|
+
source,
|
|
341
|
+
};
|
|
342
|
+
}
|
package/src/indexer.ts
CHANGED
|
@@ -258,12 +258,7 @@ export async function poolPositionsFromChain(c: LogswapClient, key: PoolKey, x:
|
|
|
258
258
|
functionName: "idOf",
|
|
259
259
|
args: [key, floor],
|
|
260
260
|
} as never)) as bigint;
|
|
261
|
-
const
|
|
262
|
-
address: c.addresses.cPoolManager,
|
|
263
|
-
abi: cPoolManagerAbi,
|
|
264
|
-
functionName: "positions",
|
|
265
|
-
args: [id],
|
|
266
|
-
} as never)) as readonly [bigint, bigint, bigint, bigint];
|
|
261
|
+
const { L, shares } = await getPositionClass(c, id); // through extsload (C version 7)
|
|
267
262
|
if (L === 0n) return null;
|
|
268
263
|
return { id, poolId: pool, floor, cap: NO_CAP, capped: false, L, shares, active: x >= floor } as PoolPosition;
|
|
269
264
|
}),
|
package/src/keys.ts
CHANGED
|
@@ -15,7 +15,9 @@ import { encodeAbiParameters, keccak256, type Address, type Hex } from "viem";
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The fee domain of the family, `FeeParams.FEE_MIN` / `FEE_MAX` (decisions 043): one bound for a
|
|
18
|
-
* C market's φ and an F pool's alike, WAD. 1 bp and 1
|
|
18
|
+
* C market's φ and an F pool's alike, WAD. 1 bp and 1% — plus 0, which since C version 7 / F
|
|
19
|
+
* version 26 is a fee-free market or pool, admitted only if the policy lists it (decisions 065).
|
|
20
|
+
* The mint fee lives in the same domain. The F manager no longer answers
|
|
19
21
|
* `FEE_MIN()` / `FEE_MAX()` (F version 21 cut the getters for bytecode); every F policy still
|
|
20
22
|
* checks φ against these at `onInitialize`, and the C side re-exposes them through the lens's
|
|
21
23
|
* `cParams()`. Constants here, so a create form bounds φ with no read.
|
|
@@ -34,7 +36,8 @@ export interface PoolKey {
|
|
|
34
36
|
tickSpacing: bigint;
|
|
35
37
|
/**
|
|
36
38
|
* The swap fee, WAD, fixed for the life of the market: {@link FEE_MIN} (1 bp) ≤ φ ≤ {@link FEE_MAX} (1%),
|
|
37
|
-
* or `0n`, which
|
|
39
|
+
* or `0n`, which since C version 7 is NO fee (decisions 065; `DEFAULT_FEE` is gone). The gate
|
|
40
|
+
* admits only its listed tiers (064: 2, 5, 10, 25, 50, 100 bp). Four fields since C version 4
|
|
38
41
|
*: `kappa` and `alpha` left with the variance kernel, and `phiMin` — a floor with
|
|
39
42
|
* nothing above it — became `phi`, named `swapFee` since C version 6 (decisions 059). The name
|
|
40
43
|
* is not part of the encoding: no pool id changed with it.
|
|
@@ -57,10 +60,9 @@ const POOL_KEY_ABI = [
|
|
|
57
60
|
/**
|
|
58
61
|
* `poolId = keccak256(abi.encode(key))` — the primary key of the entire read model.
|
|
59
62
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* market by the key it was initialized with.
|
|
63
|
+
* A key's `swapFee` is the market's fee as written — `0n` is a fee-free market since C version 7
|
|
64
|
+
* (decisions 065), not the 2 bp default it selected before. Always address a market by the key it
|
|
65
|
+
* was initialized with.
|
|
64
66
|
*/
|
|
65
67
|
export function poolId(key: PoolKey): Hex {
|
|
66
68
|
return keccak256(
|
package/src/liquidity.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { logswapLensAbi, logswapRouterAbi } from "./generated.js";
|
|
|
27
27
|
import type { LogswapClient } from "./client.js";
|
|
28
28
|
import type { PoolKey } from "./keys.js";
|
|
29
29
|
import { NO_CAP } from "./ids.js";
|
|
30
|
-
import { describeId, getPositionClass } from "./positions.js";
|
|
30
|
+
import { describeId, getPositionClass, presetMintFee } from "./positions.js";
|
|
31
31
|
import {
|
|
32
32
|
execute,
|
|
33
33
|
OPEN_DELTA,
|
|
@@ -105,6 +105,11 @@ export interface CreateMarketArgs extends WriteOptions {
|
|
|
105
105
|
targetFloor: bigint;
|
|
106
106
|
/** The founding exposure; must exceed the manager's `minBackstopL` lock. */
|
|
107
107
|
L: bigint;
|
|
108
|
+
/**
|
|
109
|
+
* The market's mint fee, WAD of newly-earning L — the creator's pick from the gate's menu
|
|
110
|
+
* (decisions 065). Defaults to {@link presetMintFee}: twice the swap fee, capped at `FEE_MAX`.
|
|
111
|
+
*/
|
|
112
|
+
mintFee?: bigint;
|
|
108
113
|
/** 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
114
|
maxBaseIn: bigint;
|
|
110
115
|
maxQuoteIn: bigint;
|
|
@@ -112,7 +117,9 @@ export interface CreateMarketArgs extends WriteOptions {
|
|
|
112
117
|
|
|
113
118
|
/**
|
|
114
119
|
* Create a market ATOMICALLY: `initialize` at `x0` plus the backstop-establishing first mint in
|
|
115
|
-
* one transaction — the canonical creation path.
|
|
120
|
+
* one transaction — the canonical creation path. Under the C gate (064) the router is a trusted
|
|
121
|
+
* forwarder: the gate checks the router's `user()` — the wallet sending this — against the
|
|
122
|
+
* creator list, so a listed creator opens and funds a market in this one call. The
|
|
116
123
|
* two-step path let an attacker seat the PERMANENT backstop at a hostile rung between an honest
|
|
117
124
|
* `initialize` and its first mint; here either both land or neither, and a front-run of the whole
|
|
118
125
|
* call reverts it without moving a wei. No cap: the first mint IS the backstop and must be
|
|
@@ -125,6 +132,7 @@ export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promi
|
|
|
125
132
|
args: [
|
|
126
133
|
a.key,
|
|
127
134
|
a.x0,
|
|
135
|
+
a.mintFee ?? presetMintFee(a.key.swapFee),
|
|
128
136
|
a.targetFloor,
|
|
129
137
|
a.L,
|
|
130
138
|
a.maxBaseIn,
|
package/src/pools.ts
CHANGED
|
@@ -18,8 +18,13 @@ import type { LogswapClient } from "./client.js";
|
|
|
18
18
|
import { signerFor } from "./write.js";
|
|
19
19
|
import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
|
|
20
20
|
import { poolId, type PoolKey } from "./keys.js";
|
|
21
|
+
import { presetMintFee } from "./positions.js";
|
|
22
|
+
import { getEventLogs } from "./history.js";
|
|
21
23
|
|
|
22
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* A market's scalar state, field-for-field with `CPoolState.PoolView` — what the manager's `getPool`
|
|
26
|
+
* returned before C version 7 moved it behind `extsload` (decisions 065); read through the lens.
|
|
27
|
+
*/
|
|
23
28
|
export interface PoolState {
|
|
24
29
|
initialized: boolean;
|
|
25
30
|
backstopSeeded: boolean;
|
|
@@ -37,32 +42,31 @@ export interface PoolState {
|
|
|
37
42
|
protocolOwed: bigint;
|
|
38
43
|
minBackstopL: bigint;
|
|
39
44
|
backstopFloor: bigint;
|
|
45
|
+
/** The market's mint fee, WAD of newly-earning L: proposed at birth, the gate's answer (065). */
|
|
46
|
+
mintFee: bigint;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
/** Read a market's full state by key. */
|
|
49
|
+
/** Read a market's full state by key, through the lens (`getPools`; C version 7). All zero, `initialized` false, for a market nobody opened. */
|
|
43
50
|
export async function getPool(c: LogswapClient, key: PoolKey): Promise<PoolState> {
|
|
44
|
-
const v = await c.public.readContract({
|
|
45
|
-
address: c.addresses.
|
|
46
|
-
abi:
|
|
47
|
-
functionName: "
|
|
48
|
-
args: [poolId(key)],
|
|
49
|
-
});
|
|
50
|
-
return v as
|
|
51
|
+
const v = (await c.public.readContract({
|
|
52
|
+
address: c.addresses.lens,
|
|
53
|
+
abi: logswapLensAbi,
|
|
54
|
+
functionName: "getPools",
|
|
55
|
+
args: [[poolId(key)]],
|
|
56
|
+
})) as readonly unknown[];
|
|
57
|
+
return v[0] as PoolState;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
54
|
-
* The market's swap fee
|
|
55
|
-
* `
|
|
56
|
-
*
|
|
57
|
-
*
|
|
61
|
+
* The market's swap fee — the key's own `swapFee` since C version 7: a fee of 0 is no fee, and
|
|
62
|
+
* `DEFAULT_FEE` / the manager's `swapFeeOf` are gone (decisions 065). Constant for the market's
|
|
63
|
+
* life. Still a read: it refuses a key nobody initialized, which `discoverMarkets` relies on to
|
|
64
|
+
* drop a stale indexer's rows. (`swapFeeOf` in `fees.ts` is the fee one swap paid.)
|
|
58
65
|
*/
|
|
59
66
|
export async function marketSwapFee(c: LogswapClient, key: PoolKey): Promise<bigint> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
functionName: "swapFeeOf",
|
|
64
|
-
args: [key],
|
|
65
|
-
}) as Promise<bigint>;
|
|
67
|
+
const s = await getPool(c, key);
|
|
68
|
+
if (!s.initialized) throw new Error(`logswap: market ${poolId(key)} is not initialized`);
|
|
69
|
+
return key.swapFee;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
/**
|
|
@@ -159,7 +163,7 @@ export interface DiscoveredMarket {
|
|
|
159
163
|
*/
|
|
160
164
|
export async function discoverMarkets(
|
|
161
165
|
c: LogswapClient,
|
|
162
|
-
opts: { fromBlock?: bigint; toBlock?: bigint; base?: Address; quote?: Address } = {},
|
|
166
|
+
opts: { fromBlock?: bigint; toBlock?: bigint; base?: Address; quote?: Address; /** The rindexer GraphQL endpoint: `Initialize` from there, the node otherwise. */ indexer?: string } = {},
|
|
163
167
|
): Promise<DiscoveredMarket[]> {
|
|
164
168
|
// Narrow the const ABI to the Initialize entry while KEEPING its literal type, so viem can infer
|
|
165
169
|
// the log shape. `as never` would compile but collapse every arg to `never` at the call site.
|
|
@@ -167,28 +171,32 @@ export async function discoverMarkets(
|
|
|
167
171
|
const ev = cPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Initialize");
|
|
168
172
|
if (!ev) throw new Error("logswap: Initialize event missing from the generated ABI");
|
|
169
173
|
|
|
170
|
-
const
|
|
174
|
+
const args: Record<string, unknown> = {};
|
|
175
|
+
if (opts.base) args.base = opts.base;
|
|
176
|
+
if (opts.quote) args.quote = opts.quote;
|
|
177
|
+
const { logs } = await getEventLogs<{ base: Address; quote: Address; tickSpacing: bigint; swapFee: bigint; x0: bigint }>(c, {
|
|
178
|
+
indexer: opts.indexer,
|
|
171
179
|
address: c.addresses.cPoolManager,
|
|
172
180
|
event: ev,
|
|
173
|
-
args
|
|
181
|
+
args,
|
|
174
182
|
fromBlock: opts.fromBlock ?? 0n,
|
|
175
|
-
toBlock: opts.toBlock
|
|
183
|
+
toBlock: opts.toBlock,
|
|
176
184
|
});
|
|
177
185
|
|
|
178
186
|
return logs.map((l) => {
|
|
179
187
|
const a = l.args;
|
|
180
188
|
const key: PoolKey = {
|
|
181
|
-
base: a.base
|
|
182
|
-
quote: a.quote
|
|
183
|
-
tickSpacing: a.tickSpacing
|
|
184
|
-
swapFee: a.swapFee
|
|
189
|
+
base: a.base,
|
|
190
|
+
quote: a.quote,
|
|
191
|
+
tickSpacing: a.tickSpacing,
|
|
192
|
+
swapFee: a.swapFee,
|
|
185
193
|
};
|
|
186
194
|
return {
|
|
187
195
|
key,
|
|
188
196
|
poolId: poolId(key),
|
|
189
|
-
x0: a.x0
|
|
190
|
-
blockNumber: l.blockNumber
|
|
191
|
-
transactionHash: l.transactionHash
|
|
197
|
+
x0: a.x0,
|
|
198
|
+
blockNumber: l.blockNumber,
|
|
199
|
+
transactionHash: l.transactionHash,
|
|
192
200
|
};
|
|
193
201
|
});
|
|
194
202
|
}
|
|
@@ -417,16 +425,18 @@ export async function marketOpenedAt(c: LogswapClient, m: DiscoveredMarket): Pro
|
|
|
417
425
|
}
|
|
418
426
|
|
|
419
427
|
/**
|
|
420
|
-
* Create a C market: `initialize(key, x0)` on the manager — a call, not a deployment
|
|
421
|
-
*
|
|
422
|
-
*
|
|
428
|
+
* Create a C market: `initialize(key, x0, mintFee)` on the manager — a call, not a deployment, and
|
|
429
|
+
* no funds move. The mint fee is the creator's pick from the gate's menu (065), preset at twice the
|
|
430
|
+
* swap fee ({@link presetMintFee}). Under a curated gate the CALLER must be a listed creator; the
|
|
431
|
+
* canonical path is the router's {@link createMarket}, which forwards its user to the gate and
|
|
432
|
+
* mints the backstop in the same transaction (064).
|
|
423
433
|
*/
|
|
424
|
-
export async function initializeMarket(c: LogswapClient, a: { key: PoolKey; x0: bigint; account: Address }) {
|
|
434
|
+
export async function initializeMarket(c: LogswapClient, a: { key: PoolKey; x0: bigint; mintFee?: bigint; account: Address }) {
|
|
425
435
|
const { request } = await c.public.simulateContract({
|
|
426
436
|
address: c.addresses.cPoolManager,
|
|
427
437
|
abi: cPoolManagerAbi,
|
|
428
438
|
functionName: "initialize",
|
|
429
|
-
args: [a.key, a.x0],
|
|
439
|
+
args: [a.key, a.x0, a.mintFee ?? presetMintFee(a.key.swapFee)],
|
|
430
440
|
account: signerFor(c, a.account),
|
|
431
441
|
} as never);
|
|
432
442
|
return c.wallet!.writeContract(request as never);
|
package/src/positions.ts
CHANGED
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* accounts holding the same class can have different claimable amounts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { parseAbiItem } from "viem";
|
|
10
|
-
import type { Address } from "viem";
|
|
9
|
+
import { encodeAbiParameters, keccak256, parseAbiItem } from "viem";
|
|
10
|
+
import type { Address, Hex } from "viem";
|
|
11
11
|
import { logswapLensAbi, cPoolManagerAbi } from "./generated.js";
|
|
12
12
|
import type { LogswapClient } from "./client.js";
|
|
13
|
-
import { poolId, type PoolKey } from "./keys.js";
|
|
13
|
+
import { FEE_MAX, poolId, type PoolKey } from "./keys.js";
|
|
14
|
+
|
|
15
|
+
/** `CPoolState.POSITIONS_SLOT`: the manager's `positions` mapping (`forge inspect CPoolManager storageLayout`). */
|
|
16
|
+
const C_POSITIONS_SLOT = 4n;
|
|
17
|
+
const U128 = (1n << 128n) - 1n;
|
|
14
18
|
import { isPosition, NO_CAP, positionId, unpackFloorCap } from "./ids.js";
|
|
19
|
+
import { getEventLogs } from "./history.js";
|
|
15
20
|
|
|
16
21
|
/** A class's aggregate state — the whole class, not one holder's slice. */
|
|
17
22
|
export interface PositionClass {
|
|
@@ -25,14 +30,23 @@ export interface PositionClass {
|
|
|
25
30
|
insideLast: bigint;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A class's aggregate state, read through `extsload` (C version 7: the manager's public
|
|
35
|
+
* `positions` mapping is gone behind it, decisions 065). The slot is `CPoolState`'s:
|
|
36
|
+
* `keccak256(abi.encode(id, POSITIONS_SLOT = 4))`, two words — (L | shares << 128) and
|
|
37
|
+
* (aps | insideLast << 128). Pinned on chain by the integration suite.
|
|
38
|
+
*/
|
|
28
39
|
export async function getPositionClass(c: LogswapClient, id: bigint): Promise<PositionClass> {
|
|
29
|
-
const
|
|
40
|
+
const slot = keccak256(encodeAbiParameters([{ type: "uint256" }, { type: "uint256" }], [id, C_POSITIONS_SLOT]));
|
|
41
|
+
const w = (await c.public.readContract({
|
|
30
42
|
address: c.addresses.cPoolManager,
|
|
31
43
|
abi: cPoolManagerAbi,
|
|
32
|
-
functionName: "
|
|
33
|
-
args: [
|
|
34
|
-
})) as readonly [
|
|
35
|
-
|
|
44
|
+
functionName: "extsload",
|
|
45
|
+
args: [slot, 2n],
|
|
46
|
+
} as never)) as readonly Hex[];
|
|
47
|
+
const w0 = BigInt(w[0]!), w1 = BigInt(w[1]!);
|
|
48
|
+
const hi = w1 >> 128n;
|
|
49
|
+
return { L: w0 & U128, shares: w0 >> 128n, aps: w1 & U128, insideLast: hi >= 1n << 127n ? hi - (1n << 128n) : hi };
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/** A holder's share balance in a class. */
|
|
@@ -183,18 +197,27 @@ export function positionQuote(
|
|
|
183
197
|
return (p.L * (upper - p.floor)) / 10n ** 18n;
|
|
184
198
|
}
|
|
185
199
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The mint fee a create form presets: twice the swap fee, capped at {@link FEE_MAX} (decisions 064
|
|
202
|
+
* (f): guidance, not a contract rule). The creator picks from the gate's or the policy's menu
|
|
203
|
+
* (065); a listed entry is what the chain accepts — read it with `policyMintFeeMenu`.
|
|
204
|
+
*/
|
|
205
|
+
export function presetMintFee(swapFee: bigint): bigint {
|
|
206
|
+
const m = 2n * swapFee;
|
|
207
|
+
return m > FEE_MAX ? FEE_MAX : m;
|
|
208
|
+
}
|
|
188
209
|
|
|
189
210
|
/**
|
|
190
211
|
* The one-time mint fee at log-price `x`, in quote units — the amount `_mintCore` pulls ON TOP of
|
|
191
|
-
* the position's own legs, so a mint's total quote pull is `positionQuote(p, x) + mintFee(p, x)`.
|
|
212
|
+
* the position's own legs, so a mint's total quote pull is `positionQuote(p, x) + mintFee(p, x, rate)`.
|
|
213
|
+
* `rate` is the MARKET's mint fee (`getPool(...).mintFee`, WAD of L): per market since C version 7,
|
|
214
|
+
* the gate's answer at birth (065); there is no global `MINT_FEE` any more.
|
|
192
215
|
*
|
|
193
216
|
* Mirrors the manager's exemption cases: a DORMANT mint pays nothing — floor above spot, or a
|
|
194
|
-
* capped class spot has already passed (`cap <= x`). Anything earning pays `
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
217
|
+
* capped class spot has already passed (`cap <= x`). Anything earning pays `rate · L`, all of it
|
|
218
|
+
* the protocol's (decisions 058); with no collector it goes through `F` to the LPs already
|
|
219
|
+
* earning, and with none earning it is not charged. This is NOT the swap fee: that one is charged
|
|
220
|
+
* per unit of |Δ log p| on swaps; this is a flat charge on L, once, at mint.
|
|
198
221
|
*
|
|
199
222
|
* One rung of imprecision: for a floor or cap exactly AT spot the contract consults its `currentK`
|
|
200
223
|
* pointer (an up-move counts the tick, a down-move does not), which this offline mirror cannot see.
|
|
@@ -203,9 +226,10 @@ export const MINT_FEE = 2n * 10n ** 14n;
|
|
|
203
226
|
export function mintFee(
|
|
204
227
|
p: { L: bigint; floor: bigint; cap: bigint; capped: boolean },
|
|
205
228
|
x: bigint,
|
|
229
|
+
rate: bigint,
|
|
206
230
|
): bigint {
|
|
207
231
|
if (p.floor > x || (p.capped && p.cap <= x)) return 0n;
|
|
208
|
-
return (p.L *
|
|
232
|
+
return (p.L * rate) / 10n ** 18n;
|
|
209
233
|
}
|
|
210
234
|
|
|
211
235
|
/**
|
|
@@ -249,10 +273,10 @@ const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes
|
|
|
249
273
|
* holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
|
|
250
274
|
* a read per registered class; fine for a pool's lifetime of classes.
|
|
251
275
|
*/
|
|
252
|
-
export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n): Promise<HeldPositionRow[]> {
|
|
276
|
+
export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n, opts: { /** The rindexer GraphQL endpoint: the class registry from there, the node otherwise. */ indexer?: string } = {}): Promise<HeldPositionRow[]> {
|
|
253
277
|
const pool = poolId(key);
|
|
254
|
-
const logs = await c
|
|
255
|
-
const ids = logs.map((l) =>
|
|
278
|
+
const { logs } = await getEventLogs<{ id?: bigint }>(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock });
|
|
279
|
+
const ids = logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
|
|
256
280
|
if (ids.length === 0) return [];
|
|
257
281
|
// one lens call: balance, class L, fees, floor and cap for every id the owner holds
|
|
258
282
|
const rows = (await c.public.readContract({
|