@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/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# logswap-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [Logswap](https://github.com/logswap/logswap-contract), the liquidity
|
|
4
|
+
primitive introducing the **logarithmic invariant** — typed wrappers over the `CPoolManager` /
|
|
5
|
+
`FPoolManager` / `LogswapRouter` / `LogswapLens` surface.
|
|
6
|
+
|
|
7
|
+
**Status: identity, reads and writes for both singletons.** Indexer next.
|
|
8
|
+
|
|
9
|
+
## What is here now
|
|
10
|
+
|
|
11
|
+
The two things the SDK must own because they are easy to get wrong by hand, and because getting them
|
|
12
|
+
wrong is silent:
|
|
13
|
+
|
|
14
|
+
| module | what it does |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `src/keys.ts` | `PoolKey` and `poolId = keccak256(abi.encode(key))` — market identity |
|
|
17
|
+
| `src/ids.ts` | the ERC-6909 id namespace: position-class ids, token-claim ids, floor/cap packing |
|
|
18
|
+
| `src/client.ts` | the handle, plus `assertVersion` — refuse a deployment the ABI was not built for |
|
|
19
|
+
| `src/pools.ts` | market discovery from `Initialize` logs, pool state, `lpEdge` = F − Σ/2 |
|
|
20
|
+
| `src/positions.ts` | classes, balances, claimable fees, the bit-255 position/claim split |
|
|
21
|
+
| `src/swap.ts` | exact-in/out, multi-hop, and exact quotes via the revert-quoter |
|
|
22
|
+
| `src/liquidity.ts` | mint, blended mint, zap, update, move, **harvest**, burn |
|
|
23
|
+
| `src/claims.ts` | fee claims and the 6909 operator grant |
|
|
24
|
+
| `src/onboard.ts` | the three approvals — ERC-20 → Permit2 → router, and the 6909 grant |
|
|
25
|
+
|
|
26
|
+
Both are computed **offline** — no RPC, no deployment needed to address a market or a position.
|
|
27
|
+
|
|
28
|
+
## Reading a holder's book
|
|
29
|
+
|
|
30
|
+
- `positionsOf(c, key, owner)` — the **uncapped** book in one lens call (the ladder bitmap
|
|
31
|
+
enumerates floors; an uncapped id is a pure function of the floor).
|
|
32
|
+
- `holdingsOf(c, key, owner)` — the **complete** book, capped classes included: replays
|
|
33
|
+
`IdRegistered` (every class is registered once, with its floor and cap) and checks the owner's
|
|
34
|
+
balance on each. Use this for "my positions"; a capped mint is invisible to `positionsOf`.
|
|
35
|
+
- `getHolderPosition(c, key, owner, floor, cap?)` — one class by (floor, cap); `move(...)` /
|
|
36
|
+
`update(...)` / `harvest(...)` / `burn(...)` / `claimFees(...)` act on it.
|
|
37
|
+
- F side: `discoverFPools(c)` (shape, and the `block` the pool was initialized in), `getFPool`,
|
|
38
|
+
`fPoolShareBalance`, `fPoolShareValue`; `previewZapIn` reads the **lens** (the router's F flows
|
|
39
|
+
live in a facet, its previews moved to the lens).
|
|
40
|
+
|
|
41
|
+
## The derivation gate
|
|
42
|
+
|
|
43
|
+
Both derivations are **tier-1 frozen** (`docs/app.md`): `poolId` is the primary key of the entire
|
|
44
|
+
read model, and the position id is `(1 << 255) | (keccak256(poolId ‖ packed) >> 1)`, baked into every
|
|
45
|
+
client. If the TypeScript and the Solidity disagree, a client addresses the wrong pool or the wrong
|
|
46
|
+
position — silently, with funds behind it.
|
|
47
|
+
|
|
48
|
+
So `test/derivation.test.ts` checks against **vectors generated by the contract**, not by hand:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run vectors # regenerate from the pinned contracts
|
|
52
|
+
npm test # 45 vectors × poolId, positionId, pack/unpack round-trip
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That makes it a genuine cross-implementation check. It is also verified non-vacuous: changing the
|
|
56
|
+
id derivation's shift by one bit fails all 45 position-id vectors.
|
|
57
|
+
|
|
58
|
+
A diff in `vectors/derivation-vectors.json` during an ordinary change means something is wrong — by
|
|
59
|
+
definition, a change there is a tier-1 event.
|
|
60
|
+
|
|
61
|
+
## The pipeline
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
logswap (forge) ──► contracts/abi/*.json ──► wagmi codegen ──► src/generated.ts
|
|
65
|
+
▲ │
|
|
66
|
+
contracts.ref (pinned SHA) the SDK wrappers
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Nothing is ever hand-copied. `contracts.ref` pins the SHA; `npm run codegen` clones it, builds, and
|
|
70
|
+
regenerates; `tsc` is the drift gate, so a contract change that breaks the SDK is a red build rather
|
|
71
|
+
than a runtime surprise.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run codegen # sync the pinned contracts, forge build, wagmi generate -> src/generated.ts
|
|
75
|
+
npm run typecheck # consumes generated.ts; run codegen first on a fresh checkout
|
|
76
|
+
npm run build
|
|
77
|
+
npm test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`typecheck` and `build` both *consume* `src/generated.ts` and neither produces it, so codegen
|
|
81
|
+
runs once and explicitly rather than being chained into both.
|
|
82
|
+
|
|
83
|
+
## Conventions
|
|
84
|
+
|
|
85
|
+
- **The SDK hides the manager/router/lens split.** Which contract hosts which action is currently
|
|
86
|
+
decided by EIP-170 headroom, not by design — so a future re-homing must be an SDK patch, not a
|
|
87
|
+
breaking change for consumers.
|
|
88
|
+
- **Zero is not the default.** `phiMin` and `alpha` of `0n` resolve to their defaults at *read* time,
|
|
89
|
+
so a key carrying `0n` and one carrying the resolved value are **different markets with different
|
|
90
|
+
ids**. Always address a market by the key it was initialized with.
|
|
91
|
+
- **Split every ERC-6909 `Transfer` on bit 255.** Clear means a token claim, not a position; an
|
|
92
|
+
indexer that ignores this reports phantom LP positions for market makers parking inventory.
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm i @logswap/sdk viem
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Published as **`@logswap/sdk`** from v0.5.35. Everything up to v0.5.34 was
|
|
101
|
+
`@unimodular/logswap-sdk`, which is deprecated and points here — the code is identical, only the
|
|
102
|
+
name moved, along with the repos to the `logswap` org.
|
|
103
|
+
|
|
104
|
+
`viem` is a peer dependency. The package ships `dist/` plus `src/`, including the generated ABIs, so
|
|
105
|
+
the contract surface it was built against travels with it.
|
|
106
|
+
|
|
107
|
+
## Release
|
|
108
|
+
|
|
109
|
+
Push-driven, and there is **no npm token anywhere**: publishing uses OIDC trusted publishing, so npm
|
|
110
|
+
verifies a GitHub-issued token per run and there is no long-lived credential to rotate or leak.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
logswap-contract push
|
|
114
|
+
→ Update SDK contracts pin (GitHub App token, cross-repo)
|
|
115
|
+
→ commit contracts.ref here
|
|
116
|
+
→ Release on contracts pin change
|
|
117
|
+
→ codegen at the pinned SHA → build → test → version bump → npm publish --provenance
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`workflow_dispatch` covers SDK-only fixes with no contracts change: same pipeline, manual trigger.
|
|
121
|
+
The version bump touches `package.json` only, not `contracts.ref`, so it cannot re-trigger itself.
|
|
122
|
+
|
|
123
|
+
Two things that are easy to get wrong:
|
|
124
|
+
|
|
125
|
+
- **`codegen` must run before `typecheck`.** `src/generated.ts` is gitignored, so it exists on a
|
|
126
|
+
developer machine and never in CI — a `typecheck`-first pipeline fails only in CI, which is where
|
|
127
|
+
it is least convenient to debug.
|
|
128
|
+
- **`0.1.0` has no provenance attestation, every release after does.** Trusted publishing cannot be
|
|
129
|
+
configured for a package that does not exist yet, so the first publish is necessarily manual.
|
|
130
|
+
`prepublishOnly` runs the build, so a hand-run publish cannot ship a stale `dist/`.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* actions.ts — the router's transaction: a BATCH (decisions 010).
|
|
3
|
+
*
|
|
4
|
+
* `execute(actions, settle)` runs a list of actions — mint, burn, update, claim, swap, path, in any
|
|
5
|
+
* order, across any C markets — under ONE manager lock. Each action accrues deltas on the router
|
|
6
|
+
* and settles nothing; the batch settles once per token in `settle.tokens`, pulling at most
|
|
7
|
+
* `maxIn[i]` from the caller and taking at least `minOut[i]` to the recipient. A touched token
|
|
8
|
+
* left off the list fails the manager's own close (`TokensNotSettled`), so the list is also the
|
|
9
|
+
* caller's statement of which tokens may move.
|
|
10
|
+
*
|
|
11
|
+
* `OPEN_DELTA` as a swap amount means "the router's open delta of that token": what earlier
|
|
12
|
+
* actions left it holding (exact-in, path) or owing (exact-out). It is what lets action i+1
|
|
13
|
+
* consume action i's proceeds — a harvest funds a buy, a burn exits to one asset, a deepen is paid
|
|
14
|
+
* in base — each moving ONE token.
|
|
15
|
+
*
|
|
16
|
+
* Payloads are `abi.encode` of the router's `*Act` structs. Their tuple types are read from the
|
|
17
|
+
* generated ABI (the router's pure `encode*` helpers exist for exactly this), so nothing here is
|
|
18
|
+
* hand-written and a struct change is a `tsc`/vector failure, not a runtime surprise.
|
|
19
|
+
*/
|
|
20
|
+
import { type Address, type Hash, type Hex } from "viem";
|
|
21
|
+
import type { LogswapClient } from "./client.js";
|
|
22
|
+
import type { PoolKey } from "./keys.js";
|
|
23
|
+
import type { Hop } from "./swap.js";
|
|
24
|
+
import { type WriteOptions } from "./write.js";
|
|
25
|
+
/** "The router's open delta of that token" — the swap-amount sentinel. `type(uint256).max`. */
|
|
26
|
+
export declare const OPEN_DELTA: bigint;
|
|
27
|
+
/** No bound. The same word as the sentinel, kept apart because it means something else. */
|
|
28
|
+
export declare const UNBOUNDED: bigint;
|
|
29
|
+
/** The router's op tags — `OpType` in `RouterOps.sol`, C actions only (the F ops have their own lock). */
|
|
30
|
+
export declare enum Op {
|
|
31
|
+
MintBlended = 0,
|
|
32
|
+
Burn = 1,
|
|
33
|
+
SwapExactIn = 2,
|
|
34
|
+
SwapExactOut = 3,
|
|
35
|
+
Claim = 4,
|
|
36
|
+
Update = 5,
|
|
37
|
+
Path = 6
|
|
38
|
+
}
|
|
39
|
+
export interface Action {
|
|
40
|
+
op: Op;
|
|
41
|
+
data: Hex;
|
|
42
|
+
}
|
|
43
|
+
export interface Settle {
|
|
44
|
+
tokens: readonly Address[];
|
|
45
|
+
maxIn: readonly bigint[];
|
|
46
|
+
minOut: readonly bigint[];
|
|
47
|
+
/** Receives whatever is taken, and the shares the actions mint. */
|
|
48
|
+
recipient: Address;
|
|
49
|
+
}
|
|
50
|
+
/** Mint exposure `L` at `targetFloor` (off-grid blends the two adjacent rungs), `cap` optional. */
|
|
51
|
+
export declare function mintAction(a: {
|
|
52
|
+
key: PoolKey;
|
|
53
|
+
targetFloor: bigint;
|
|
54
|
+
cap?: bigint;
|
|
55
|
+
L: bigint;
|
|
56
|
+
}): Action;
|
|
57
|
+
/** Burn `shares[i]` of `ids[i]`; accrued fees are swept into the same batch. Needs the operator grant. */
|
|
58
|
+
export declare function burnAction(a: {
|
|
59
|
+
key: PoolKey;
|
|
60
|
+
ids: readonly bigint[];
|
|
61
|
+
shares: readonly bigint[];
|
|
62
|
+
}): Action;
|
|
63
|
+
/** Exact-in swap. `amountIn` may be `OPEN_DELTA`: the input an earlier action left the router holding. */
|
|
64
|
+
export declare function swapExactInAction(a: {
|
|
65
|
+
key: PoolKey;
|
|
66
|
+
baseIn: boolean;
|
|
67
|
+
amountIn: bigint;
|
|
68
|
+
minOut?: bigint;
|
|
69
|
+
}): Action;
|
|
70
|
+
/** Exact-out swap. `amountOut` may be `OPEN_DELTA`: the output an earlier action left the router owing. */
|
|
71
|
+
export declare function swapExactOutAction(a: {
|
|
72
|
+
key: PoolKey;
|
|
73
|
+
baseOut: boolean;
|
|
74
|
+
amountOut: bigint;
|
|
75
|
+
maxIn?: bigint;
|
|
76
|
+
}): Action;
|
|
77
|
+
/** Collect the caller's fees on `ids`; positions untouched. */
|
|
78
|
+
export declare function claimAction(a: {
|
|
79
|
+
key: PoolKey;
|
|
80
|
+
ids: readonly bigint[];
|
|
81
|
+
}): Action;
|
|
82
|
+
/** The general edit — set `shares` of `fromId` to (`toFloor`, `toCap`, `newL`); `newL = 0` exits. */
|
|
83
|
+
export declare function updateAction(a: {
|
|
84
|
+
key: PoolKey;
|
|
85
|
+
fromId: bigint;
|
|
86
|
+
shares: bigint;
|
|
87
|
+
toFloor: bigint;
|
|
88
|
+
toCap?: bigint;
|
|
89
|
+
newL: bigint;
|
|
90
|
+
}): Action;
|
|
91
|
+
/** Multi-hop exact-in route; `amountIn` may be `OPEN_DELTA`. Intermediate tokens never move. */
|
|
92
|
+
export declare function pathAction(a: {
|
|
93
|
+
hops: readonly Hop[];
|
|
94
|
+
amountIn: bigint;
|
|
95
|
+
minOut?: bigint;
|
|
96
|
+
}): Action;
|
|
97
|
+
/** A settlement over explicit tokens; missing bounds default to unbounded in, zero out. */
|
|
98
|
+
export declare function settlement(a: {
|
|
99
|
+
tokens: readonly Address[];
|
|
100
|
+
maxIn?: readonly bigint[];
|
|
101
|
+
minOut?: readonly bigint[];
|
|
102
|
+
recipient: Address;
|
|
103
|
+
}): Settle;
|
|
104
|
+
export interface PairBounds {
|
|
105
|
+
maxBaseIn?: bigint;
|
|
106
|
+
maxQuoteIn?: bigint;
|
|
107
|
+
minBaseOut?: bigint;
|
|
108
|
+
minQuoteOut?: bigint;
|
|
109
|
+
}
|
|
110
|
+
/** Settle both of a market's tokens, in kind. */
|
|
111
|
+
export declare function settleInKind(key: PoolKey, recipient: Address, b?: PairBounds): Settle;
|
|
112
|
+
/**
|
|
113
|
+
* Settle in ONE of a market's tokens: the other is bound to zero both ways, so the batch must have
|
|
114
|
+
* netted it — which is what a trailing `OPEN_DELTA` swap does.
|
|
115
|
+
*/
|
|
116
|
+
export declare function settleOnly(key: PoolKey, recipient: Address, side: "base" | "quote", b?: {
|
|
117
|
+
maxIn?: bigint;
|
|
118
|
+
minOut?: bigint;
|
|
119
|
+
}): Settle;
|
|
120
|
+
export interface ExecuteArgs extends WriteOptions {
|
|
121
|
+
actions: readonly Action[];
|
|
122
|
+
settle: Omit<Settle, "recipient"> & {
|
|
123
|
+
recipient?: Address;
|
|
124
|
+
};
|
|
125
|
+
/** Tokens the batch may pull, for the Permit2 signatures. Defaults to every token with `maxIn > 0`. */
|
|
126
|
+
pullTokens?: readonly Address[];
|
|
127
|
+
}
|
|
128
|
+
/** Run a batch: one lock, one settlement per token, one transaction (Permit2 signatures ride inside). */
|
|
129
|
+
export declare function execute(c: LogswapClient, a: ExecuteArgs): Promise<Hash>;
|
|
130
|
+
/** Simulate a batch: the per-action returns and the signed flows (+ = paid in), without sending. */
|
|
131
|
+
export declare function simulateExecute(c: LogswapClient, a: ExecuteArgs & {
|
|
132
|
+
account?: Address;
|
|
133
|
+
}): Promise<{
|
|
134
|
+
results: readonly Hex[];
|
|
135
|
+
flows: readonly bigint[];
|
|
136
|
+
}>;
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* actions.ts — the router's transaction: a BATCH (decisions 010).
|
|
3
|
+
*
|
|
4
|
+
* `execute(actions, settle)` runs a list of actions — mint, burn, update, claim, swap, path, in any
|
|
5
|
+
* order, across any C markets — under ONE manager lock. Each action accrues deltas on the router
|
|
6
|
+
* and settles nothing; the batch settles once per token in `settle.tokens`, pulling at most
|
|
7
|
+
* `maxIn[i]` from the caller and taking at least `minOut[i]` to the recipient. A touched token
|
|
8
|
+
* left off the list fails the manager's own close (`TokensNotSettled`), so the list is also the
|
|
9
|
+
* caller's statement of which tokens may move.
|
|
10
|
+
*
|
|
11
|
+
* `OPEN_DELTA` as a swap amount means "the router's open delta of that token": what earlier
|
|
12
|
+
* actions left it holding (exact-in, path) or owing (exact-out). It is what lets action i+1
|
|
13
|
+
* consume action i's proceeds — a harvest funds a buy, a burn exits to one asset, a deepen is paid
|
|
14
|
+
* in base — each moving ONE token.
|
|
15
|
+
*
|
|
16
|
+
* Payloads are `abi.encode` of the router's `*Act` structs. Their tuple types are read from the
|
|
17
|
+
* generated ABI (the router's pure `encode*` helpers exist for exactly this), so nothing here is
|
|
18
|
+
* hand-written and a struct change is a `tsc`/vector failure, not a runtime surprise.
|
|
19
|
+
*/
|
|
20
|
+
import { encodeAbiParameters } from "viem";
|
|
21
|
+
import { logswapRouterAbi } from "./generated.js";
|
|
22
|
+
import { NO_CAP } from "./ids.js";
|
|
23
|
+
import { resolveDeadline, resolveRecipient, sendRouterWriteWithPermits, simulateRouterWrite } from "./write.js";
|
|
24
|
+
/** "The router's open delta of that token" — the swap-amount sentinel. `type(uint256).max`. */
|
|
25
|
+
export const OPEN_DELTA = (1n << 256n) - 1n;
|
|
26
|
+
/** No bound. The same word as the sentinel, kept apart because it means something else. */
|
|
27
|
+
export const UNBOUNDED = (1n << 256n) - 1n;
|
|
28
|
+
/** The router's op tags — `OpType` in `RouterOps.sol`, C actions only (the F ops have their own lock). */
|
|
29
|
+
export var Op;
|
|
30
|
+
(function (Op) {
|
|
31
|
+
Op[Op["MintBlended"] = 0] = "MintBlended";
|
|
32
|
+
Op[Op["Burn"] = 1] = "Burn";
|
|
33
|
+
Op[Op["SwapExactIn"] = 2] = "SwapExactIn";
|
|
34
|
+
Op[Op["SwapExactOut"] = 3] = "SwapExactOut";
|
|
35
|
+
Op[Op["Claim"] = 4] = "Claim";
|
|
36
|
+
Op[Op["Update"] = 5] = "Update";
|
|
37
|
+
Op[Op["Path"] = 6] = "Path";
|
|
38
|
+
})(Op || (Op = {}));
|
|
39
|
+
/* payload encoding ========================================================== */
|
|
40
|
+
function payloadType(encoder) {
|
|
41
|
+
const f = logswapRouterAbi.find((x) => x.type === "function" && x.name === encoder);
|
|
42
|
+
if (!f?.inputs)
|
|
43
|
+
throw new Error(`logswap: the router ABI has no ${encoder} — SDK and contracts disagree`);
|
|
44
|
+
return f.inputs;
|
|
45
|
+
}
|
|
46
|
+
const enc = (encoder, value) => encodeAbiParameters(payloadType(encoder), [value]);
|
|
47
|
+
/** Mint exposure `L` at `targetFloor` (off-grid blends the two adjacent rungs), `cap` optional. */
|
|
48
|
+
export function mintAction(a) {
|
|
49
|
+
return { op: Op.MintBlended, data: enc("encodeMint", { key: a.key, targetFloor: a.targetFloor, cap: a.cap ?? NO_CAP, L: a.L }) };
|
|
50
|
+
}
|
|
51
|
+
/** Burn `shares[i]` of `ids[i]`; accrued fees are swept into the same batch. Needs the operator grant. */
|
|
52
|
+
export function burnAction(a) {
|
|
53
|
+
if (a.ids.length !== a.shares.length)
|
|
54
|
+
throw new Error("logswap: ids and shares length mismatch");
|
|
55
|
+
return { op: Op.Burn, data: enc("encodeBurn", { key: a.key, ids: a.ids, shares: a.shares }) };
|
|
56
|
+
}
|
|
57
|
+
/** Exact-in swap. `amountIn` may be `OPEN_DELTA`: the input an earlier action left the router holding. */
|
|
58
|
+
export function swapExactInAction(a) {
|
|
59
|
+
return { op: Op.SwapExactIn, data: enc("encodeSwap", { key: a.key, baseSide: a.baseIn, amount: a.amountIn, limit: a.minOut ?? 0n }) };
|
|
60
|
+
}
|
|
61
|
+
/** Exact-out swap. `amountOut` may be `OPEN_DELTA`: the output an earlier action left the router owing. */
|
|
62
|
+
export function swapExactOutAction(a) {
|
|
63
|
+
return {
|
|
64
|
+
op: Op.SwapExactOut,
|
|
65
|
+
data: enc("encodeSwap", { key: a.key, baseSide: a.baseOut, amount: a.amountOut, limit: a.maxIn ?? UNBOUNDED }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Collect the caller's fees on `ids`; positions untouched. */
|
|
69
|
+
export function claimAction(a) {
|
|
70
|
+
return { op: Op.Claim, data: enc("encodeClaim", { key: a.key, ids: a.ids }) };
|
|
71
|
+
}
|
|
72
|
+
/** The general edit — set `shares` of `fromId` to (`toFloor`, `toCap`, `newL`); `newL = 0` exits. */
|
|
73
|
+
export function updateAction(a) {
|
|
74
|
+
return {
|
|
75
|
+
op: Op.Update,
|
|
76
|
+
data: enc("encodeUpdate", {
|
|
77
|
+
key: a.key,
|
|
78
|
+
fromId: a.fromId,
|
|
79
|
+
shares: a.shares,
|
|
80
|
+
toFloor: a.toFloor,
|
|
81
|
+
toCap: a.toCap ?? NO_CAP,
|
|
82
|
+
newL: a.newL,
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Multi-hop exact-in route; `amountIn` may be `OPEN_DELTA`. Intermediate tokens never move. */
|
|
87
|
+
export function pathAction(a) {
|
|
88
|
+
if (a.hops.length === 0)
|
|
89
|
+
throw new Error("logswap: empty path");
|
|
90
|
+
return { op: Op.Path, data: enc("encodePath", { hops: a.hops, amountIn: a.amountIn, minOut: a.minOut ?? 0n }) };
|
|
91
|
+
}
|
|
92
|
+
/* settlement builders ======================================================= */
|
|
93
|
+
/** A settlement over explicit tokens; missing bounds default to unbounded in, zero out. */
|
|
94
|
+
export function settlement(a) {
|
|
95
|
+
const n = a.tokens.length;
|
|
96
|
+
const maxIn = a.maxIn ?? a.tokens.map(() => UNBOUNDED);
|
|
97
|
+
const minOut = a.minOut ?? a.tokens.map(() => 0n);
|
|
98
|
+
if (maxIn.length !== n || minOut.length !== n)
|
|
99
|
+
throw new Error("logswap: settlement bounds length mismatch");
|
|
100
|
+
return { tokens: a.tokens, maxIn, minOut, recipient: a.recipient };
|
|
101
|
+
}
|
|
102
|
+
/** Settle both of a market's tokens, in kind. */
|
|
103
|
+
export function settleInKind(key, recipient, b = {}) {
|
|
104
|
+
return settlement({
|
|
105
|
+
tokens: [key.base, key.quote],
|
|
106
|
+
maxIn: [b.maxBaseIn ?? UNBOUNDED, b.maxQuoteIn ?? UNBOUNDED],
|
|
107
|
+
minOut: [b.minBaseOut ?? 0n, b.minQuoteOut ?? 0n],
|
|
108
|
+
recipient,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Settle in ONE of a market's tokens: the other is bound to zero both ways, so the batch must have
|
|
113
|
+
* netted it — which is what a trailing `OPEN_DELTA` swap does.
|
|
114
|
+
*/
|
|
115
|
+
export function settleOnly(key, recipient, side, b = {}) {
|
|
116
|
+
const base = side === "base";
|
|
117
|
+
return settlement({
|
|
118
|
+
tokens: [key.base, key.quote],
|
|
119
|
+
maxIn: [base ? (b.maxIn ?? UNBOUNDED) : 0n, base ? 0n : (b.maxIn ?? UNBOUNDED)],
|
|
120
|
+
minOut: [base ? (b.minOut ?? 0n) : 0n, base ? 0n : (b.minOut ?? 0n)],
|
|
121
|
+
recipient,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** Run a batch: one lock, one settlement per token, one transaction (Permit2 signatures ride inside). */
|
|
125
|
+
export async function execute(c, a) {
|
|
126
|
+
const settle = { ...a.settle, recipient: a.settle.recipient ?? resolveRecipient(c, a) };
|
|
127
|
+
const pull = a.pullTokens ?? settle.tokens.filter((_, i) => settle.maxIn[i] > 0n);
|
|
128
|
+
return sendRouterWriteWithPermits(c, { abi: logswapRouterAbi, functionName: "execute", args: [a.actions, settle, resolveDeadline(a)] }, [...pull]);
|
|
129
|
+
}
|
|
130
|
+
/** Simulate a batch: the per-action returns and the signed flows (+ = paid in), without sending. */
|
|
131
|
+
export async function simulateExecute(c, a) {
|
|
132
|
+
const settle = { ...a.settle, recipient: a.settle.recipient ?? a.account ?? resolveRecipient(c, a) };
|
|
133
|
+
const [results, flows] = await simulateRouterWrite(c, {
|
|
134
|
+
abi: logswapRouterAbi,
|
|
135
|
+
functionName: "execute",
|
|
136
|
+
args: [a.actions, settle, resolveDeadline(a)],
|
|
137
|
+
account: a.account,
|
|
138
|
+
});
|
|
139
|
+
return { results, flows };
|
|
140
|
+
}
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin — the protocol's (feeCollector's) writes: the fee rates, the harvest-fee exemption, and
|
|
3
|
+
* the two-step collector handover, on both managers. Plain writes to the managers; no router.
|
|
4
|
+
*/
|
|
5
|
+
import type { Address, Hash, Hex } from "viem";
|
|
6
|
+
import type { LogswapClient } from "./client.js";
|
|
7
|
+
export declare function setProtocolFeeCut(c: LogswapClient, cutWad: bigint): Promise<Hash>;
|
|
8
|
+
export declare function setFeeParams(c: LogswapClient, mintFeeProtocolCutWad: bigint, harvestFeeWad: bigint): Promise<Hash>;
|
|
9
|
+
export declare function setHarvestFeeExempt(c: LogswapClient, poolId: Hex, exempt: boolean): Promise<Hash>;
|
|
10
|
+
export declare function proposeCollector(c: LogswapClient, side: "C" | "F", next: Address): Promise<Hash>;
|
|
11
|
+
export declare function acceptCollector(c: LogswapClient, side: "C" | "F"): Promise<Hash>;
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin — the protocol's (feeCollector's) writes: the fee rates, the harvest-fee exemption, and
|
|
3
|
+
* the two-step collector handover, on both managers. Plain writes to the managers; no router.
|
|
4
|
+
*/
|
|
5
|
+
import { cPoolManagerAbi, fPoolManagerAbi } from "./generated.js";
|
|
6
|
+
function wallet(c) {
|
|
7
|
+
if (!c.wallet)
|
|
8
|
+
throw new Error("a wallet client is required");
|
|
9
|
+
return c.wallet;
|
|
10
|
+
}
|
|
11
|
+
export async function setProtocolFeeCut(c, cutWad) {
|
|
12
|
+
return wallet(c).writeContract({ address: c.addresses.cPoolManager, abi: cPoolManagerAbi, functionName: "setProtocolFeeCut", args: [cutWad], chain: c.wallet.chain, account: c.wallet.account });
|
|
13
|
+
}
|
|
14
|
+
export async function setFeeParams(c, mintFeeProtocolCutWad, harvestFeeWad) {
|
|
15
|
+
return wallet(c).writeContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName: "setFeeParams", args: [mintFeeProtocolCutWad, harvestFeeWad], chain: c.wallet.chain, account: c.wallet.account });
|
|
16
|
+
}
|
|
17
|
+
export async function setHarvestFeeExempt(c, poolId, exempt) {
|
|
18
|
+
return wallet(c).writeContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName: "setHarvestFeeExempt", args: [poolId, exempt], chain: c.wallet.chain, account: c.wallet.account });
|
|
19
|
+
}
|
|
20
|
+
export async function proposeCollector(c, side, next) {
|
|
21
|
+
const address = side === "C" ? c.addresses.cPoolManager : c.addresses.fPoolManager;
|
|
22
|
+
const abi = side === "C" ? cPoolManagerAbi : fPoolManagerAbi;
|
|
23
|
+
return wallet(c).writeContract({ address, abi, functionName: "proposeCollector", args: [next], chain: c.wallet.chain, account: c.wallet.account });
|
|
24
|
+
}
|
|
25
|
+
export async function acceptCollector(c, side) {
|
|
26
|
+
const address = side === "C" ? c.addresses.cPoolManager : c.addresses.fPoolManager;
|
|
27
|
+
const abi = side === "C" ? cPoolManagerAbi : fPoolManagerAbi;
|
|
28
|
+
return wallet(c).writeContract({ address, abi, functionName: "acceptCollector", args: [], chain: c.wallet.chain, account: c.wallet.account });
|
|
29
|
+
}
|
package/dist/claims.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claims.ts — fee claims, and the ERC-6909 operator grant every router write depends on.
|
|
3
|
+
*
|
|
4
|
+
* Note the two unrelated meanings of "claim" in this protocol, kept apart here:
|
|
5
|
+
* - a FEE claim — `claimFees`, withdrawing accrued quote while the position stays whole
|
|
6
|
+
* - a TOKEN claim — a bearer IOU (bit-255-clear id) for inventory parked inside the manager
|
|
7
|
+
*/
|
|
8
|
+
import type { Address, Hash } from "viem";
|
|
9
|
+
import type { LogswapClient } from "./client.js";
|
|
10
|
+
import type { PoolKey } from "./keys.js";
|
|
11
|
+
import { type WriteOptions } from "./write.js";
|
|
12
|
+
export interface ClaimFeesArgs extends WriteOptions {
|
|
13
|
+
key: PoolKey;
|
|
14
|
+
ids: readonly bigint[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Claim accrued fees across one or more classes: one transfer, positions untouched.
|
|
18
|
+
*
|
|
19
|
+
* **Every id must be REGISTERED** — one the manager has seen minted. An id for a class that has
|
|
20
|
+
* never existed reverts the whole batch with `WrongPool`, because an unregistered id has
|
|
21
|
+
* `poolId == 0` and cannot be shown to belong to this pool. In a contract holding every market's
|
|
22
|
+
* assets, "cannot be shown to belong" must not be read as "belongs". A class that exists but which
|
|
23
|
+
* the caller does not hold is fine — that is a no-op, not a revert.
|
|
24
|
+
*/
|
|
25
|
+
export declare function claimFees(c: LogswapClient, a: ClaimFeesArgs): Promise<Hash>;
|
|
26
|
+
/**
|
|
27
|
+
* Grant the router operator rights over your ERC-6909 positions.
|
|
28
|
+
*
|
|
29
|
+
* **Required before any burn, update, move or harvest** — those pull the shares to the router
|
|
30
|
+
* first. Without it they revert on the transfer, which reads as an unrelated balance error. Swaps
|
|
31
|
+
* and mints do not need it.
|
|
32
|
+
*
|
|
33
|
+
* One grant covers every market, because the positions all live in the one manager.
|
|
34
|
+
*/
|
|
35
|
+
export declare function approveRouterAsOperator(c: LogswapClient, approved?: boolean): Promise<Hash>;
|
|
36
|
+
/** Is the router already an operator for `owner`? Check before prompting for a signature. */
|
|
37
|
+
export declare function isRouterApproved(c: LogswapClient, owner: Address): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* What a wallet still has to approve before a given action.
|
|
40
|
+
*
|
|
41
|
+
* Surfacing this up front is the difference between one prompt and a failed transaction: the ERC-20
|
|
42
|
+
* allowances and the 6909 operator grant are separate approvals with separate failure modes.
|
|
43
|
+
*/
|
|
44
|
+
export interface Readiness {
|
|
45
|
+
needsOperator: boolean;
|
|
46
|
+
needsBaseAllowance: boolean;
|
|
47
|
+
needsQuoteAllowance: boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare function checkReadiness(c: LogswapClient, key: PoolKey, owner: Address, need?: {
|
|
50
|
+
base?: bigint;
|
|
51
|
+
quote?: bigint;
|
|
52
|
+
positions?: boolean;
|
|
53
|
+
}): Promise<Readiness>;
|
package/dist/claims.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claims.ts — fee claims, and the ERC-6909 operator grant every router write depends on.
|
|
3
|
+
*
|
|
4
|
+
* Note the two unrelated meanings of "claim" in this protocol, kept apart here:
|
|
5
|
+
* - a FEE claim — `claimFees`, withdrawing accrued quote while the position stays whole
|
|
6
|
+
* - a TOKEN claim — a bearer IOU (bit-255-clear id) for inventory parked inside the manager
|
|
7
|
+
*/
|
|
8
|
+
import { cPoolManagerAbi, logswapRouterAbi } from "./generated.js";
|
|
9
|
+
import { isPosition } from "./ids.js";
|
|
10
|
+
import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite } from "./write.js";
|
|
11
|
+
/**
|
|
12
|
+
* Claim accrued fees across one or more classes: one transfer, positions untouched.
|
|
13
|
+
*
|
|
14
|
+
* **Every id must be REGISTERED** — one the manager has seen minted. An id for a class that has
|
|
15
|
+
* never existed reverts the whole batch with `WrongPool`, because an unregistered id has
|
|
16
|
+
* `poolId == 0` and cannot be shown to belong to this pool. In a contract holding every market's
|
|
17
|
+
* assets, "cannot be shown to belong" must not be read as "belongs". A class that exists but which
|
|
18
|
+
* the caller does not hold is fine — that is a no-op, not a revert.
|
|
19
|
+
*/
|
|
20
|
+
export async function claimFees(c, a) {
|
|
21
|
+
for (const id of a.ids) {
|
|
22
|
+
if (!isPosition(id)) {
|
|
23
|
+
throw new Error(`logswap: id ${id} is a TOKEN claim (bit 255 clear), not a position — it has no fees to claim.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return sendRouterWrite(c, {
|
|
27
|
+
abi: logswapRouterAbi,
|
|
28
|
+
functionName: "claim",
|
|
29
|
+
args: [a.key, a.ids, resolveRecipient(c, a), resolveDeadline(a)],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Grant the router operator rights over your ERC-6909 positions.
|
|
34
|
+
*
|
|
35
|
+
* **Required before any burn, update, move or harvest** — those pull the shares to the router
|
|
36
|
+
* first. Without it they revert on the transfer, which reads as an unrelated balance error. Swaps
|
|
37
|
+
* and mints do not need it.
|
|
38
|
+
*
|
|
39
|
+
* One grant covers every market, because the positions all live in the one manager.
|
|
40
|
+
*/
|
|
41
|
+
export async function approveRouterAsOperator(c, approved = true) {
|
|
42
|
+
const { account } = requireWallet(c);
|
|
43
|
+
return c.wallet.writeContract({
|
|
44
|
+
address: c.addresses.cPoolManager,
|
|
45
|
+
abi: cPoolManagerAbi,
|
|
46
|
+
functionName: "setOperator",
|
|
47
|
+
args: [c.addresses.router, approved],
|
|
48
|
+
account,
|
|
49
|
+
chain: c.wallet.chain,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Is the router already an operator for `owner`? Check before prompting for a signature. */
|
|
53
|
+
export async function isRouterApproved(c, owner) {
|
|
54
|
+
return c.public.readContract({
|
|
55
|
+
address: c.addresses.cPoolManager,
|
|
56
|
+
abi: cPoolManagerAbi,
|
|
57
|
+
functionName: "isOperator",
|
|
58
|
+
args: [owner, c.addresses.router],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export async function checkReadiness(c, key, owner, need = {}) {
|
|
62
|
+
const erc20 = [
|
|
63
|
+
{
|
|
64
|
+
type: "function",
|
|
65
|
+
name: "allowance",
|
|
66
|
+
stateMutability: "view",
|
|
67
|
+
inputs: [
|
|
68
|
+
{ name: "owner", type: "address" },
|
|
69
|
+
{ name: "spender", type: "address" },
|
|
70
|
+
],
|
|
71
|
+
outputs: [{ type: "uint256" }],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
const [operator, baseAllow, quoteAllow] = await Promise.all([
|
|
75
|
+
need.positions ? isRouterApproved(c, owner) : Promise.resolve(true),
|
|
76
|
+
need.base
|
|
77
|
+
? c.public.readContract({
|
|
78
|
+
address: key.base,
|
|
79
|
+
abi: erc20,
|
|
80
|
+
functionName: "allowance",
|
|
81
|
+
args: [owner, c.addresses.router],
|
|
82
|
+
})
|
|
83
|
+
: Promise.resolve(0n),
|
|
84
|
+
need.quote
|
|
85
|
+
? c.public.readContract({
|
|
86
|
+
address: key.quote,
|
|
87
|
+
abi: erc20,
|
|
88
|
+
functionName: "allowance",
|
|
89
|
+
args: [owner, c.addresses.router],
|
|
90
|
+
})
|
|
91
|
+
: Promise.resolve(0n),
|
|
92
|
+
]);
|
|
93
|
+
return {
|
|
94
|
+
needsOperator: !operator,
|
|
95
|
+
needsBaseAllowance: need.base !== undefined && baseAllow < need.base,
|
|
96
|
+
needsQuoteAllowance: need.quote !== undefined && quoteAllow < need.quote,
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* client.ts — the SDK handle: addresses + viem clients.
|
|
3
|
+
*
|
|
4
|
+
* Construct once and pass to every helper. `wallet` is optional: read helpers need only `public`.
|
|
5
|
+
*
|
|
6
|
+
* **Three addresses, permanently.** Not three per market — every market lives in the one manager,
|
|
7
|
+
* and a market is an `initialize(key, x0)` call rather than a deployment. So this is a constant of
|
|
8
|
+
* the protocol, not configuration that grows.
|
|
9
|
+
*/
|
|
10
|
+
import type { Address, PublicClient, WalletClient } from "viem";
|
|
11
|
+
export interface LogswapAddresses {
|
|
12
|
+
/** The C singleton — a floor per position, on a tick ladder. ERC-6909 positions and token
|
|
13
|
+
* claims live here. */
|
|
14
|
+
cPoolManager: Address;
|
|
15
|
+
/** User entry for every market: multicall, blended mint, zap, routes, Permit2. */
|
|
16
|
+
router: Address;
|
|
17
|
+
/** Read-only: ladder derivations and revert-quoter previews. */
|
|
18
|
+
lens: Address;
|
|
19
|
+
/**
|
|
20
|
+
* The F-pool singleton — fungible claims, one shared floor, n bases. Optional only because a
|
|
21
|
+
* deployment predating it will not carry the address; every F-pool call needs it.
|
|
22
|
+
*/
|
|
23
|
+
fPoolManager?: Address;
|
|
24
|
+
/** Optional — the router falls back to plain approvals when absent. */
|
|
25
|
+
permit2?: Address;
|
|
26
|
+
}
|
|
27
|
+
export interface LogswapClient {
|
|
28
|
+
public: PublicClient;
|
|
29
|
+
wallet?: WalletClient;
|
|
30
|
+
addresses: LogswapAddresses;
|
|
31
|
+
}
|
|
32
|
+
export declare function createLogswapClient(args: {
|
|
33
|
+
public: PublicClient;
|
|
34
|
+
wallet?: WalletClient;
|
|
35
|
+
addresses: LogswapAddresses;
|
|
36
|
+
}): LogswapClient;
|
|
37
|
+
/**
|
|
38
|
+
* Assert the deployment matches the ABI this build was generated against.
|
|
39
|
+
*
|
|
40
|
+
* Cheap insurance worth taking at construction: without it, a manager from a different deployment
|
|
41
|
+
* decodes into plausible-looking garbage rather than failing. `version` is bumped on any tier-1 or
|
|
42
|
+
* tier-2 change (docs/app.md).
|
|
43
|
+
*
|
|
44
|
+
* **2** since 2026-09-01. It sat at 1 across a read surface that changed underneath it — the nine
|
|
45
|
+
* `public constant` getters left the manager for `CParams`, `move` gained its settlement bounds,
|
|
46
|
+
* `createMarket` arrived — so a 1 answered for two incompatible ABIs and this check passed against
|
|
47
|
+
* a manager it should have rejected. Observed, not theorised: a local chain running a pre-trim
|
|
48
|
+
* manager sailed through `assertVersion` while `FEE_MAX()` still answered on it. Move this in the
|
|
49
|
+
* same change as `CParams.VERSION`, never after it.
|
|
50
|
+
*
|
|
51
|
+
* **3** since 2026-09-03 (contracts `d8406fb`): `burnById` and `update` take an `owner`, `update`
|
|
52
|
+
* mints to `to`, `positions()` returns `aps` as uint128 — and a returned quote flow now carries the
|
|
53
|
+
* swept fees (decisions 018), which is why `exit`'s settle-to-one-asset math changed with it.
|
|
54
|
+
*/
|
|
55
|
+
export declare const EXPECTED_VERSION = 3n;
|
|
56
|
+
/**
|
|
57
|
+
* The F manager's own tag (contracts 25a7bcf: 5 — the key's salt, pool names, the sponsor registry;
|
|
58
|
+
* 4 was the income-only harvest cut).
|
|
59
|
+
*
|
|
60
|
+
* A FLOOR, not an equality — unlike `EXPECTED_VERSION` above. The F primitive is iterating fast and
|
|
61
|
+
* its bumps are mostly additive (6 is `multicall`, which nothing this SDK calls goes near), so an
|
|
62
|
+
* exact match meant each one bricked every app on the deployment until the SDK was republished — a
|
|
63
|
+
* version wall in front of a change that broke nothing. The cost is that a genuinely breaking bump
|
|
64
|
+
* passes it; when one lands, raise this constant in the same change, which is the discipline
|
|
65
|
+
* `EXPECTED_VERSION` enforces mechanically and this one asks for.
|
|
66
|
+
*/
|
|
67
|
+
export declare const EXPECTED_F_VERSION = 5n;
|
|
68
|
+
export declare function assertVersion(c: LogswapClient, expected?: bigint): Promise<void>;
|