@logswap/sdk 0.5.62 → 0.5.64
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/client.js +16 -19
- package/dist/fpool.d.ts +3 -1
- package/dist/fpool.js +8 -12
- package/dist/history.js +22 -14
- package/package.json +1 -1
- package/src/client.ts +17 -21
- package/src/fpool.ts +9 -13
- package/src/history.ts +23 -19
package/dist/client.js
CHANGED
|
@@ -123,30 +123,27 @@ export const EXPECTED_VERSION = 5n;
|
|
|
123
123
|
// `isTaker`; a pad and a POL hold one base (`OneBase`).
|
|
124
124
|
export const EXPECTED_F_VERSION = 19n;
|
|
125
125
|
export async function assertVersion(c, expected = EXPECTED_VERSION) {
|
|
126
|
-
const { cPoolManagerAbi } = await import("./generated.js");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
abi: cPoolManagerAbi,
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
const { cPoolManagerAbi, fPoolManagerAbi } = await import("./generated.js");
|
|
127
|
+
// three independent reads: one round trip, not three in a row
|
|
128
|
+
const [got, gotF, wiring] = await Promise.all([
|
|
129
|
+
c.public.readContract({ address: c.addresses.cPoolManager, abi: cPoolManagerAbi, functionName: "version" }),
|
|
130
|
+
// the F manager carries its own tag; assert it the same way when deployed
|
|
131
|
+
c.addresses.fPoolManager
|
|
132
|
+
? c.public.readContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName: "version" })
|
|
133
|
+
: Promise.resolve(null),
|
|
134
|
+
assertWiring(c).then(() => null, (e) => e),
|
|
135
|
+
]);
|
|
132
136
|
if (got !== expected) {
|
|
133
137
|
throw new Error(`logswap: manager at ${c.addresses.cPoolManager} reports version ${got}, but this SDK was built ` +
|
|
134
138
|
`against version ${expected}. Upgrade the SDK or point at the matching deployment.`);
|
|
135
139
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const gotF = await c.public.readContract({
|
|
140
|
-
address: c.addresses.fPoolManager,
|
|
141
|
-
abi: fPoolManagerAbi,
|
|
142
|
-
functionName: "version",
|
|
143
|
-
});
|
|
144
|
-
if (gotF < EXPECTED_F_VERSION) {
|
|
145
|
-
throw new Error(`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
146
|
-
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`);
|
|
147
|
-
}
|
|
140
|
+
if (gotF !== null && gotF < EXPECTED_F_VERSION) {
|
|
141
|
+
throw new Error(`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
142
|
+
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`);
|
|
148
143
|
}
|
|
149
|
-
|
|
144
|
+
// the wiring's error is the less specific one, so a version mismatch is reported first
|
|
145
|
+
if (wiring)
|
|
146
|
+
throw wiring;
|
|
150
147
|
}
|
|
151
148
|
/**
|
|
152
149
|
* The versions say the managers are the right code; this says the rest of the addresses belong
|
package/dist/fpool.d.ts
CHANGED
|
@@ -1433,11 +1433,13 @@ export interface DiscoveredFPool {
|
|
|
1433
1433
|
* Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
|
|
1434
1434
|
* policy, cached. Possible only because the event carries the full key preimage (bases,
|
|
1435
1435
|
* weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
|
|
1436
|
-
* exists for exactly this call.
|
|
1436
|
+
* exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
|
|
1437
|
+
* a scan from block 0 per call is what made the first load wait on the node.
|
|
1437
1438
|
*/
|
|
1438
1439
|
export declare function discoverFPools(c: LogswapClient, opts?: {
|
|
1439
1440
|
fromBlock?: bigint;
|
|
1440
1441
|
toBlock?: bigint;
|
|
1442
|
+
indexer?: string;
|
|
1441
1443
|
}): Promise<DiscoveredFPool[]>;
|
|
1442
1444
|
/** A pool an authority controls, with what the policy page shows of it. */
|
|
1443
1445
|
export interface FPoolControlled {
|
package/dist/fpool.js
CHANGED
|
@@ -46,6 +46,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
46
46
|
*/
|
|
47
47
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
48
48
|
import { fallBackUnlessRejected, signerFor } from "./write.js";
|
|
49
|
+
import { getEventLogs } from "./history.js";
|
|
49
50
|
const WAD = 10n ** 18n;
|
|
50
51
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
51
52
|
/** The optional questions a policy declared at birth, as bits of `Pool.asks`. */
|
|
@@ -1044,23 +1045,18 @@ export async function fPoolTransferShares(c, a) {
|
|
|
1044
1045
|
* Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
|
|
1045
1046
|
* policy, cached. Possible only because the event carries the full key preimage (bases,
|
|
1046
1047
|
* weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
|
|
1047
|
-
* exists for exactly this call.
|
|
1048
|
+
* exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
|
|
1049
|
+
* a scan from block 0 per call is what made the first load wait on the node.
|
|
1048
1050
|
*/
|
|
1049
1051
|
export async function discoverFPools(c, opts = {}) {
|
|
1050
1052
|
const ev = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "FInitialize");
|
|
1051
1053
|
if (!ev)
|
|
1052
1054
|
throw new Error("logswap: F Initialize event missing from the generated ABI");
|
|
1053
1055
|
const seedEv = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "Seeded");
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
fromBlock: opts.fromBlock ?? 0n,
|
|
1059
|
-
toBlock: opts.toBlock ?? "latest",
|
|
1060
|
-
}),
|
|
1061
|
-
seedEv
|
|
1062
|
-
? c.public.getLogs({ address: c.addresses.fPoolManager, event: seedEv, fromBlock: opts.fromBlock ?? 0n, toBlock: opts.toBlock ?? "latest" })
|
|
1063
|
-
: Promise.resolve([]),
|
|
1056
|
+
const range = { indexer: opts.indexer, address: c.addresses.fPoolManager, fromBlock: opts.fromBlock, toBlock: opts.toBlock };
|
|
1057
|
+
const [{ logs }, { logs: seeds }] = await Promise.all([
|
|
1058
|
+
getEventLogs(c, { ...range, event: ev }),
|
|
1059
|
+
seedEv ? getEventLogs(c, { ...range, event: seedEv }) : Promise.resolve({ logs: [] }),
|
|
1064
1060
|
]);
|
|
1065
1061
|
const q0Of = new Map();
|
|
1066
1062
|
for (const l of seeds) {
|
|
@@ -1080,7 +1076,7 @@ export async function discoverFPools(c, opts = {}) {
|
|
|
1080
1076
|
kind,
|
|
1081
1077
|
policy: a.policy,
|
|
1082
1078
|
creator: a.creator,
|
|
1083
|
-
block: l.blockNumber
|
|
1079
|
+
block: l.blockNumber,
|
|
1084
1080
|
seeded: q0 !== undefined,
|
|
1085
1081
|
q0: q0 ?? 0n,
|
|
1086
1082
|
shape: kind === "pad" ? "launch" : "basket", // the policy's word, not n = 1: a mono POL is a pool
|
package/dist/history.js
CHANGED
|
@@ -66,8 +66,10 @@ function decode(type, v) {
|
|
|
66
66
|
return getAddress(v);
|
|
67
67
|
return v; // bool, bytes32 (hex string), string
|
|
68
68
|
}
|
|
69
|
-
const PAGE = 1000;
|
|
69
|
+
const PAGE = 1000; // postgraphile's cap
|
|
70
70
|
const MAX_PAGES = 200;
|
|
71
|
+
/** Pages in flight at once, after the first. */
|
|
72
|
+
const PARALLEL_PAGES = 8;
|
|
71
73
|
async function fromIndexer(c, q) {
|
|
72
74
|
const { field, type } = tableFor(c, q.address, q.event.name);
|
|
73
75
|
const inputs = q.event.inputs;
|
|
@@ -106,24 +108,30 @@ async function fromIndexer(c, q) {
|
|
|
106
108
|
// postgraphile refuses a null or an empty filter, so each argument appears only when it has content
|
|
107
109
|
const hasCondition = Object.keys(condition).length > 0;
|
|
108
110
|
const hasFilter = Object.keys(filter).length > 0;
|
|
109
|
-
const decl = `$
|
|
110
|
-
const argsOf = `first: ${pageSize},
|
|
111
|
+
const decl = `$offset: Int${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
|
|
112
|
+
const argsOf = `first: ${pageSize}, offset: $offset, orderBy: ${order}${hasCondition ? ", condition: $condition" : ""}${hasFilter ? ", filter: $filter" : ""}`;
|
|
111
113
|
const query = `query History(${decl}) {
|
|
112
114
|
${field}(${argsOf}) {
|
|
113
|
-
|
|
115
|
+
totalCount nodes { ${selection} }
|
|
114
116
|
}
|
|
115
117
|
}`;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const page = async (offset) => (await gql(q.indexer, query, { offset, ...(hasCondition ? { condition } : {}), ...(hasFilter ? { filter } : {}) }, q.signal))[field];
|
|
119
|
+
// The first page says how many rows there are; the rest are fetched in parallel by offset, not
|
|
120
|
+
// one cursor after another: a day of a busy pool is a few pages, and a round trip to a distant
|
|
121
|
+
// indexer costs more than the page. Ascending by key, rows that land meanwhile append at the end
|
|
122
|
+
// and shift no earlier offset.
|
|
123
|
+
const first = await page(0);
|
|
124
|
+
const rows = [...first.nodes];
|
|
125
|
+
if (q.latest === undefined && first.totalCount > first.nodes.length) {
|
|
126
|
+
const pages = Math.ceil(first.totalCount / PAGE);
|
|
127
|
+
if (pages > MAX_PAGES)
|
|
120
128
|
throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
const offsets = Array.from({ length: pages - 1 }, (_, i) => (i + 1) * PAGE);
|
|
130
|
+
for (let i = 0; i < offsets.length; i += PARALLEL_PAGES) {
|
|
131
|
+
const got = await Promise.all(offsets.slice(i, i + PARALLEL_PAGES).map(page));
|
|
132
|
+
for (const g of got)
|
|
133
|
+
rows.push(...g.nodes);
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
129
137
|
const want = q.address.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logswap/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.64",
|
|
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/client.ts
CHANGED
|
@@ -168,34 +168,30 @@ export const EXPECTED_VERSION = 5n;
|
|
|
168
168
|
export const EXPECTED_F_VERSION = 19n;
|
|
169
169
|
|
|
170
170
|
export async function assertVersion(c: LogswapClient, expected = EXPECTED_VERSION): Promise<void> {
|
|
171
|
-
const { cPoolManagerAbi } = await import("./generated.js");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
abi: cPoolManagerAbi,
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
const { cPoolManagerAbi, fPoolManagerAbi } = await import("./generated.js");
|
|
172
|
+
// three independent reads: one round trip, not three in a row
|
|
173
|
+
const [got, gotF, wiring] = await Promise.all([
|
|
174
|
+
c.public.readContract({ address: c.addresses.cPoolManager, abi: cPoolManagerAbi, functionName: "version" }),
|
|
175
|
+
// the F manager carries its own tag; assert it the same way when deployed
|
|
176
|
+
c.addresses.fPoolManager
|
|
177
|
+
? c.public.readContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName: "version" })
|
|
178
|
+
: Promise.resolve(null),
|
|
179
|
+
assertWiring(c).then(() => null, (e: unknown) => e),
|
|
180
|
+
]);
|
|
177
181
|
if (got !== expected) {
|
|
178
182
|
throw new Error(
|
|
179
183
|
`logswap: manager at ${c.addresses.cPoolManager} reports version ${got}, but this SDK was built ` +
|
|
180
184
|
`against version ${expected}. Upgrade the SDK or point at the matching deployment.`,
|
|
181
185
|
);
|
|
182
186
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
abi: fPoolManagerAbi,
|
|
189
|
-
functionName: "version",
|
|
190
|
-
});
|
|
191
|
-
if (gotF < EXPECTED_F_VERSION) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
194
|
-
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
187
|
+
if (gotF !== null && gotF < EXPECTED_F_VERSION) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
190
|
+
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`,
|
|
191
|
+
);
|
|
197
192
|
}
|
|
198
|
-
|
|
193
|
+
// the wiring's error is the less specific one, so a version mismatch is reported first
|
|
194
|
+
if (wiring) throw wiring;
|
|
199
195
|
}
|
|
200
196
|
|
|
201
197
|
/**
|
package/src/fpool.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
49
49
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
50
50
|
import type { LogswapClient } from "./client.js";
|
|
51
51
|
import { fallBackUnlessRejected, signerFor } from "./write.js";
|
|
52
|
+
import { getEventLogs } from "./history.js";
|
|
52
53
|
|
|
53
54
|
const WAD = 10n ** 18n;
|
|
54
55
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
|
|
@@ -1474,27 +1475,22 @@ export interface DiscoveredFPool {
|
|
|
1474
1475
|
* Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
|
|
1475
1476
|
* policy, cached. Possible only because the event carries the full key preimage (bases,
|
|
1476
1477
|
* weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
|
|
1477
|
-
* exists for exactly this call.
|
|
1478
|
+
* exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
|
|
1479
|
+
* a scan from block 0 per call is what made the first load wait on the node.
|
|
1478
1480
|
*/
|
|
1479
1481
|
export async function discoverFPools(
|
|
1480
1482
|
c: LogswapClient,
|
|
1481
|
-
opts: { fromBlock?: bigint; toBlock?: bigint } = {},
|
|
1483
|
+
opts: { fromBlock?: bigint; toBlock?: bigint; indexer?: string } = {},
|
|
1482
1484
|
): Promise<DiscoveredFPool[]> {
|
|
1483
1485
|
type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "FInitialize" }>;
|
|
1484
1486
|
const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "FInitialize");
|
|
1485
1487
|
if (!ev) throw new Error("logswap: F Initialize event missing from the generated ABI");
|
|
1486
1488
|
type SeedEv = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "Seeded" }>;
|
|
1487
1489
|
const seedEv = fPoolManagerAbi.find((x): x is SeedEv => x.type === "event" && x.name === "Seeded");
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
fromBlock: opts.fromBlock ?? 0n,
|
|
1493
|
-
toBlock: opts.toBlock ?? "latest",
|
|
1494
|
-
}),
|
|
1495
|
-
seedEv
|
|
1496
|
-
? c.public.getLogs({ address: c.addresses.fPoolManager!, event: seedEv, fromBlock: opts.fromBlock ?? 0n, toBlock: opts.toBlock ?? "latest" })
|
|
1497
|
-
: Promise.resolve([]),
|
|
1490
|
+
const range = { indexer: opts.indexer, address: c.addresses.fPoolManager!, fromBlock: opts.fromBlock, toBlock: opts.toBlock };
|
|
1491
|
+
const [{ logs }, { logs: seeds }] = await Promise.all([
|
|
1492
|
+
getEventLogs(c, { ...range, event: ev }),
|
|
1493
|
+
seedEv ? getEventLogs(c, { ...range, event: seedEv }) : Promise.resolve({ logs: [] }),
|
|
1498
1494
|
]);
|
|
1499
1495
|
const q0Of = new Map<string, bigint>();
|
|
1500
1496
|
for (const l of seeds) {
|
|
@@ -1518,7 +1514,7 @@ export async function discoverFPools(
|
|
|
1518
1514
|
kind,
|
|
1519
1515
|
policy: a.policy,
|
|
1520
1516
|
creator: a.creator,
|
|
1521
|
-
block: l.blockNumber
|
|
1517
|
+
block: l.blockNumber,
|
|
1522
1518
|
seeded: q0 !== undefined,
|
|
1523
1519
|
q0: q0 ?? 0n,
|
|
1524
1520
|
shape: kind === "pad" ? "launch": "basket", // the policy's word, not n = 1: a mono POL is a pool
|
package/src/history.ts
CHANGED
|
@@ -101,8 +101,10 @@ function decode(type: string, v: unknown): unknown {
|
|
|
101
101
|
return v; // bool, bytes32 (hex string), string
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const PAGE = 1000;
|
|
104
|
+
const PAGE = 1000; // postgraphile's cap
|
|
105
105
|
const MAX_PAGES = 200;
|
|
106
|
+
/** Pages in flight at once, after the first. */
|
|
107
|
+
const PARALLEL_PAGES = 8;
|
|
106
108
|
|
|
107
109
|
async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
108
110
|
const { field, type } = tableFor(c, q.address, q.event.name);
|
|
@@ -135,29 +137,31 @@ async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<Hist
|
|
|
135
137
|
// postgraphile refuses a null or an empty filter, so each argument appears only when it has content
|
|
136
138
|
const hasCondition = Object.keys(condition).length > 0;
|
|
137
139
|
const hasFilter = Object.keys(filter).length > 0;
|
|
138
|
-
const decl = `$
|
|
139
|
-
const argsOf = `first: ${pageSize},
|
|
140
|
+
const decl = `$offset: Int${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
|
|
141
|
+
const argsOf = `first: ${pageSize}, offset: $offset, orderBy: ${order}${hasCondition ? ", condition: $condition" : ""}${hasFilter ? ", filter: $filter" : ""}`;
|
|
140
142
|
const query = `query History(${decl}) {
|
|
141
143
|
${field}(${argsOf}) {
|
|
142
|
-
|
|
144
|
+
totalCount nodes { ${selection} }
|
|
143
145
|
}
|
|
144
146
|
}`;
|
|
145
147
|
type Row = Record<string, unknown>;
|
|
146
|
-
type Conn = {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
type Conn = { totalCount: number; nodes: Row[] };
|
|
149
|
+
const page = async (offset: number): Promise<Conn> =>
|
|
150
|
+
(await gql<Record<string, Conn>>(q.indexer!, query, { offset, ...(hasCondition ? { condition } : {}), ...(hasFilter ? { filter } : {}) }, q.signal))[field]!;
|
|
151
|
+
// The first page says how many rows there are; the rest are fetched in parallel by offset, not
|
|
152
|
+
// one cursor after another: a day of a busy pool is a few pages, and a round trip to a distant
|
|
153
|
+
// indexer costs more than the page. Ascending by key, rows that land meanwhile append at the end
|
|
154
|
+
// and shift no earlier offset.
|
|
155
|
+
const first = await page(0);
|
|
156
|
+
const rows: Row[] = [...first.nodes];
|
|
157
|
+
if (q.latest === undefined && first.totalCount > first.nodes.length) {
|
|
158
|
+
const pages = Math.ceil(first.totalCount / PAGE);
|
|
159
|
+
if (pages > MAX_PAGES) throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
160
|
+
const offsets = Array.from({ length: pages - 1 }, (_, i) => (i + 1) * PAGE);
|
|
161
|
+
for (let i = 0; i < offsets.length; i += PARALLEL_PAGES) {
|
|
162
|
+
const got = await Promise.all(offsets.slice(i, i + PARALLEL_PAGES).map(page));
|
|
163
|
+
for (const g of got) rows.push(...g.nodes);
|
|
164
|
+
}
|
|
161
165
|
}
|
|
162
166
|
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
163
167
|
const want = q.address.toLowerCase();
|