@logswap/sdk 0.5.35
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 +130 -0
- package/dist/actions.d.ts +136 -0
- package/dist/actions.js +140 -0
- package/dist/admin.d.ts +11 -0
- package/dist/admin.js +29 -0
- package/dist/claims.d.ts +53 -0
- package/dist/claims.js +98 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.js +68 -0
- package/dist/fpool.d.ts +416 -0
- package/dist/fpool.js +653 -0
- package/dist/generated.d.ts +6522 -0
- package/dist/generated.js +3755 -0
- package/dist/ids.d.ts +42 -0
- package/dist/ids.js +72 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/indexer.d.ts +91 -0
- package/dist/indexer.js +197 -0
- package/dist/keys.d.ts +40 -0
- package/dist/keys.js +50 -0
- package/dist/liquidity.d.ts +223 -0
- package/dist/liquidity.js +305 -0
- package/dist/onboard.d.ts +56 -0
- package/dist/onboard.js +188 -0
- package/dist/pools.d.ts +182 -0
- package/dist/pools.js +322 -0
- package/dist/positions.d.ts +164 -0
- package/dist/positions.js +197 -0
- package/dist/swap.d.ts +68 -0
- package/dist/swap.js +91 -0
- package/dist/tokencache.d.ts +76 -0
- package/dist/tokencache.js +150 -0
- package/dist/write.d.ts +86 -0
- package/dist/write.js +106 -0
- package/package.json +48 -0
- package/src/actions.ts +207 -0
- package/src/admin.ts +33 -0
- package/src/claims.ts +132 -0
- package/src/client.ts +104 -0
- package/src/fpool.ts +935 -0
- package/src/generated.ts +3762 -0
- package/src/ids.ts +78 -0
- package/src/index.ts +23 -0
- package/src/indexer.ts +252 -0
- package/src/keys.ts +73 -0
- package/src/liquidity.ts +430 -0
- package/src/onboard.ts +228 -0
- package/src/pools.ts +396 -0
- package/src/positions.ts +276 -0
- package/src/swap.ts +140 -0
- package/src/tokencache.ts +169 -0
- package/src/write.ts +134 -0
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ids.ts — the ERC-6909 id namespace, computed OFFLINE.
|
|
3
|
+
*
|
|
4
|
+
* Two kinds of id share one ERC-6909 space, split on **bit 255**:
|
|
5
|
+
*
|
|
6
|
+
* bit 255 SET a position class: (1 << 255) | (keccak256(poolId ‖ packed) >> 1)
|
|
7
|
+
* bit 255 CLEAR a token claim: uint160(tokenAddress)
|
|
8
|
+
*
|
|
9
|
+
* Both are **tier-1 frozen** (docs/app.md). The namespace bit is what lets a wallet UI, a lender, or
|
|
10
|
+
* an indexer tell an LP position from a bearer IOU — and an indexer that ignores it will report
|
|
11
|
+
* phantom LP positions for market makers parking inventory.
|
|
12
|
+
*/
|
|
13
|
+
import { type Address, type Hex } from "viem";
|
|
14
|
+
/** Sentinel meaning "no cap" — `type(int256).min`. */
|
|
15
|
+
export declare const NO_CAP: bigint;
|
|
16
|
+
/**
|
|
17
|
+
* `packed` — the class's (floor, cap) as two 32-bit fields: `kFloor + 2^31` in the low half,
|
|
18
|
+
* `kCap + 2^31` in the high half, or zero there when uncapped.
|
|
19
|
+
*
|
|
20
|
+
* Throws on an off-grid floor or cap rather than producing an id the manager would reject, so a
|
|
21
|
+
* client fails at the call site instead of at the transaction.
|
|
22
|
+
*/
|
|
23
|
+
export declare function packFloorCap(tickSpacing: bigint, floor: bigint, cap?: bigint): bigint;
|
|
24
|
+
/** Inverse of {@link packFloorCap}. `capped === false` means the cap field is unset. */
|
|
25
|
+
export declare function unpackFloorCap(tickSpacing: bigint, packed: bigint): {
|
|
26
|
+
floor: bigint;
|
|
27
|
+
cap: bigint;
|
|
28
|
+
capped: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The position-class id: `(1 << 255) | (keccak256(poolId ‖ packed) >> 1)`.
|
|
32
|
+
*
|
|
33
|
+
* The `>> 1` is what frees bit 255 for the namespace flag, so the hash contributes 255 bits.
|
|
34
|
+
* `abi.encodePacked(bytes32, uint64)` is 40 bytes — 32 for the pool id, 8 for `packed`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function positionId(poolId_: Hex, tickSpacing: bigint, floor: bigint, cap?: bigint): bigint;
|
|
37
|
+
/** A token claim's id is just the token address. */
|
|
38
|
+
export declare function claimId(token: Address): bigint;
|
|
39
|
+
/** True for a position class, false for a token claim. Split every 6909 `Transfer` on this. */
|
|
40
|
+
export declare function isPosition(id: bigint): boolean;
|
|
41
|
+
/** The token an id refers to, or `null` if the id is a position rather than a claim. */
|
|
42
|
+
export declare function claimToken(id: bigint): Address | null;
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ids.ts — the ERC-6909 id namespace, computed OFFLINE.
|
|
3
|
+
*
|
|
4
|
+
* Two kinds of id share one ERC-6909 space, split on **bit 255**:
|
|
5
|
+
*
|
|
6
|
+
* bit 255 SET a position class: (1 << 255) | (keccak256(poolId ‖ packed) >> 1)
|
|
7
|
+
* bit 255 CLEAR a token claim: uint160(tokenAddress)
|
|
8
|
+
*
|
|
9
|
+
* Both are **tier-1 frozen** (docs/app.md). The namespace bit is what lets a wallet UI, a lender, or
|
|
10
|
+
* an indexer tell an LP position from a bearer IOU — and an indexer that ignores it will report
|
|
11
|
+
* phantom LP positions for market makers parking inventory.
|
|
12
|
+
*/
|
|
13
|
+
import { concatHex, keccak256, pad, toHex } from "viem";
|
|
14
|
+
/** Packing offset for the signed tick index `k`, so it fits an unsigned 32-bit field. */
|
|
15
|
+
const ID_OFFSET = 1n << 31n;
|
|
16
|
+
/** Sentinel meaning "no cap" — `type(int256).min`. */
|
|
17
|
+
export const NO_CAP = -(2n ** 255n);
|
|
18
|
+
const BIT_255 = 1n << 255n;
|
|
19
|
+
/**
|
|
20
|
+
* `packed` — the class's (floor, cap) as two 32-bit fields: `kFloor + 2^31` in the low half,
|
|
21
|
+
* `kCap + 2^31` in the high half, or zero there when uncapped.
|
|
22
|
+
*
|
|
23
|
+
* Throws on an off-grid floor or cap rather than producing an id the manager would reject, so a
|
|
24
|
+
* client fails at the call site instead of at the transaction.
|
|
25
|
+
*/
|
|
26
|
+
export function packFloorCap(tickSpacing, floor, cap = NO_CAP) {
|
|
27
|
+
if (tickSpacing <= 0n)
|
|
28
|
+
throw new Error("tickSpacing must be positive");
|
|
29
|
+
if (floor % tickSpacing !== 0n)
|
|
30
|
+
throw new Error(`floor ${floor} is off-grid for spacing ${tickSpacing}`);
|
|
31
|
+
let packed = BigInt.asUintN(64, floor / tickSpacing + ID_OFFSET);
|
|
32
|
+
if (cap !== NO_CAP) {
|
|
33
|
+
if (cap <= floor)
|
|
34
|
+
throw new Error("cap must be strictly above floor");
|
|
35
|
+
if (cap % tickSpacing !== 0n)
|
|
36
|
+
throw new Error(`cap ${cap} is off-grid for spacing ${tickSpacing}`);
|
|
37
|
+
packed |= BigInt.asUintN(64, cap / tickSpacing + ID_OFFSET) << 32n;
|
|
38
|
+
}
|
|
39
|
+
return packed;
|
|
40
|
+
}
|
|
41
|
+
/** Inverse of {@link packFloorCap}. `capped === false` means the cap field is unset. */
|
|
42
|
+
export function unpackFloorCap(tickSpacing, packed) {
|
|
43
|
+
const kf = (packed & 0xffffffffn) - ID_OFFSET;
|
|
44
|
+
const hi = packed >> 32n;
|
|
45
|
+
const capped = hi !== 0n;
|
|
46
|
+
return { floor: kf * tickSpacing, cap: capped ? (hi - ID_OFFSET) * tickSpacing : NO_CAP, capped };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The position-class id: `(1 << 255) | (keccak256(poolId ‖ packed) >> 1)`.
|
|
50
|
+
*
|
|
51
|
+
* The `>> 1` is what frees bit 255 for the namespace flag, so the hash contributes 255 bits.
|
|
52
|
+
* `abi.encodePacked(bytes32, uint64)` is 40 bytes — 32 for the pool id, 8 for `packed`.
|
|
53
|
+
*/
|
|
54
|
+
export function positionId(poolId_, tickSpacing, floor, cap = NO_CAP) {
|
|
55
|
+
const packed = packFloorCap(tickSpacing, floor, cap);
|
|
56
|
+
const h = BigInt(keccak256(concatHex([poolId_, pad(toHex(packed), { size: 8 })])));
|
|
57
|
+
return BIT_255 | (h >> 1n);
|
|
58
|
+
}
|
|
59
|
+
/** A token claim's id is just the token address. */
|
|
60
|
+
export function claimId(token) {
|
|
61
|
+
return BigInt(token);
|
|
62
|
+
}
|
|
63
|
+
/** True for a position class, false for a token claim. Split every 6909 `Transfer` on this. */
|
|
64
|
+
export function isPosition(id) {
|
|
65
|
+
return (id & BIT_255) !== 0n;
|
|
66
|
+
}
|
|
67
|
+
/** The token an id refers to, or `null` if the id is a position rather than a claim. */
|
|
68
|
+
export function claimToken(id) {
|
|
69
|
+
if (isPosition(id))
|
|
70
|
+
return null;
|
|
71
|
+
return pad(toHex(id), { size: 20 });
|
|
72
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logswap SDK — typed, offline-first helpers over the CPoolManager / Router / Lens surface.
|
|
3
|
+
*
|
|
4
|
+
* The manager/router/lens split is an EIP-170 artifact, not a design boundary, so it is hidden
|
|
5
|
+
* here: callers address markets by `PoolKey` and never choose a contract. A future re-homing of
|
|
6
|
+
* a function is then an SDK patch rather than a breaking change (docs/app.md, tier 2).
|
|
7
|
+
*/
|
|
8
|
+
export * from "./keys.js";
|
|
9
|
+
export * from "./ids.js";
|
|
10
|
+
export * from "./client.js";
|
|
11
|
+
export * from "./pools.js";
|
|
12
|
+
export * from "./positions.js";
|
|
13
|
+
export * from "./write.js";
|
|
14
|
+
export * from "./swap.js";
|
|
15
|
+
export * from "./liquidity.js";
|
|
16
|
+
export * from "./actions.js";
|
|
17
|
+
export * from "./claims.js";
|
|
18
|
+
export * from "./onboard.js";
|
|
19
|
+
export * from "./indexer.js";
|
|
20
|
+
export * from "./tokencache.js";
|
|
21
|
+
export * from "./fpool.js";
|
|
22
|
+
export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi } from "./generated.js";
|
|
23
|
+
export * from "./admin.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logswap SDK — typed, offline-first helpers over the CPoolManager / Router / Lens surface.
|
|
3
|
+
*
|
|
4
|
+
* The manager/router/lens split is an EIP-170 artifact, not a design boundary, so it is hidden
|
|
5
|
+
* here: callers address markets by `PoolKey` and never choose a contract. A future re-homing of
|
|
6
|
+
* a function is then an SDK patch rather than a breaking change (docs/app.md, tier 2).
|
|
7
|
+
*/
|
|
8
|
+
export * from "./keys.js";
|
|
9
|
+
export * from "./ids.js";
|
|
10
|
+
export * from "./client.js";
|
|
11
|
+
export * from "./pools.js";
|
|
12
|
+
export * from "./positions.js";
|
|
13
|
+
export * from "./write.js";
|
|
14
|
+
export * from "./swap.js";
|
|
15
|
+
export * from "./liquidity.js";
|
|
16
|
+
export * from "./actions.js";
|
|
17
|
+
export * from "./claims.js";
|
|
18
|
+
export * from "./onboard.js";
|
|
19
|
+
export * from "./indexer.js";
|
|
20
|
+
export * from "./tokencache.js";
|
|
21
|
+
export * from "./fpool.js";
|
|
22
|
+
export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi } from "./generated.js";
|
|
23
|
+
export * from "./admin.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* indexer.ts — reads the rindexer GraphQL that state cannot answer.
|
|
3
|
+
*
|
|
4
|
+
* The division of labour matters and is not arbitrary: **the indexer enumerates, the chain
|
|
5
|
+
* values.** ERC-6909 has no enumeration, so there is no way to ask a node "which positions exist in
|
|
6
|
+
* this pool" — only the `IdRegistered` log knows. But an indexed row is a historical fact, and `L`
|
|
7
|
+
* changes with every mint, burn and harvest afterwards. So ids come from here and the current `L`
|
|
8
|
+
* is read on-chain, rather than trusting a number that was true at index time.
|
|
9
|
+
*/
|
|
10
|
+
import type { Hex } from "viem";
|
|
11
|
+
import type { LogswapClient } from "./client.js";
|
|
12
|
+
import { type DiscoveredMarket } from "./pools.js";
|
|
13
|
+
import { type PoolKey } from "./keys.js";
|
|
14
|
+
export interface IndexerError {
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A `bytea` column renders as `0x…` but FILTERS as `\x…`.
|
|
19
|
+
*
|
|
20
|
+
* Passing back the exact value the API just gave you returns zero rows, with no error — the query
|
|
21
|
+
* is valid, the comparison simply never matches. Every filter on a hash goes through here.
|
|
22
|
+
*
|
|
23
|
+
* The result is passed as a **variable**, never interpolated into query text: `\x` is not a legal
|
|
24
|
+
* escape inside a GraphQL string literal, so an interpolated filter fails with
|
|
25
|
+
* `Syntax Error: Invalid character escape sequence`. Variables carry the value untouched.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bytea(hex: Hex): string;
|
|
28
|
+
export declare function gql<T>(url: string, query: string, variables?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
|
|
29
|
+
/** A position class as the chain registered it: the floor/cap geometry, fixed for the id's life. */
|
|
30
|
+
export interface RegisteredId {
|
|
31
|
+
id: bigint;
|
|
32
|
+
floor: bigint;
|
|
33
|
+
cap: bigint;
|
|
34
|
+
capped: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Every position class ever registered in a pool. Historical — some may since have burnt to zero. */
|
|
37
|
+
export declare function registeredIds(url: string, poolId: Hex, signal?: AbortSignal): Promise<RegisteredId[]>;
|
|
38
|
+
export interface PoolPosition extends RegisteredId {
|
|
39
|
+
/** Total exposure across every holder, read from the chain — not from the indexed row. */
|
|
40
|
+
L: bigint;
|
|
41
|
+
shares: bigint;
|
|
42
|
+
/** Spot is inside this position's range, so it is earning fees. */
|
|
43
|
+
active: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every live position class in a pool, split by whether it is earning.
|
|
47
|
+
*
|
|
48
|
+
* A position is **active** when spot sits at or above its floor and, if capped, below its cap —
|
|
49
|
+
* the same condition the tick ladder applies when it steps `lActive`.
|
|
50
|
+
*
|
|
51
|
+
* Classes that have burnt to `L = 0` are dropped: they are registered forever, but a list of
|
|
52
|
+
* positions should show positions, not the history of ones that existed.
|
|
53
|
+
*/
|
|
54
|
+
export declare function poolPositions(c: LogswapClient, url: string, poolId: Hex, x: bigint, signal?: AbortSignal): Promise<PoolPosition[]>;
|
|
55
|
+
/** Is the indexer answering? Used to degrade a screen rather than fail it. */
|
|
56
|
+
export declare function indexerHealthy(url: string, signal?: AbortSignal): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Markets, from the indexer rather than a log scan.
|
|
59
|
+
*
|
|
60
|
+
* `discoverMarkets` reads `Initialize` logs straight from the node, which is correct until the node
|
|
61
|
+
* stops keeping them. anvil's `--prune-history` drops historical transactions and receipts beyond a
|
|
62
|
+
* window, so on a devnet that mines steadily the deployment's own logs age out and a scan from
|
|
63
|
+
* block 0 returns nothing — while the contracts remain perfectly alive. Any real chain has the same
|
|
64
|
+
* shape in a different form: archive-depth `getLogs` is exactly what public RPCs refuse.
|
|
65
|
+
*
|
|
66
|
+
* An indexer does not have that problem, because it recorded the event when it happened. This is
|
|
67
|
+
* the more durable path and should be preferred wherever an indexer is configured, with the log
|
|
68
|
+
* scan as the fallback for when it is not.
|
|
69
|
+
*
|
|
70
|
+
* `Initialize` carries every field of the `PoolKey`, so the key is rebuilt exactly and `poolId` is
|
|
71
|
+
* recomputed locally rather than trusted from the row — the same offline derivation the SDK uses
|
|
72
|
+
* everywhere else, so a wrong id fails loudly instead of addressing the wrong pool.
|
|
73
|
+
*/
|
|
74
|
+
export declare function discoverMarketsIndexed(url: string, signal?: AbortSignal): Promise<DiscoveredMarket[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Markets, preferring the indexer and falling back to a log scan.
|
|
77
|
+
*
|
|
78
|
+
* The order matters: the indexer is authoritative for history, the node is authoritative for state.
|
|
79
|
+
* A market the node can no longer tell you about is still a market.
|
|
80
|
+
*/
|
|
81
|
+
export declare function discoverMarketsBest(c: LogswapClient, url: string | undefined, signal?: AbortSignal): Promise<{
|
|
82
|
+
markets: DiscoveredMarket[];
|
|
83
|
+
source: "indexer" | "chain";
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* The UNCAPPED position classes of a pool, from the CHAIN alone: the lens enumerates live floors
|
|
87
|
+
* (one bitmap bit each), and an uncapped id is a pure function of (key, floor). No indexer.
|
|
88
|
+
* Capped classes need `IdRegistered` replay and are absent here — callers that also want those
|
|
89
|
+
* use the indexed `poolPositions` and fall back to this when the indexer is missing or stale.
|
|
90
|
+
*/
|
|
91
|
+
export declare function poolPositionsFromChain(c: LogswapClient, key: PoolKey, x: bigint): Promise<PoolPosition[]>;
|
package/dist/indexer.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* indexer.ts — reads the rindexer GraphQL that state cannot answer.
|
|
3
|
+
*
|
|
4
|
+
* The division of labour matters and is not arbitrary: **the indexer enumerates, the chain
|
|
5
|
+
* values.** ERC-6909 has no enumeration, so there is no way to ask a node "which positions exist in
|
|
6
|
+
* this pool" — only the `IdRegistered` log knows. But an indexed row is a historical fact, and `L`
|
|
7
|
+
* changes with every mint, burn and harvest afterwards. So ids come from here and the current `L`
|
|
8
|
+
* is read on-chain, rather than trusting a number that was true at index time.
|
|
9
|
+
*/
|
|
10
|
+
import { cPoolManagerAbi } from "./generated.js";
|
|
11
|
+
import { getPositionClass } from "./positions.js";
|
|
12
|
+
import { discoverMarkets } from "./pools.js";
|
|
13
|
+
import { poolId } from "./keys.js";
|
|
14
|
+
import { NO_CAP } from "./ids.js";
|
|
15
|
+
/**
|
|
16
|
+
* A `bytea` column renders as `0x…` but FILTERS as `\x…`.
|
|
17
|
+
*
|
|
18
|
+
* Passing back the exact value the API just gave you returns zero rows, with no error — the query
|
|
19
|
+
* is valid, the comparison simply never matches. Every filter on a hash goes through here.
|
|
20
|
+
*
|
|
21
|
+
* The result is passed as a **variable**, never interpolated into query text: `\x` is not a legal
|
|
22
|
+
* escape inside a GraphQL string literal, so an interpolated filter fails with
|
|
23
|
+
* `Syntax Error: Invalid character escape sequence`. Variables carry the value untouched.
|
|
24
|
+
*/
|
|
25
|
+
export function bytea(hex) {
|
|
26
|
+
return `\\x${hex.replace(/^0x/, "")}`;
|
|
27
|
+
}
|
|
28
|
+
export async function gql(url, query, variables, signal) {
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "content-type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ query, variables }),
|
|
33
|
+
signal,
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
throw new Error(`indexer: HTTP ${res.status} from ${url}`);
|
|
37
|
+
const body = (await res.json());
|
|
38
|
+
if (body.errors?.length)
|
|
39
|
+
throw new Error(`indexer: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
40
|
+
if (!body.data)
|
|
41
|
+
throw new Error("indexer: empty response");
|
|
42
|
+
return body.data;
|
|
43
|
+
}
|
|
44
|
+
/** Every position class ever registered in a pool. Historical — some may since have burnt to zero. */
|
|
45
|
+
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) => ({
|
|
51
|
+
id: BigInt(n.id),
|
|
52
|
+
floor: BigInt(n.floor),
|
|
53
|
+
cap: BigInt(n.cap),
|
|
54
|
+
capped: n.capped,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Every live position class in a pool, split by whether it is earning.
|
|
59
|
+
*
|
|
60
|
+
* A position is **active** when spot sits at or above its floor and, if capped, below its cap —
|
|
61
|
+
* the same condition the tick ladder applies when it steps `lActive`.
|
|
62
|
+
*
|
|
63
|
+
* Classes that have burnt to `L = 0` are dropped: they are registered forever, but a list of
|
|
64
|
+
* positions should show positions, not the history of ones that existed.
|
|
65
|
+
*/
|
|
66
|
+
export async function poolPositions(c, url, poolId, x, signal) {
|
|
67
|
+
const ids = await registeredIds(url, poolId, signal);
|
|
68
|
+
const rows = await Promise.all(ids.map(async (r) => {
|
|
69
|
+
const cls = await getPositionClass(c, r.id);
|
|
70
|
+
return { ...r, L: cls.L, shares: cls.shares, active: x >= r.floor && (!r.capped || x < r.cap) };
|
|
71
|
+
}));
|
|
72
|
+
return rows.filter((r) => r.L > 0n).sort((a, b) => (b.L > a.L ? 1 : b.L < a.L ? -1 : 0));
|
|
73
|
+
}
|
|
74
|
+
/** Is the indexer answering? Used to degrade a screen rather than fail it. */
|
|
75
|
+
export async function indexerHealthy(url, signal) {
|
|
76
|
+
try {
|
|
77
|
+
await gql(url, "{ __typename }", undefined, signal);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Markets, from the indexer rather than a log scan.
|
|
86
|
+
*
|
|
87
|
+
* `discoverMarkets` reads `Initialize` logs straight from the node, which is correct until the node
|
|
88
|
+
* stops keeping them. anvil's `--prune-history` drops historical transactions and receipts beyond a
|
|
89
|
+
* window, so on a devnet that mines steadily the deployment's own logs age out and a scan from
|
|
90
|
+
* block 0 returns nothing — while the contracts remain perfectly alive. Any real chain has the same
|
|
91
|
+
* shape in a different form: archive-depth `getLogs` is exactly what public RPCs refuse.
|
|
92
|
+
*
|
|
93
|
+
* An indexer does not have that problem, because it recorded the event when it happened. This is
|
|
94
|
+
* the more durable path and should be preferred wherever an indexer is configured, with the log
|
|
95
|
+
* scan as the fallback for when it is not.
|
|
96
|
+
*
|
|
97
|
+
* `Initialize` carries every field of the `PoolKey`, so the key is rebuilt exactly and `poolId` is
|
|
98
|
+
* recomputed locally rather than trusted from the row — the same offline derivation the SDK uses
|
|
99
|
+
* everywhere else, so a wrong id fails loudly instead of addressing the wrong pool.
|
|
100
|
+
*/
|
|
101
|
+
export async function discoverMarketsIndexed(url, signal) {
|
|
102
|
+
const q = `{ allInitializes(first: 1000) { nodes {
|
|
103
|
+
poolId base quote tickSpacing phiMin kappa alpha x0 blockNumber txHash
|
|
104
|
+
} } }`;
|
|
105
|
+
const d = await gql(url, q, undefined, signal);
|
|
106
|
+
return d.allInitializes.nodes.map((n) => {
|
|
107
|
+
const key = {
|
|
108
|
+
base: n.base,
|
|
109
|
+
quote: n.quote,
|
|
110
|
+
tickSpacing: BigInt(n.tickSpacing),
|
|
111
|
+
phiMin: BigInt(n.phiMin),
|
|
112
|
+
kappa: BigInt(n.kappa),
|
|
113
|
+
alpha: BigInt(n.alpha),
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
key,
|
|
117
|
+
poolId: poolId(key),
|
|
118
|
+
x0: BigInt(n.x0),
|
|
119
|
+
blockNumber: BigInt(n.blockNumber),
|
|
120
|
+
transactionHash: n.txHash,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Markets, preferring the indexer and falling back to a log scan.
|
|
126
|
+
*
|
|
127
|
+
* The order matters: the indexer is authoritative for history, the node is authoritative for state.
|
|
128
|
+
* A market the node can no longer tell you about is still a market.
|
|
129
|
+
*/
|
|
130
|
+
export async function discoverMarketsBest(c, url, signal) {
|
|
131
|
+
if (url) {
|
|
132
|
+
try {
|
|
133
|
+
const markets = await discoverMarketsIndexed(url, signal);
|
|
134
|
+
// The indexer is a CACHE and the chain is the truth. After a redeploy the indexer keeps
|
|
135
|
+
// serving the previous deployment's markets; calling the new manager with those keys makes
|
|
136
|
+
// every read revert, refresh-proof (it presented as "phiEff reverted", forever). So verify
|
|
137
|
+
// each candidate against the manager the client is actually pointed at, and keep only the
|
|
138
|
+
// markets that exist there; if none survive, the chain scan is the answer.
|
|
139
|
+
if (markets.length) {
|
|
140
|
+
const alive = (await Promise.all(markets.map(async (m) => {
|
|
141
|
+
try {
|
|
142
|
+
await c.public.readContract({
|
|
143
|
+
address: c.addresses.cPoolManager,
|
|
144
|
+
abi: cPoolManagerAbi,
|
|
145
|
+
functionName: "phiEff",
|
|
146
|
+
args: [m.key],
|
|
147
|
+
});
|
|
148
|
+
return m;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}))).filter((m) => m !== null);
|
|
154
|
+
if (alive.length)
|
|
155
|
+
return { markets: alive, source: "indexer" };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* fall through — a missing indexer must not cost you the market list */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { markets: await discoverMarkets(c), source: "chain" };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The UNCAPPED position classes of a pool, from the CHAIN alone: the lens enumerates live floors
|
|
166
|
+
* (one bitmap bit each), and an uncapped id is a pure function of (key, floor). No indexer.
|
|
167
|
+
* Capped classes need `IdRegistered` replay and are absent here — callers that also want those
|
|
168
|
+
* use the indexed `poolPositions` and fall back to this when the indexer is missing or stale.
|
|
169
|
+
*/
|
|
170
|
+
export async function poolPositionsFromChain(c, key, x) {
|
|
171
|
+
const { logswapLensAbi } = await import("./generated.js");
|
|
172
|
+
const floors = (await c.public.readContract({
|
|
173
|
+
address: c.addresses.lens,
|
|
174
|
+
abi: logswapLensAbi,
|
|
175
|
+
functionName: "liveTicks",
|
|
176
|
+
args: [key],
|
|
177
|
+
}));
|
|
178
|
+
const pool = poolId(key);
|
|
179
|
+
const rows = await Promise.all([...floors].map(async (floor) => {
|
|
180
|
+
const id = (await c.public.readContract({
|
|
181
|
+
address: c.addresses.cPoolManager,
|
|
182
|
+
abi: cPoolManagerAbi,
|
|
183
|
+
functionName: "idOf",
|
|
184
|
+
args: [key, floor],
|
|
185
|
+
}));
|
|
186
|
+
const [L, shares] = (await c.public.readContract({
|
|
187
|
+
address: c.addresses.cPoolManager,
|
|
188
|
+
abi: cPoolManagerAbi,
|
|
189
|
+
functionName: "positions",
|
|
190
|
+
args: [id],
|
|
191
|
+
}));
|
|
192
|
+
if (L === 0n)
|
|
193
|
+
return null;
|
|
194
|
+
return { id, poolId: pool, floor, cap: NO_CAP, capped: false, L, shares, active: x >= floor };
|
|
195
|
+
}));
|
|
196
|
+
return rows.filter((r) => r !== null);
|
|
197
|
+
}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keys.ts — market identity, computed OFFLINE.
|
|
3
|
+
*
|
|
4
|
+
* A market is a `PoolKey`, and its id is the hash of that key. Both are **tier-1 frozen**
|
|
5
|
+
* (docs/app.md): the key is in every keyed signature *and* `poolId = keccak256(abi.encode(key))`,
|
|
6
|
+
* so adding a field would change every selector and re-identify every market, orphaning all
|
|
7
|
+
* indexed history.
|
|
8
|
+
*
|
|
9
|
+
* This module reimplements `PoolKeyLib.toId` in TypeScript so a client can address a market with no
|
|
10
|
+
* RPC at all. `test/derivation.test.ts` pins it against vectors generated by the contract itself —
|
|
11
|
+
* if the two ever disagree, that test fails rather than the SDK silently addressing the wrong pool.
|
|
12
|
+
*/
|
|
13
|
+
import { type Address, type Hex } from "viem";
|
|
14
|
+
/**
|
|
15
|
+
* The economic tuple that IS the market. Field order is load-bearing: it is the ABI encoding order,
|
|
16
|
+
* so it must match `src/types/PoolKey.sol` exactly.
|
|
17
|
+
*/
|
|
18
|
+
export interface PoolKey {
|
|
19
|
+
base: Address;
|
|
20
|
+
quote: Address;
|
|
21
|
+
/** Log-tick spacing, WAD. The LP-boundary ladder — position floors and caps. */
|
|
22
|
+
tickSpacing: bigint;
|
|
23
|
+
/** Fee floor, WAD. `0n` selects `DEFAULT_FEE_MIN` (2 bp) at read time. */
|
|
24
|
+
phiMin: bigint;
|
|
25
|
+
/** Variance-fee coefficient, WAD. `0n` disables the dynamic fee. */
|
|
26
|
+
kappa: bigint;
|
|
27
|
+
/** EMA decay, WAD. `0n` selects `DEFAULT_ALPHA` (1/2) at read time. */
|
|
28
|
+
alpha: bigint;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `poolId = keccak256(abi.encode(key))` — the primary key of the entire read model.
|
|
32
|
+
*
|
|
33
|
+
* Note the zero-vs-default caveat the Solidity type documents: `phiMin` and `alpha` of `0` resolve
|
|
34
|
+
* to their defaults at READ time, so a key carrying `0n` and one carrying the resolved default are
|
|
35
|
+
* DIFFERENT markets with different ids, even though they behave identically. Always address a
|
|
36
|
+
* market by the key it was initialized with.
|
|
37
|
+
*/
|
|
38
|
+
export declare function poolId(key: PoolKey): Hex;
|
|
39
|
+
/** Structural equality of two keys — i.e. "the same market". */
|
|
40
|
+
export declare function sameMarket(a: PoolKey, b: PoolKey): boolean;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keys.ts — market identity, computed OFFLINE.
|
|
3
|
+
*
|
|
4
|
+
* A market is a `PoolKey`, and its id is the hash of that key. Both are **tier-1 frozen**
|
|
5
|
+
* (docs/app.md): the key is in every keyed signature *and* `poolId = keccak256(abi.encode(key))`,
|
|
6
|
+
* so adding a field would change every selector and re-identify every market, orphaning all
|
|
7
|
+
* indexed history.
|
|
8
|
+
*
|
|
9
|
+
* This module reimplements `PoolKeyLib.toId` in TypeScript so a client can address a market with no
|
|
10
|
+
* RPC at all. `test/derivation.test.ts` pins it against vectors generated by the contract itself —
|
|
11
|
+
* if the two ever disagree, that test fails rather than the SDK silently addressing the wrong pool.
|
|
12
|
+
*/
|
|
13
|
+
import { encodeAbiParameters, keccak256 } from "viem";
|
|
14
|
+
const POOL_KEY_ABI = [
|
|
15
|
+
{
|
|
16
|
+
type: "tuple",
|
|
17
|
+
components: [
|
|
18
|
+
{ name: "base", type: "address" },
|
|
19
|
+
{ name: "quote", type: "address" },
|
|
20
|
+
{ name: "tickSpacing", type: "int256" },
|
|
21
|
+
{ name: "phiMin", type: "uint256" },
|
|
22
|
+
{ name: "kappa", type: "uint256" },
|
|
23
|
+
{ name: "alpha", type: "uint256" },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* `poolId = keccak256(abi.encode(key))` — the primary key of the entire read model.
|
|
29
|
+
*
|
|
30
|
+
* Note the zero-vs-default caveat the Solidity type documents: `phiMin` and `alpha` of `0` resolve
|
|
31
|
+
* to their defaults at READ time, so a key carrying `0n` and one carrying the resolved default are
|
|
32
|
+
* DIFFERENT markets with different ids, even though they behave identically. Always address a
|
|
33
|
+
* market by the key it was initialized with.
|
|
34
|
+
*/
|
|
35
|
+
export function poolId(key) {
|
|
36
|
+
return keccak256(encodeAbiParameters(POOL_KEY_ABI, [
|
|
37
|
+
{
|
|
38
|
+
base: key.base,
|
|
39
|
+
quote: key.quote,
|
|
40
|
+
tickSpacing: key.tickSpacing,
|
|
41
|
+
phiMin: key.phiMin,
|
|
42
|
+
kappa: key.kappa,
|
|
43
|
+
alpha: key.alpha,
|
|
44
|
+
},
|
|
45
|
+
]));
|
|
46
|
+
}
|
|
47
|
+
/** Structural equality of two keys — i.e. "the same market". */
|
|
48
|
+
export function sameMarket(a, b) {
|
|
49
|
+
return poolId(a) === poolId(b);
|
|
50
|
+
}
|