@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/src/swap.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* swap.ts — trading.
|
|
3
|
+
*
|
|
4
|
+
* All four shapes go through the router, which is where slippage and deadline live. The manager's
|
|
5
|
+
* merged `swap(key, baseSide, amount, limit)` uses a NEGATIVE amount for exact-out; that convention
|
|
6
|
+
* is deliberately not exposed here — callers say `swapExactOut`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Address, Hash } from "viem";
|
|
10
|
+
import { logswapLensAbi, logswapRouterAbi } from "./generated.js";
|
|
11
|
+
import type { LogswapClient } from "./client.js";
|
|
12
|
+
import type { PoolKey } from "./keys.js";
|
|
13
|
+
import { resolveDeadline, resolveRecipient, sendRouterWrite, type WriteOptions , sendRouterWriteWithPermits } from "./write.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Quote an exact-in swap. Exact, not approximate — the lens is a revert-quoter that runs the real
|
|
17
|
+
* swap and reverts with the result, so what it returns is what execution returns, bit for bit
|
|
18
|
+
* (including the dynamic fee, even across a block boundary with the variance fold pending).
|
|
19
|
+
*
|
|
20
|
+
* `simulate`, not a plain read: the function is non-view by construction.
|
|
21
|
+
*/
|
|
22
|
+
export async function quoteExactIn(
|
|
23
|
+
c: LogswapClient,
|
|
24
|
+
key: PoolKey,
|
|
25
|
+
baseIn: boolean,
|
|
26
|
+
amountIn: bigint,
|
|
27
|
+
): Promise<bigint> {
|
|
28
|
+
const { result } = await c.public.simulateContract({
|
|
29
|
+
address: c.addresses.lens,
|
|
30
|
+
abi: logswapLensAbi,
|
|
31
|
+
functionName: "previewSwapExactIn",
|
|
32
|
+
args: [key, baseIn, amountIn],
|
|
33
|
+
});
|
|
34
|
+
return result as bigint;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Quote an exact-out swap: how much input it will cost. Exact, same mechanism as above. */
|
|
38
|
+
export async function quoteExactOut(
|
|
39
|
+
c: LogswapClient,
|
|
40
|
+
key: PoolKey,
|
|
41
|
+
baseOut: boolean,
|
|
42
|
+
amountOut: bigint,
|
|
43
|
+
): Promise<bigint> {
|
|
44
|
+
const { result } = await c.public.simulateContract({
|
|
45
|
+
address: c.addresses.lens,
|
|
46
|
+
abi: logswapLensAbi,
|
|
47
|
+
functionName: "previewSwapExactOut",
|
|
48
|
+
args: [key, baseOut, amountOut],
|
|
49
|
+
});
|
|
50
|
+
return result as bigint;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SwapExactInArgs extends WriteOptions {
|
|
54
|
+
key: PoolKey;
|
|
55
|
+
/** true = base in, quote out (price falls). false = quote in, base out (price rises). */
|
|
56
|
+
baseIn: boolean;
|
|
57
|
+
amountIn: bigint;
|
|
58
|
+
/** Reverts below this. Derive from a quote with `withSlippage(q, bps, "output")`. */
|
|
59
|
+
minOut: bigint;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function swapExactIn(c: LogswapClient, a: SwapExactInArgs): Promise<Hash> {
|
|
63
|
+
return sendRouterWriteWithPermits(c, {
|
|
64
|
+
abi: logswapRouterAbi,
|
|
65
|
+
functionName: "swapExactIn",
|
|
66
|
+
args: [a.key, a.baseIn, a.amountIn, a.minOut, resolveRecipient(c, a), resolveDeadline(a)],
|
|
67
|
+
}, [a.baseIn ? a.key.base : a.key.quote]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SwapExactOutArgs extends WriteOptions {
|
|
71
|
+
key: PoolKey;
|
|
72
|
+
/** true = base out (price rises). false = quote out (price falls). */
|
|
73
|
+
baseOut: boolean;
|
|
74
|
+
amountOut: bigint;
|
|
75
|
+
/** Reverts above this. Derive from a quote with `withSlippage(q, bps, "input")`. */
|
|
76
|
+
maxIn: bigint;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function swapExactOut(c: LogswapClient, a: SwapExactOutArgs): Promise<Hash> {
|
|
80
|
+
return sendRouterWriteWithPermits(c, {
|
|
81
|
+
abi: logswapRouterAbi,
|
|
82
|
+
functionName: "swapExactOut",
|
|
83
|
+
args: [a.key, a.baseOut, a.amountOut, a.maxIn, resolveRecipient(c, a), resolveDeadline(a)],
|
|
84
|
+
}, [a.baseOut ? a.key.quote : a.key.base]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One leg of a route. `baseIn` is that hop's direction through its own market. */
|
|
88
|
+
export interface Hop {
|
|
89
|
+
key: PoolKey;
|
|
90
|
+
baseIn: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SwapPathArgs extends WriteOptions {
|
|
94
|
+
hops: readonly Hop[];
|
|
95
|
+
amountIn: bigint;
|
|
96
|
+
minOut: bigint;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Multi-hop, all inside ONE lock.
|
|
101
|
+
*
|
|
102
|
+
* The singleton dividend: deltas accrue per TOKEN across the whole route, so an intermediate asset
|
|
103
|
+
* is never actually moved — a three-market route transfers only the tokens at the endpoints. The
|
|
104
|
+
* path is not validated on-chain, and it does not need to be: a mis-chained hop leaves a dangling
|
|
105
|
+
* delta and the lock refuses to close.
|
|
106
|
+
*/
|
|
107
|
+
export async function swapExactInPath(c: LogswapClient, a: SwapPathArgs): Promise<Hash> {
|
|
108
|
+
if (a.hops.length === 0) throw new Error("logswap: empty path");
|
|
109
|
+
return sendRouterWrite(c, {
|
|
110
|
+
abi: logswapRouterAbi,
|
|
111
|
+
functionName: "swapExactInPath",
|
|
112
|
+
args: [
|
|
113
|
+
a.hops.map((h) => ({ key: h.key, baseIn: h.baseIn })),
|
|
114
|
+
a.amountIn,
|
|
115
|
+
a.minOut,
|
|
116
|
+
resolveRecipient(c, a),
|
|
117
|
+
resolveDeadline(a),
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Which token a hop takes in and pays out — the check a UI should run before building a route, so
|
|
124
|
+
* a mis-chained path fails in the form rather than at the lock.
|
|
125
|
+
*/
|
|
126
|
+
export function hopTokens(h: Hop): { tokenIn: Address; tokenOut: Address } {
|
|
127
|
+
return h.baseIn
|
|
128
|
+
? { tokenIn: h.key.base, tokenOut: h.key.quote }
|
|
129
|
+
: { tokenIn: h.key.quote, tokenOut: h.key.base };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True when every hop's output feeds the next hop's input. */
|
|
133
|
+
export function isPathChained(hops: readonly Hop[]): boolean {
|
|
134
|
+
for (let i = 1; i < hops.length; i++) {
|
|
135
|
+
const prev = hopTokens(hops[i - 1]!);
|
|
136
|
+
const next = hopTokens(hops[i]!);
|
|
137
|
+
if (prev.tokenOut.toLowerCase() !== next.tokenIn.toLowerCase()) return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tokencache.ts — remember token metadata so it is read once, not once per screen.
|
|
3
|
+
*
|
|
4
|
+
* `marketTokens` costs four reads per market and every screen calls it. On two markets that is
|
|
5
|
+
* free; on fifty it is a hundred round trips on every navigation, over a tunnel.
|
|
6
|
+
*
|
|
7
|
+
* **A cache over a value that becomes a transaction amount is a way to be wrong, so three rules
|
|
8
|
+
* hold here and are enforced by tests:**
|
|
9
|
+
*
|
|
10
|
+
* 1. **Only successes are stored.** The previous fix made a failed `decimals()` read throw rather
|
|
11
|
+
* than assume 18; caching a failure would reintroduce that bug with a longer half-life.
|
|
12
|
+
* 2. **The key includes the chain id.** The same address is a different token on a different
|
|
13
|
+
* chain — a mock USDC at 18 decimals on a devnet, the real one at 6 on mainnet. Keying on
|
|
14
|
+
* address alone is the classic version of this bug and it survives a page reload.
|
|
15
|
+
* 3. **A fallback symbol is never stored.** `symbol` degrades to `0x5FbD…` on failure, which is
|
|
16
|
+
* fine as a label but must not be persisted — one blip would name the token that forever.
|
|
17
|
+
*
|
|
18
|
+
* 4. **A resettable chain never reaches the persistent store.** Rule 3's premise is that decimals
|
|
19
|
+
* are immutable *for the life of a token*, and the key's premise is that an address names one
|
|
20
|
+
* token forever. Both hold on a public chain. Neither holds on a local one: restart anvil and
|
|
21
|
+
* the very same deterministic addresses come back holding DIFFERENT contracts. That is not
|
|
22
|
+
* hypothetical — a `logswap.token.31337:0x5fbd…` entry saying USDC has 18 decimals outlived the
|
|
23
|
+
* deploy that wrote it and made every quote-denominated number in the app read as zero, with
|
|
24
|
+
* nothing anywhere reporting a fault. Those chains get the memory store, so a page reload is
|
|
25
|
+
* enough to correct them.
|
|
26
|
+
*
|
|
27
|
+
* Decimals are immutable for the life of a token: ERC-20 exposes no way to change them, and a token
|
|
28
|
+
* that did would break every integration, not just this one. That is what makes an unbounded TTL
|
|
29
|
+
* correct rather than merely convenient — on a chain whose addresses are permanent.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { Address } from "viem";
|
|
33
|
+
|
|
34
|
+
export interface CachedToken {
|
|
35
|
+
symbol: string;
|
|
36
|
+
decimals: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Somewhere to keep entries. Sync and tiny on purpose — `localStorage` fits, and so does a Map. */
|
|
40
|
+
export interface TokenStore {
|
|
41
|
+
get(key: string): string | null;
|
|
42
|
+
set(key: string, value: string): void;
|
|
43
|
+
/** Drop everything this store owns. Optional: a store that cannot enumerate may omit it. */
|
|
44
|
+
clear?(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const memory = new Map<string, string>();
|
|
48
|
+
|
|
49
|
+
/** The default: per-process, no persistence. Works in node, workers and a browser alike. */
|
|
50
|
+
export const memoryStore: TokenStore = {
|
|
51
|
+
get: (k) => memory.get(k) ?? null,
|
|
52
|
+
set: (k, v) => void memory.set(k, v),
|
|
53
|
+
clear: () => memory.clear(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `localStorage`, when there is one. Returns the memory store otherwise, so a caller never has to
|
|
58
|
+
* branch on the environment — and a browser in private mode degrades instead of throwing.
|
|
59
|
+
*/
|
|
60
|
+
export function browserStore(prefix = "logswap.token."): TokenStore {
|
|
61
|
+
try {
|
|
62
|
+
if (typeof localStorage === "undefined") return memoryStore;
|
|
63
|
+
localStorage.getItem(`${prefix}probe`); // Safari private mode throws on access, not on use
|
|
64
|
+
return {
|
|
65
|
+
get: (k) => {
|
|
66
|
+
try {
|
|
67
|
+
return localStorage.getItem(prefix + k);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
set: (k, v) => {
|
|
73
|
+
try {
|
|
74
|
+
localStorage.setItem(prefix + k, v);
|
|
75
|
+
} catch {
|
|
76
|
+
/* quota or private mode — the cache is an optimisation, never a requirement */
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
// Only this prefix, never the whole of localStorage: the app's own keys live there too.
|
|
80
|
+
clear: () => {
|
|
81
|
+
try {
|
|
82
|
+
for (const k of Object.keys(localStorage)) if (k.startsWith(prefix)) localStorage.removeItem(k);
|
|
83
|
+
} catch {
|
|
84
|
+
/* nothing to do — see set */
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return memoryStore;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let store: TokenStore = memoryStore;
|
|
94
|
+
|
|
95
|
+
/** Swap the backing store. Call once at startup; `browserStore()` is the usual argument. */
|
|
96
|
+
export function setTokenStore(s: TokenStore): void {
|
|
97
|
+
store = s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Address casing varies by source, so normalise — otherwise the same token gets two entries. */
|
|
101
|
+
const keyOf = (chainId: number, address: Address) => `${chainId}:${address.toLowerCase()}`;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Chains whose addresses are not permanent identities: a development node can be wiped and
|
|
105
|
+
* redeployed, and because deployment addresses are derived from a deterministic nonce sequence the
|
|
106
|
+
* SAME address comes back holding a different contract. 1337 is ganache's and hardhat's legacy id,
|
|
107
|
+
* 31337 anvil's and hardhat's default, 31338 and 31339 the unimod and logswap devnets — both
|
|
108
|
+
* persisted with `--state`, both documented as resettable by deleting that file.
|
|
109
|
+
*
|
|
110
|
+
* Add to this set rather than removing from it: a chain wrongly listed here costs some RPC round
|
|
111
|
+
* trips, while one wrongly absent silently serves a stale scale, which is the bug this exists for.
|
|
112
|
+
*/
|
|
113
|
+
export const RESETTABLE_CHAINS: ReadonlySet<number> = new Set([1337, 31337, 31338, 31339]);
|
|
114
|
+
|
|
115
|
+
/** True when a `(chainId, address)` pair cannot be trusted to name one contract forever. */
|
|
116
|
+
export function isResettableChain(chainId: number): boolean {
|
|
117
|
+
return RESETTABLE_CHAINS.has(chainId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The store an entry for this chain may use. A resettable chain gets memory — good enough to stop
|
|
122
|
+
* one navigation re-reading the same token fifty times, and gone on reload, which is exactly the
|
|
123
|
+
* lifetime over which its addresses can be trusted.
|
|
124
|
+
*/
|
|
125
|
+
const storeFor = (chainId: number): TokenStore => (isResettableChain(chainId) ? memoryStore : store);
|
|
126
|
+
|
|
127
|
+
export function readCachedToken(chainId: number, address: Address): CachedToken | null {
|
|
128
|
+
const raw = storeFor(chainId).get(keyOf(chainId, address));
|
|
129
|
+
if (!raw) return null;
|
|
130
|
+
try {
|
|
131
|
+
const v = JSON.parse(raw) as CachedToken;
|
|
132
|
+
// Validate on the way OUT as well as in: the store is shared with other tabs and other builds,
|
|
133
|
+
// and a corrupt entry must be ignored rather than trusted.
|
|
134
|
+
if (typeof v?.decimals !== "number" || !Number.isInteger(v.decimals) || v.decimals < 0 || v.decimals > 36) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (typeof v.symbol !== "string" || v.symbol === "") return null;
|
|
138
|
+
return v;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function writeCachedToken(chainId: number, address: Address, t: CachedToken): void {
|
|
145
|
+
storeFor(chainId).set(keyOf(chainId, address), JSON.stringify({ symbol: t.symbol, decimals: t.decimals }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Forget everything, including reads still in flight.
|
|
150
|
+
*
|
|
151
|
+
* For tests, and for a devnet that redeployed its mocks to the same addresses — the one case where
|
|
152
|
+
* a token at a known address legitimately changes its decimals, because it is a different contract
|
|
153
|
+
* wearing the same address.
|
|
154
|
+
*/
|
|
155
|
+
export function clearTokenCache(): void {
|
|
156
|
+
memory.clear();
|
|
157
|
+
// The persistent store too. It did not used to be, which made this function useless against the
|
|
158
|
+
// one situation its own doc names: the entry that survives a redeploy lives in `localStorage`,
|
|
159
|
+
// so clearing only memory left it in place and the next read served it straight back.
|
|
160
|
+
store.clear?.();
|
|
161
|
+
onClear.forEach((f) => f());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const onClear = new Set<() => void>();
|
|
165
|
+
|
|
166
|
+
/** Internal: lets `pools.ts` drop its in-flight map when the cache is cleared. */
|
|
167
|
+
export function registerCacheClear(f: () => void): void {
|
|
168
|
+
onClear.add(f);
|
|
169
|
+
}
|
package/src/write.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write.ts — shared plumbing for every mutating call.
|
|
3
|
+
*
|
|
4
|
+
* Two conventions every write helper inherits, so no caller has to remember them:
|
|
5
|
+
*
|
|
6
|
+
* **Deadlines are mandatory on-chain and defaulted here.** Every mutating router entry carries
|
|
7
|
+
* `checkDeadline`. Passing `type(uint256).max` disables it, which is what most SDKs do implicitly
|
|
8
|
+
* and is a real hazard for a user whose transaction sits in the mempool through a price move. The
|
|
9
|
+
* default is a bounded window; pass `deadline: NEVER` only deliberately.
|
|
10
|
+
*
|
|
11
|
+
* **Slippage is expressed as a bound, never as a percentage of a stale quote.** Helpers take the
|
|
12
|
+
* raw `minOut`/`maxIn` the contract wants. `withSlippage()` derives one from a quote when that is
|
|
13
|
+
* what you have — but the bound is what ships.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Address, Hash } from "viem";
|
|
17
|
+
import type { LogswapClient } from "./client.js";
|
|
18
|
+
|
|
19
|
+
/** Disables the deadline check. Deliberate opt-out, never a default. */
|
|
20
|
+
export const NEVER = (1n << 256n) - 1n;
|
|
21
|
+
|
|
22
|
+
/** Default deadline window: 20 minutes, the same order as every major DEX front end. */
|
|
23
|
+
export const DEFAULT_DEADLINE_SECONDS = 20n * 60n;
|
|
24
|
+
|
|
25
|
+
export interface WriteOptions {
|
|
26
|
+
/** Absolute unix seconds. Defaults to now + {@link DEFAULT_DEADLINE_SECONDS}. */
|
|
27
|
+
deadline?: bigint;
|
|
28
|
+
/** Who receives the output. Defaults to the wallet account. */
|
|
29
|
+
recipient?: Address;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveDeadline(o: WriteOptions | undefined, now = Date.now()): bigint {
|
|
33
|
+
return o?.deadline ?? BigInt(Math.floor(now / 1000)) + DEFAULT_DEADLINE_SECONDS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The wallet's account, as the ACCOUNT OBJECT rather than its address.
|
|
38
|
+
*
|
|
39
|
+
* This distinction decides how the transaction is signed, and getting it wrong fails in only half
|
|
40
|
+
* the cases — which is how it survived a long time here. viem treats a bare `0x…` address as a
|
|
41
|
+
* *JSON-RPC account* and calls `eth_sendTransaction`, asking the NODE to sign. That is right for an
|
|
42
|
+
* injected wallet and wrong for a local account, which must sign in-process and send
|
|
43
|
+
* `eth_sendRawTransaction`.
|
|
44
|
+
*
|
|
45
|
+
* A dev node holds the keys for its own accounts, so passing an address appears to work right up
|
|
46
|
+
* until someone uses a key the node does not have — a bot, a script, CI, or a real user's wallet.
|
|
47
|
+
* Passing the object lets viem pick the correct path for both kinds.
|
|
48
|
+
*/
|
|
49
|
+
export function requireWallet(c: LogswapClient): { account: NonNullable<LogswapClient["wallet"]>["account"]; address: Address } {
|
|
50
|
+
const account = c.wallet?.account;
|
|
51
|
+
if (!c.wallet || !account) {
|
|
52
|
+
throw new Error("logswap: this call needs a wallet client — createLogswapClient({ wallet, … })");
|
|
53
|
+
}
|
|
54
|
+
return { account, address: account.address };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveRecipient(c: LogswapClient, o?: WriteOptions): Address {
|
|
58
|
+
return o?.recipient ?? requireWallet(c).address;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Turn a quoted amount into a slippage bound.
|
|
63
|
+
*
|
|
64
|
+
* `bps` is basis points of tolerance. Direction matters: an output bound floors, an input bound
|
|
65
|
+
* ceilings. Getting that backwards produces a bound that can never bind, which is worse than no
|
|
66
|
+
* bound at all because it looks protective.
|
|
67
|
+
*/
|
|
68
|
+
export function withSlippage(quoted: bigint, bps: number, side: "output" | "input"): bigint {
|
|
69
|
+
if (bps < 0 || bps > 10_000) throw new Error(`logswap: slippage bps out of range: ${bps}`);
|
|
70
|
+
const b = BigInt(Math.round(bps));
|
|
71
|
+
return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b)) / 10_000n;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Send a router write with a gas buffer over an explicit estimate.
|
|
76
|
+
*
|
|
77
|
+
* A swap's gas is path-dependent: how many ticks it crosses depends on the price when it LANDS, not
|
|
78
|
+
* when it was estimated, and the variance kernel does more work when a block boundary has passed
|
|
79
|
+
* since the last touch. An exact `eth_estimateGas` can therefore OutOfGas one block later. Every
|
|
80
|
+
* write goes through here.
|
|
81
|
+
*/
|
|
82
|
+
export async function sendRouterWrite(
|
|
83
|
+
c: LogswapClient,
|
|
84
|
+
args: { abi: readonly unknown[]; functionName: string; args: readonly unknown[]; bufferPct?: bigint },
|
|
85
|
+
): Promise<Hash> {
|
|
86
|
+
const { account } = requireWallet(c);
|
|
87
|
+
const params = {
|
|
88
|
+
address: c.addresses.router,
|
|
89
|
+
abi: args.abi,
|
|
90
|
+
functionName: args.functionName,
|
|
91
|
+
args: args.args,
|
|
92
|
+
account,
|
|
93
|
+
chain: c.wallet!.chain,
|
|
94
|
+
} as never;
|
|
95
|
+
|
|
96
|
+
const estimate = await c.public.estimateContractGas(params);
|
|
97
|
+
const gas = (estimate * (100n + (args.bufferPct ?? 30n))) / 100n;
|
|
98
|
+
return c.wallet!.writeContract({ ...(params as object), gas } as never);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Simulate a router write and return its decoded result, without sending. */
|
|
102
|
+
export async function simulateRouterWrite<T>(
|
|
103
|
+
c: LogswapClient,
|
|
104
|
+
args: { abi: readonly unknown[]; functionName: string; args: readonly unknown[]; account?: Address },
|
|
105
|
+
): Promise<T> {
|
|
106
|
+
const account = args.account ?? c.wallet?.account?.address;
|
|
107
|
+
const { result } = await c.public.simulateContract({
|
|
108
|
+
address: c.addresses.router,
|
|
109
|
+
abi: args.abi,
|
|
110
|
+
functionName: args.functionName,
|
|
111
|
+
args: args.args,
|
|
112
|
+
account,
|
|
113
|
+
} as never);
|
|
114
|
+
return result as T;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A router write that carries its own Permit2 permissions: missing allowances for `pullTokens`
|
|
119
|
+
* are SIGNED (no gas, no extra transaction) and submitted as `applyPermit2` calls in one
|
|
120
|
+
* multicall with the action. The 3-transaction mint becomes 1. Falls through to a plain write
|
|
121
|
+
* when nothing is missing or Permit2 is not wired.
|
|
122
|
+
*/
|
|
123
|
+
export async function sendRouterWriteWithPermits(
|
|
124
|
+
c: LogswapClient,
|
|
125
|
+
args: { abi: readonly unknown[]; functionName: string; args: readonly unknown[]; bufferPct?: bigint },
|
|
126
|
+
pullTokens: Address[],
|
|
127
|
+
): Promise<Hash> {
|
|
128
|
+
const { permit2SigCalls } = await import("./onboard.js");
|
|
129
|
+
const sigCalls = await permit2SigCalls(c, pullTokens).catch(() => []);
|
|
130
|
+
if (sigCalls.length === 0) return sendRouterWrite(c, args);
|
|
131
|
+
const { encodeFunctionData } = await import("viem");
|
|
132
|
+
const inner = encodeFunctionData({ abi: args.abi, functionName: args.functionName, args: args.args } as never);
|
|
133
|
+
return sendRouterWrite(c, { abi: args.abi, functionName: "multicall", args: [[...sigCalls, inner]], bufferPct: args.bufferPct });
|
|
134
|
+
}
|