@logswap/sdk 0.5.53 → 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.js +8 -7
- package/dist/indexer.d.ts +15 -0
- package/dist/indexer.js +57 -34
- package/dist/pools.d.ts +2 -0
- package/dist/pools.js +6 -14
- package/dist/write.d.ts +8 -0
- package/dist/write.js +14 -0
- package/package.json +1 -1
- package/src/fpool.ts +8 -7
- package/src/indexer.ts +63 -41
- package/src/pools.ts +8 -13
- package/src/write.ts +15 -0
package/dist/fpool.js
CHANGED
|
@@ -45,6 +45,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
45
45
|
* in for the base — a call through it to any of the four policies decodes the base's functions.
|
|
46
46
|
*/
|
|
47
47
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
48
|
+
import { signerFor } from "./write.js";
|
|
48
49
|
const WAD = 10n ** 18n;
|
|
49
50
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
50
51
|
/** The optional questions a policy declared at birth (decisions 044), as bits of `Pool.asks`. */
|
|
@@ -410,7 +411,7 @@ async function writeFPool(c, poolId, functionName, args, account) {
|
|
|
410
411
|
// one place all seven helpers share, is what keeps a caller from ever omitting it. The `as
|
|
411
412
|
// never` casts blind tsc to arg counts, so this file's encode test is the real guard.
|
|
412
413
|
args: [poolId, ...args],
|
|
413
|
-
account,
|
|
414
|
+
account: signerFor(c, account),
|
|
414
415
|
});
|
|
415
416
|
return wallet.writeContract(request);
|
|
416
417
|
}
|
|
@@ -422,7 +423,7 @@ async function writeFPool0(c, functionName, args, account) {
|
|
|
422
423
|
abi: fPoolManagerAbi,
|
|
423
424
|
functionName,
|
|
424
425
|
args,
|
|
425
|
-
account,
|
|
426
|
+
account: signerFor(c, account),
|
|
426
427
|
});
|
|
427
428
|
return wallet.writeContract(request);
|
|
428
429
|
}
|
|
@@ -447,7 +448,7 @@ pullTokens = []) {
|
|
|
447
448
|
abi: logswapRouterAbi,
|
|
448
449
|
functionName: fn,
|
|
449
450
|
args: sendArgs,
|
|
450
|
-
account,
|
|
451
|
+
account: signerFor(c, account),
|
|
451
452
|
});
|
|
452
453
|
return wallet.writeContract(request);
|
|
453
454
|
}
|
|
@@ -502,7 +503,7 @@ export async function approveRouterForFPool(c, account) {
|
|
|
502
503
|
abi: fPoolManagerAbi,
|
|
503
504
|
functionName: "setOperator",
|
|
504
505
|
args: [c.addresses.router, true],
|
|
505
|
-
account,
|
|
506
|
+
account: signerFor(c, account),
|
|
506
507
|
});
|
|
507
508
|
return wallet.writeContract(request);
|
|
508
509
|
}
|
|
@@ -900,7 +901,7 @@ async function writePolicy(c, abi, a, functionName, args) {
|
|
|
900
901
|
abi,
|
|
901
902
|
functionName,
|
|
902
903
|
args: [a.poolId, ...args],
|
|
903
|
-
account: a.account,
|
|
904
|
+
account: signerFor(c, a.account),
|
|
904
905
|
});
|
|
905
906
|
return wallet.writeContract(request);
|
|
906
907
|
}
|
|
@@ -971,7 +972,7 @@ export async function fPoolSetSponsorName(c, a) {
|
|
|
971
972
|
abi: policyRegistryAbi,
|
|
972
973
|
functionName: "setSponsorName",
|
|
973
974
|
args: [a.who, textToBytes32(a.name)],
|
|
974
|
-
account: a.account,
|
|
975
|
+
account: signerFor(c, a.account),
|
|
975
976
|
});
|
|
976
977
|
return wallet.writeContract(request);
|
|
977
978
|
}
|
|
@@ -1035,7 +1036,7 @@ export async function fPoolTransferShares(c, a) {
|
|
|
1035
1036
|
abi: fPoolManagerAbi,
|
|
1036
1037
|
functionName: "transfer",
|
|
1037
1038
|
args: [a.to, fPoolShareId(a.poolId, gen), a.shares],
|
|
1038
|
-
account: a.account,
|
|
1039
|
+
account: signerFor(c, a.account),
|
|
1039
1040
|
});
|
|
1040
1041
|
return wallet.writeContract(request);
|
|
1041
1042
|
}
|
package/dist/indexer.d.ts
CHANGED
|
@@ -25,7 +25,22 @@ export interface IndexerError {
|
|
|
25
25
|
* `Syntax Error: Invalid character escape sequence`. Variables carry the value untouched.
|
|
26
26
|
*/
|
|
27
27
|
export declare function bytea(hex: Hex): string;
|
|
28
|
+
/** How long an indexer call may take before the caller falls back (to the chain, or an empty view). */
|
|
29
|
+
export declare const INDEXER_TIMEOUT_MS = 8000;
|
|
28
30
|
export declare function gql<T>(url: string, query: string, variables?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
|
|
31
|
+
/**
|
|
32
|
+
* Every row of a connection, page by page. A single `first: 1000` silently drops row 1001 onward —
|
|
33
|
+
* a pool's 1001st position class, a deployment's 1001st market — with nothing to say so. Pages by
|
|
34
|
+
* cursor in primary-key order, so rows inserted while paging cannot shift the window.
|
|
35
|
+
*/
|
|
36
|
+
export declare function gqlAll<N>(url: string, field: string, selection: string, opts?: {
|
|
37
|
+
vars?: Record<string, {
|
|
38
|
+
type: string;
|
|
39
|
+
value: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
condition?: string;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}): Promise<N[]>;
|
|
29
44
|
/** A position class as the chain registered it: the floor/cap geometry, fixed for the id's life. */
|
|
30
45
|
export interface RegisteredId {
|
|
31
46
|
id: bigint;
|
package/dist/indexer.js
CHANGED
|
@@ -25,15 +25,24 @@ import { NO_CAP } from "./ids.js";
|
|
|
25
25
|
export function bytea(hex) {
|
|
26
26
|
return `\\x${hex.replace(/^0x/, "")}`;
|
|
27
27
|
}
|
|
28
|
+
/** How long an indexer call may take before the caller falls back (to the chain, or an empty view). */
|
|
29
|
+
export const INDEXER_TIMEOUT_MS = 8_000;
|
|
28
30
|
export async function gql(url, query, variables, signal) {
|
|
31
|
+
// A hung indexer must not hang its caller: every consumer has a fallback, and it only runs when
|
|
32
|
+
// this call ends. The caller's signal, if any, still wins.
|
|
29
33
|
const res = await fetch(url, {
|
|
30
34
|
method: "POST",
|
|
31
35
|
headers: { "content-type": "application/json" },
|
|
32
36
|
body: JSON.stringify({ query, variables }),
|
|
33
|
-
signal,
|
|
37
|
+
signal: signal ?? AbortSignal.timeout(INDEXER_TIMEOUT_MS),
|
|
34
38
|
});
|
|
35
39
|
if (!res.ok)
|
|
36
40
|
throw new Error(`indexer: HTTP ${res.status} from ${url}`);
|
|
41
|
+
// an Access login page or a proxy's error page answers 200 with HTML; say so, rather than
|
|
42
|
+
// "Unexpected token <"
|
|
43
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
44
|
+
if (!ct.includes("json"))
|
|
45
|
+
throw new Error(`indexer: ${url} answered ${ct || "no content-type"}, not JSON (a login page?)`);
|
|
37
46
|
const body = (await res.json());
|
|
38
47
|
if (body.errors?.length)
|
|
39
48
|
throw new Error(`indexer: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
@@ -41,13 +50,43 @@ export async function gql(url, query, variables, signal) {
|
|
|
41
50
|
throw new Error("indexer: empty response");
|
|
42
51
|
return body.data;
|
|
43
52
|
}
|
|
53
|
+
const PAGE = 1000;
|
|
54
|
+
const MAX_PAGES = 100;
|
|
55
|
+
/**
|
|
56
|
+
* Every row of a connection, page by page. A single `first: 1000` silently drops row 1001 onward —
|
|
57
|
+
* a pool's 1001st position class, a deployment's 1001st market — with nothing to say so. Pages by
|
|
58
|
+
* cursor in primary-key order, so rows inserted while paging cannot shift the window.
|
|
59
|
+
*/
|
|
60
|
+
export async function gqlAll(url, field, selection, opts = {}) {
|
|
61
|
+
const vars = opts.vars ?? {};
|
|
62
|
+
const decl = Object.entries(vars).map(([k, v]) => `, $${k}: ${v.type}`).join("");
|
|
63
|
+
const cond = opts.condition ? `, condition: ${opts.condition}` : "";
|
|
64
|
+
const q = `query Page($after: Cursor${decl}) {
|
|
65
|
+
${field}(first: ${PAGE}, after: $after, orderBy: PRIMARY_KEY_ASC${cond}) {
|
|
66
|
+
pageInfo { hasNextPage endCursor } nodes { ${selection} }
|
|
67
|
+
}
|
|
68
|
+
}`;
|
|
69
|
+
const values = Object.fromEntries(Object.entries(vars).map(([k, v]) => [k, v.value]));
|
|
70
|
+
const out = [];
|
|
71
|
+
let after = null;
|
|
72
|
+
for (let i = 0; i < MAX_PAGES; i++) {
|
|
73
|
+
const d = await gql(url, q, { ...values, after }, opts.signal);
|
|
74
|
+
const page = d[field];
|
|
75
|
+
out.push(...page.nodes);
|
|
76
|
+
if (!page.pageInfo.hasNextPage || !page.pageInfo.endCursor)
|
|
77
|
+
return out;
|
|
78
|
+
after = page.pageInfo.endCursor;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`indexer: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
81
|
+
}
|
|
44
82
|
/** Every position class ever registered in a pool. Historical — some may since have burnt to zero. */
|
|
45
83
|
export async function registeredIds(url, poolId, signal) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
const nodes = await gqlAll(url, "allIdRegistereds", "id floor cap capped", {
|
|
85
|
+
vars: { pool: { type: "String!", value: bytea(poolId) } },
|
|
86
|
+
condition: "{poolId: $pool}",
|
|
87
|
+
signal,
|
|
88
|
+
});
|
|
89
|
+
return nodes.map((n) => ({
|
|
51
90
|
id: BigInt(n.id),
|
|
52
91
|
floor: BigInt(n.floor),
|
|
53
92
|
cap: BigInt(n.cap),
|
|
@@ -99,11 +138,8 @@ export async function indexerHealthy(url, signal) {
|
|
|
99
138
|
* everywhere else, so a wrong id fails loudly instead of addressing the wrong pool.
|
|
100
139
|
*/
|
|
101
140
|
export async function discoverMarketsIndexed(url, signal) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
} } }`;
|
|
105
|
-
const d = await gql(url, q, undefined, signal);
|
|
106
|
-
return d.allInitializes.nodes.map((n) => {
|
|
141
|
+
const nodes = await gqlAll(url, "allInitializes", "poolId base quote tickSpacing phi x0 blockNumber txHash contractAddress", { signal });
|
|
142
|
+
return nodes.map((n) => {
|
|
107
143
|
const key = {
|
|
108
144
|
base: n.base,
|
|
109
145
|
quote: n.quote,
|
|
@@ -116,6 +152,7 @@ export async function discoverMarketsIndexed(url, signal) {
|
|
|
116
152
|
x0: BigInt(n.x0),
|
|
117
153
|
blockNumber: BigInt(n.blockNumber),
|
|
118
154
|
transactionHash: n.txHash,
|
|
155
|
+
manager: n.contractAddress,
|
|
119
156
|
};
|
|
120
157
|
});
|
|
121
158
|
}
|
|
@@ -129,29 +166,15 @@ export async function discoverMarketsBest(c, url, signal) {
|
|
|
129
166
|
if (url) {
|
|
130
167
|
try {
|
|
131
168
|
const markets = await discoverMarketsIndexed(url, signal);
|
|
132
|
-
// The indexer is a CACHE and the chain is the truth. After a redeploy
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
address: c.addresses.cPoolManager,
|
|
142
|
-
abi: cPoolManagerAbi,
|
|
143
|
-
functionName: "phiOf",
|
|
144
|
-
args: [m.key],
|
|
145
|
-
});
|
|
146
|
-
return m;
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
}))).filter((m) => m !== null);
|
|
152
|
-
if (alive.length)
|
|
153
|
-
return { markets: alive, source: "indexer" };
|
|
154
|
-
}
|
|
169
|
+
// The indexer is a CACHE and the chain is the truth. After a redeploy it keeps serving the
|
|
170
|
+
// previous deployment's markets, and calling the new manager with those keys makes every
|
|
171
|
+
// read revert, refresh-proof. Each row names the manager that emitted it, so keep only the
|
|
172
|
+
// rows of the manager this client is pointed at — no read per row. None left means the
|
|
173
|
+
// indexer has not seen this deployment: the chain scan is the answer.
|
|
174
|
+
const mine = c.addresses.cPoolManager.toLowerCase();
|
|
175
|
+
const alive = markets.filter((m) => m.manager?.toLowerCase() === mine);
|
|
176
|
+
if (alive.length)
|
|
177
|
+
return { markets: alive, source: "indexer" };
|
|
155
178
|
}
|
|
156
179
|
catch {
|
|
157
180
|
/* fall through — a missing indexer must not cost you the market list */
|
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) {
|
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;
|
|
@@ -644,7 +645,7 @@ async function writeFPool(c: LogswapClient, poolId: Hex, functionName: string, a
|
|
|
644
645
|
// one place all seven helpers share, is what keeps a caller from ever omitting it. The `as
|
|
645
646
|
// never` casts blind tsc to arg counts, so this file's encode test is the real guard.
|
|
646
647
|
args: [poolId, ...args],
|
|
647
|
-
account,
|
|
648
|
+
account: signerFor(c, account),
|
|
648
649
|
} as never);
|
|
649
650
|
return wallet.writeContract(request as never);
|
|
650
651
|
}
|
|
@@ -657,7 +658,7 @@ async function writeFPool0(c: LogswapClient, functionName: string, args: unknown
|
|
|
657
658
|
abi: fPoolManagerAbi,
|
|
658
659
|
functionName,
|
|
659
660
|
args,
|
|
660
|
-
account,
|
|
661
|
+
account: signerFor(c, account),
|
|
661
662
|
} as never);
|
|
662
663
|
return wallet.writeContract(request as never);
|
|
663
664
|
}
|
|
@@ -688,7 +689,7 @@ async function writeRouter(
|
|
|
688
689
|
abi: logswapRouterAbi,
|
|
689
690
|
functionName: fn,
|
|
690
691
|
args: sendArgs,
|
|
691
|
-
account,
|
|
692
|
+
account: signerFor(c, account),
|
|
692
693
|
} as never);
|
|
693
694
|
return wallet.writeContract(request as never);
|
|
694
695
|
}
|
|
@@ -749,7 +750,7 @@ export async function approveRouterForFPool(c: LogswapClient, account: Address)
|
|
|
749
750
|
abi: fPoolManagerAbi,
|
|
750
751
|
functionName: "setOperator",
|
|
751
752
|
args: [c.addresses.router, true],
|
|
752
|
-
account,
|
|
753
|
+
account: signerFor(c, account),
|
|
753
754
|
} as never);
|
|
754
755
|
return wallet.writeContract(request as never);
|
|
755
756
|
}
|
|
@@ -1269,7 +1270,7 @@ async function writePolicy(c: LogswapClient, abi: readonly unknown[], a: { poolI
|
|
|
1269
1270
|
abi,
|
|
1270
1271
|
functionName,
|
|
1271
1272
|
args: [a.poolId, ...args],
|
|
1272
|
-
account: a.account,
|
|
1273
|
+
account: signerFor(c, a.account),
|
|
1273
1274
|
} as never);
|
|
1274
1275
|
return wallet.writeContract(request as never);
|
|
1275
1276
|
}
|
|
@@ -1361,7 +1362,7 @@ export async function fPoolSetSponsorName(c: LogswapClient, a: { who: Address; n
|
|
|
1361
1362
|
abi: policyRegistryAbi,
|
|
1362
1363
|
functionName: "setSponsorName",
|
|
1363
1364
|
args: [a.who, textToBytes32(a.name)],
|
|
1364
|
-
account: a.account,
|
|
1365
|
+
account: signerFor(c, a.account),
|
|
1365
1366
|
} as never);
|
|
1366
1367
|
return wallet.writeContract(request as never);
|
|
1367
1368
|
}
|
|
@@ -1436,7 +1437,7 @@ export async function fPoolTransferShares(
|
|
|
1436
1437
|
abi: fPoolManagerAbi,
|
|
1437
1438
|
functionName: "transfer",
|
|
1438
1439
|
args: [a.to, fPoolShareId(a.poolId, gen), a.shares],
|
|
1439
|
-
account: a.account,
|
|
1440
|
+
account: signerFor(c, a.account),
|
|
1440
1441
|
} as never);
|
|
1441
1442
|
return wallet.writeContract(request as never);
|
|
1442
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
|
}
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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) {
|