@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 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 q = `query Ids($pool: String!) {
47
- allIdRegistereds(condition: {poolId: $pool}, first: 1000) { nodes { id floor cap capped } }
48
- }`;
49
- const d = await gql(url, q, { pool: bytea(poolId) }, signal);
50
- return d.allIdRegistereds.nodes.map((n) => ({
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 q = `{ allInitializes(first: 1000) { nodes {
103
- poolId base quote tickSpacing phi x0 blockNumber txHash
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 the indexer keeps
133
- // serving the previous deployment's markets; calling the new manager with those keys makes
134
- // every read revert, refresh-proof (it presented as "phiEff reverted", forever). So verify
135
- // each candidate against the manager the client is actually pointed at, and keep only the
136
- // markets that exist there; if none survive, the chain scan is the answer.
137
- if (markets.length) {
138
- const alive = (await Promise.all(markets.map(async (m) => {
139
- try {
140
- await c.public.readContract({
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
- const abi = [
141
- { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
142
- ];
143
- const read = async (token) => {
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.53",
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 q = `query Ids($pool: String!) {
67
- allIdRegistereds(condition: {poolId: $pool}, first: 1000) { nodes { id floor cap capped } }
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 d.allIdRegistereds.nodes.map((n) => ({
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 d = await gql<{ allInitializes: { nodes: Row[] } }>(url, q, undefined, signal);
153
- return d.allInitializes.nodes.map((n) => {
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 the indexer keeps
185
- // serving the previous deployment's markets; calling the new manager with those keys makes
186
- // every read revert, refresh-proof (it presented as "phiEff reverted", forever). So verify
187
- // each candidate against the manager the client is actually pointed at, and keep only the
188
- // markets that exist there; if none survive, the chain scan is the answer.
189
- if (markets.length) {
190
- const alive = (
191
- await Promise.all(
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
- const abi = [
192
- { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
193
- ] as const;
194
- const read = async (token: Address): Promise<number> => {
195
- try {
196
- return Number(await c.public.readContract({ address: token, abi, functionName: "decimals" }));
197
- } catch {
198
- return 18; // same fallback the manager uses when the token does not implement it
199
- }
200
- };
201
- const [base, quote] = await Promise.all([read(key.base), read(key.quote)]);
202
- return { base, quote };
194
+ // It used to fall back to 18 on ANY failure, a dropped request included — a 6-decimal quote read
195
+ // as 18 is every amount 10^12 off. `marketTokens` retries and then refuses; so does this.
196
+ const { base, quote } = await marketTokens(c, key);
197
+ return { base: base.decimals, quote: quote.decimals };
203
198
  }
204
199
 
205
200
  /** A token as a UI needs it: address, symbol and decimals, all read from the chain. */
@@ -391,7 +386,7 @@ export async function initializeMarket(c: LogswapClient, a: { key: PoolKey; x0:
391
386
  abi: cPoolManagerAbi,
392
387
  functionName: "initialize",
393
388
  args: [a.key, a.x0],
394
- account: a.account,
389
+ account: signerFor(c, a.account),
395
390
  } as never);
396
391
  return c.wallet!.writeContract(request as never);
397
392
  }
package/src/write.ts CHANGED
@@ -46,6 +46,21 @@ export function resolveDeadline(o: WriteOptions | undefined, now = Date.now()):
46
46
  * until someone uses a key the node does not have — a bot, a script, CI, or a real user's wallet.
47
47
  * Passing the object lets viem pick the correct path for both kinds.
48
48
  */
49
+ /**
50
+ * The wallet's account OBJECT for a write the caller named by address — see {@link requireWallet}
51
+ * for why the object and never the address. The F helpers take `account: Address` in their
52
+ * arguments; passed straight to viem it became a JSON-RPC account and the NODE was asked to sign,
53
+ * which works only on a dev node holding the key (every agent on the devnet did exactly that).
54
+ * A named account that is not the wallet's is refused: nothing else could sign for it.
55
+ */
56
+ export function signerFor(c: LogswapClient, address: Address): NonNullable<LogswapClient["wallet"]>["account"] {
57
+ const { account } = requireWallet(c);
58
+ if (account!.address.toLowerCase() !== address.toLowerCase()) {
59
+ throw new Error(`logswap: the write names ${address} but the wallet signs as ${account!.address}`);
60
+ }
61
+ return account;
62
+ }
63
+
49
64
  export function requireWallet(c: LogswapClient): { account: NonNullable<LogswapClient["wallet"]>["account"]; address: Address } {
50
65
  const account = c.wallet?.account;
51
66
  if (!c.wallet || !account) {