sally-defi-ts-sdk 0.3.2
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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/aio/client.d.ts +93 -0
- package/dist/aio/client.d.ts.map +1 -0
- package/dist/aio/client.js +283 -0
- package/dist/aio/client.js.map +1 -0
- package/dist/aio/index.d.ts +20 -0
- package/dist/aio/index.d.ts.map +1 -0
- package/dist/aio/index.js +19 -0
- package/dist/aio/index.js.map +1 -0
- package/dist/aio/modules/fees.d.ts +19 -0
- package/dist/aio/modules/fees.d.ts.map +1 -0
- package/dist/aio/modules/fees.js +47 -0
- package/dist/aio/modules/fees.js.map +1 -0
- package/dist/aio/modules/liquidity.d.ts +47 -0
- package/dist/aio/modules/liquidity.d.ts.map +1 -0
- package/dist/aio/modules/liquidity.js +115 -0
- package/dist/aio/modules/liquidity.js.map +1 -0
- package/dist/aio/modules/prices.d.ts +18 -0
- package/dist/aio/modules/prices.d.ts.map +1 -0
- package/dist/aio/modules/prices.js +48 -0
- package/dist/aio/modules/prices.js.map +1 -0
- package/dist/aio/modules/swap.d.ts +50 -0
- package/dist/aio/modules/swap.d.ts.map +1 -0
- package/dist/aio/modules/swap.js +267 -0
- package/dist/aio/modules/swap.js.map +1 -0
- package/dist/aio/modules/wallet.d.ts +13 -0
- package/dist/aio/modules/wallet.d.ts.map +1 -0
- package/dist/aio/modules/wallet.js +27 -0
- package/dist/aio/modules/wallet.js.map +1 -0
- package/dist/aio/token.d.ts +19 -0
- package/dist/aio/token.d.ts.map +1 -0
- package/dist/aio/token.js +50 -0
- package/dist/aio/token.js.map +1 -0
- package/dist/client.d.ts +142 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +452 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +36 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +39 -0
- package/dist/constants.js.map +1 -0
- package/dist/data/deployment.json +1 -0
- package/dist/deployment.d.ts +44 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +118 -0
- package/dist/deployment.js.map +1 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +197 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/fees.d.ts +32 -0
- package/dist/modules/fees.d.ts.map +1 -0
- package/dist/modules/fees.js +64 -0
- package/dist/modules/fees.js.map +1 -0
- package/dist/modules/liquidity.d.ts +134 -0
- package/dist/modules/liquidity.d.ts.map +1 -0
- package/dist/modules/liquidity.js +277 -0
- package/dist/modules/liquidity.js.map +1 -0
- package/dist/modules/prices.d.ts +47 -0
- package/dist/modules/prices.d.ts.map +1 -0
- package/dist/modules/prices.js +85 -0
- package/dist/modules/prices.js.map +1 -0
- package/dist/modules/swap.d.ts +102 -0
- package/dist/modules/swap.d.ts.map +1 -0
- package/dist/modules/swap.js +400 -0
- package/dist/modules/swap.js.map +1 -0
- package/dist/modules/wallet.d.ts +16 -0
- package/dist/modules/wallet.d.ts.map +1 -0
- package/dist/modules/wallet.js +30 -0
- package/dist/modules/wallet.js.map +1 -0
- package/dist/permit2.d.ts +97 -0
- package/dist/permit2.d.ts.map +1 -0
- package/dist/permit2.js +130 -0
- package/dist/permit2.js.map +1 -0
- package/dist/previews.d.ts +57 -0
- package/dist/previews.d.ts.map +1 -0
- package/dist/previews.js +69 -0
- package/dist/previews.js.map +1 -0
- package/dist/safety.d.ts +80 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +133 -0
- package/dist/safety.js.map +1 -0
- package/dist/token.d.ts +215 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +239 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +229 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +462 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +22 -0
- package/dist/util.js.map +1 -0
- package/package.json +48 -0
- package/src/aio/client.ts +329 -0
- package/src/aio/index.ts +20 -0
- package/src/aio/modules/fees.ts +60 -0
- package/src/aio/modules/liquidity.ts +181 -0
- package/src/aio/modules/prices.ts +57 -0
- package/src/aio/modules/swap.ts +347 -0
- package/src/aio/modules/wallet.ts +34 -0
- package/src/aio/token.ts +59 -0
- package/src/client.ts +526 -0
- package/src/constants.ts +43 -0
- package/src/data/deployment.json +1 -0
- package/src/deployment.ts +132 -0
- package/src/errors.ts +215 -0
- package/src/index.ts +90 -0
- package/src/modules/fees.ts +78 -0
- package/src/modules/liquidity.ts +446 -0
- package/src/modules/prices.ts +97 -0
- package/src/modules/swap.ts +502 -0
- package/src/modules/wallet.ts +37 -0
- package/src/permit2.ts +169 -0
- package/src/previews.ts +95 -0
- package/src/safety.ts +152 -0
- package/src/token.ts +254 -0
- package/src/types.ts +438 -0
- package/src/util.ts +20 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid swaps — quote, compare, simulate, guard, execute.
|
|
3
|
+
*
|
|
4
|
+
* The high-level {@link Swap.execute} does the safe thing by default:
|
|
5
|
+
*
|
|
6
|
+
* 1. **approve** the input (exact amount, not infinite) to the swap proxy,
|
|
7
|
+
* 2. **enumerate** candidate routes (best / deep / per-version),
|
|
8
|
+
* 3. **integrity-check** each so funds can never enter a wrong/mismatched pool,
|
|
9
|
+
* 4. **simulate** every candidate on-chain (`eth_call` of the real
|
|
10
|
+
* `executeHybridSwap`, which returns the realized output) and pick the best,
|
|
11
|
+
* 5. **honeypot/tax preflight** the output token via the `getTokenInfos` probe,
|
|
12
|
+
* 6. set `minOut` from the *simulated* output and a slippage floor,
|
|
13
|
+
* 7. send, then **assert the received balance grew by >= minOut** (trust the chain).
|
|
14
|
+
*
|
|
15
|
+
* Use {@link Swap.plan} to get the same vetted {@link SwapPlan} without sending.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Contract, getAddress } from "ethers";
|
|
19
|
+
import type { SallyClient } from "../client.js";
|
|
20
|
+
import { SallyError, SallyRouteError, SallySafetyError } from "../errors.js";
|
|
21
|
+
import { RouteCandidate, SafetyConfig, SwapPlan } from "../safety.js";
|
|
22
|
+
import { MAX_UINT256, ZERO, isNative } from "../token.js";
|
|
23
|
+
import { SwapPath, TokenInfo } from "../types.js";
|
|
24
|
+
import { pyRound } from "../util.js";
|
|
25
|
+
|
|
26
|
+
const MAX_DEADLINE = 2n ** 256n - 1n;
|
|
27
|
+
|
|
28
|
+
export interface ExecuteOptions {
|
|
29
|
+
slippageBps?: number | null;
|
|
30
|
+
minOutput?: bigint | null;
|
|
31
|
+
path?: SwapPath | null;
|
|
32
|
+
referral?: string;
|
|
33
|
+
deadline?: number | null;
|
|
34
|
+
config?: SafetyConfig | null;
|
|
35
|
+
approval?: "approve" | "permit" | "none";
|
|
36
|
+
force?: boolean;
|
|
37
|
+
buildOnly?: boolean;
|
|
38
|
+
tx?: Record<string, any>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Swap {
|
|
42
|
+
private _c: SallyClient;
|
|
43
|
+
private _swap: Contract;
|
|
44
|
+
private _wnative: string | null = null;
|
|
45
|
+
|
|
46
|
+
constructor(client: SallyClient) {
|
|
47
|
+
this._c = client;
|
|
48
|
+
this._swap = client.swapContract;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private _a(x: string): string {
|
|
52
|
+
return getAddress(x);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async wnative(): Promise<string> {
|
|
56
|
+
if (this._wnative === null) {
|
|
57
|
+
this._wnative = String(await this._c.call(this._swap.getFunction("wnative"), []));
|
|
58
|
+
}
|
|
59
|
+
return this._wnative;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// -- quoting ----------------------------------------------------------- //
|
|
63
|
+
/** Best cross-version route for `amountIn`. `getBestSwapPath`. */
|
|
64
|
+
async quote(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
65
|
+
return SwapPath.fromRaw(
|
|
66
|
+
await this._c.call(this._swap.getFunction("getBestSwapPath"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async quoteV2(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
71
|
+
return SwapPath.fromRaw(
|
|
72
|
+
await this._c.call(this._swap.getFunction("getSwapPathV2"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async quoteV3(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
77
|
+
return SwapPath.fromRaw(
|
|
78
|
+
await this._c.call(this._swap.getFunction("getSwapPathV3"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async quoteV4(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
83
|
+
return SwapPath.fromRaw(
|
|
84
|
+
await this._c.call(this._swap.getFunction("getSwapPathV4"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Deeper, more exhaustive route search. `getBestSwapPathDeep` (Lens). */
|
|
89
|
+
async quoteDeep(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
90
|
+
return SwapPath.fromRaw(
|
|
91
|
+
await this._c.call(this._c.lensContract.getFunction("getBestSwapPathDeep"), [
|
|
92
|
+
this._a(tokenIn),
|
|
93
|
+
this._a(tokenOut),
|
|
94
|
+
amountIn,
|
|
95
|
+
]),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -- token safety probe ------------------------------------------------ //
|
|
100
|
+
/** Honeypot / buy-sell-tax round-trip probe. `getTokenInfos`. */
|
|
101
|
+
async tokenInfo(token: string, opts: { probeValue?: bigint | null } = {}): Promise<TokenInfo> {
|
|
102
|
+
let probeValue = opts.probeValue ?? null;
|
|
103
|
+
if (probeValue === null) probeValue = new SafetyConfig().probeValue;
|
|
104
|
+
const raw = await this._swap.getFunction("getTokenInfos").staticCall(this._a(token), {
|
|
105
|
+
from: ZERO,
|
|
106
|
+
value: probeValue,
|
|
107
|
+
});
|
|
108
|
+
return TokenInfo.fromRaw(raw);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// -- protocol config reads -------------------------------------------- //
|
|
112
|
+
async perHopSlippageBps(): Promise<number> {
|
|
113
|
+
return Number(await this._c.call(this._swap.getFunction("perHopSlippageBps"), []));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Stablecoins the protocol prices against. `getUsdStables`. */
|
|
117
|
+
async usdStables(): Promise<string[]> {
|
|
118
|
+
const r = await this._c.call(this._swap.getFunction("getUsdStables"), []);
|
|
119
|
+
return [...r].map((a) => String(a));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async poolManagerV4(): Promise<string> {
|
|
123
|
+
return String(await this._c.call(this._swap.getFunction("poolManagerV4"), []));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// -- helpers ----------------------------------------------------------- //
|
|
127
|
+
/**
|
|
128
|
+
* Slippage floor: `amountOut * (1 - bps/10000)`.
|
|
129
|
+
*
|
|
130
|
+
* Clamped to >= 1 when the output is positive, so integer flooring on a tiny
|
|
131
|
+
* output can never produce `minOut == 0` (which would disable the on-chain
|
|
132
|
+
* slippage guard and expose the swap to a 100% sandwich).
|
|
133
|
+
*/
|
|
134
|
+
static minOut(amountOut: bigint | SwapPath | null, slippageBps: number): bigint {
|
|
135
|
+
const out: bigint = amountOut instanceof SwapPath ? amountOut.estimatedAmountOut : (amountOut ?? 0n);
|
|
136
|
+
const floor = (out * BigInt(10_000 - slippageBps)) / 10_000n;
|
|
137
|
+
return out > 0n ? (floor > 1n ? floor : 1n) : 0n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
minOut(amountOut: bigint | SwapPath | null, slippageBps: number): bigint {
|
|
141
|
+
return Swap.minOut(amountOut, slippageBps);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
deadline(secs: number | null = null): number {
|
|
145
|
+
// Wall-clock based (like the Uniswap SDK): robust even if the node reports a
|
|
146
|
+
// stale `latest` block while mining ahead.
|
|
147
|
+
const s = secs !== null ? secs : new SafetyConfig().deadlineSecs;
|
|
148
|
+
return Math.floor(Date.now() / 1000) + s;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** All distinct quoted routes (best / deep / per-version), deduped by pools. */
|
|
152
|
+
async candidates(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<RouteCandidate[]> {
|
|
153
|
+
tokenIn = this._a(tokenIn);
|
|
154
|
+
tokenOut = this._a(tokenOut);
|
|
155
|
+
const out: RouteCandidate[] = [];
|
|
156
|
+
const seen = new Set<string>();
|
|
157
|
+
const fns: [string, (a: string, b: string, c: bigint) => Promise<SwapPath>][] = [
|
|
158
|
+
["best", (a, b, c) => this.quote(a, b, c)],
|
|
159
|
+
["deep", (a, b, c) => this.quoteDeep(a, b, c)],
|
|
160
|
+
["v2", (a, b, c) => this.quoteV2(a, b, c)],
|
|
161
|
+
["v3", (a, b, c) => this.quoteV3(a, b, c)],
|
|
162
|
+
["v4", (a, b, c) => this.quoteV4(a, b, c)],
|
|
163
|
+
];
|
|
164
|
+
for (const [label, fn] of fns) {
|
|
165
|
+
let route: SwapPath;
|
|
166
|
+
try {
|
|
167
|
+
route = await fn(tokenIn, tokenOut, amountIn);
|
|
168
|
+
} catch (exc) {
|
|
169
|
+
if (exc instanceof SallyError) continue;
|
|
170
|
+
throw exc;
|
|
171
|
+
}
|
|
172
|
+
if (route.stepCount === 0) continue;
|
|
173
|
+
const key = JSON.stringify([
|
|
174
|
+
route.poolAddresses.map((p) => p.toLowerCase()),
|
|
175
|
+
route.stepCount,
|
|
176
|
+
route.estimatedAmountOut.toString(),
|
|
177
|
+
]);
|
|
178
|
+
if (seen.has(key)) continue;
|
|
179
|
+
seen.add(key);
|
|
180
|
+
out.push(new RouteCandidate(label, route, route.estimatedAmountOut));
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `eth_call` the real `executeHybridSwap` and return realized output.
|
|
187
|
+
*
|
|
188
|
+
* Returns `null` if the route reverts. Requires `sender` to hold the input
|
|
189
|
+
* token + allowance (or `value` for native in) — exactly what `execute`
|
|
190
|
+
* arranges before planning.
|
|
191
|
+
*/
|
|
192
|
+
async simulateRoute(
|
|
193
|
+
route: SwapPath,
|
|
194
|
+
amountIn: bigint,
|
|
195
|
+
opts: { value?: bigint; sender?: string | null } = {},
|
|
196
|
+
): Promise<bigint | null> {
|
|
197
|
+
const value = opts.value ?? 0n;
|
|
198
|
+
const sender = opts.sender ?? this._c.address;
|
|
199
|
+
if (sender == null) return null;
|
|
200
|
+
try {
|
|
201
|
+
const r = await this._swap.getFunction("executeHybridSwap").staticCall(
|
|
202
|
+
route.toTuple(),
|
|
203
|
+
amountIn,
|
|
204
|
+
0,
|
|
205
|
+
MAX_DEADLINE,
|
|
206
|
+
ZERO,
|
|
207
|
+
{ from: sender, value },
|
|
208
|
+
);
|
|
209
|
+
return BigInt(r);
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Impact vs the marginal (small-trade) price, in bps. null if unknown. */
|
|
216
|
+
private async _priceImpactBps(
|
|
217
|
+
tokenIn: string,
|
|
218
|
+
tokenOut: string,
|
|
219
|
+
amountIn: bigint,
|
|
220
|
+
realized: bigint,
|
|
221
|
+
): Promise<number | null> {
|
|
222
|
+
const probe = amountIn / 1000n > 1n ? amountIn / 1000n : 1n;
|
|
223
|
+
let probeOut: bigint;
|
|
224
|
+
try {
|
|
225
|
+
probeOut = (await this.quote(tokenIn, tokenOut, probe)).estimatedAmountOut;
|
|
226
|
+
} catch (exc) {
|
|
227
|
+
if (exc instanceof SallyError) return null;
|
|
228
|
+
throw exc;
|
|
229
|
+
}
|
|
230
|
+
if (probeOut <= 0n || realized <= 0n) return null;
|
|
231
|
+
const expected = (probeOut * amountIn) / probe;
|
|
232
|
+
if (expected <= 0n) return null;
|
|
233
|
+
return Math.max(0, pyRound((Number(expected - realized) / Number(expected)) * 10_000));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Honeypot/tax probe on the output token + price-impact check.
|
|
238
|
+
*
|
|
239
|
+
* Returns `[tokenSafety, priceImpactBps, blockReasons, warnings]`. Shared by
|
|
240
|
+
* `plan` and the supplied-path branch of `execute` so neither is a safety bypass.
|
|
241
|
+
*/
|
|
242
|
+
private async _safetyPreflight(
|
|
243
|
+
quoteIn: string,
|
|
244
|
+
tokenOut: string,
|
|
245
|
+
amountIn: bigint,
|
|
246
|
+
realized: bigint,
|
|
247
|
+
cfg: SafetyConfig,
|
|
248
|
+
): Promise<[TokenInfo | null, number | null, string[], string[]]> {
|
|
249
|
+
const blocks: string[] = [];
|
|
250
|
+
const warns: string[] = [];
|
|
251
|
+
let safety: TokenInfo | null = null;
|
|
252
|
+
if (cfg.checkHoneypot && !isNative(tokenOut)) {
|
|
253
|
+
try {
|
|
254
|
+
safety = await this.tokenInfo(tokenOut, { probeValue: cfg.probeValue });
|
|
255
|
+
} catch {
|
|
256
|
+
safety = null;
|
|
257
|
+
}
|
|
258
|
+
if (safety !== null && safety.buySuccess) {
|
|
259
|
+
if (safety.isHoneypot) blocks.push("honeypot");
|
|
260
|
+
const tax = Math.max(safety.buyTaxBps, safety.sellTaxBps);
|
|
261
|
+
if (tax >= cfg.taxBlockBps) blocks.push(`tax_${tax}bps`);
|
|
262
|
+
else if (tax >= cfg.taxWarnBps) warns.push(`high_tax_${tax}bps`);
|
|
263
|
+
} else {
|
|
264
|
+
warns.push("safety_probe_unavailable");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const impact = await this._priceImpactBps(quoteIn, tokenOut, amountIn, realized);
|
|
269
|
+
if (impact !== null) {
|
|
270
|
+
if (impact >= cfg.priceImpactBlockBps) blocks.push(`price_impact_${impact}bps`);
|
|
271
|
+
else if (impact >= cfg.priceImpactWarnBps) warns.push(`price_impact_${impact}bps`);
|
|
272
|
+
}
|
|
273
|
+
return [safety, impact, blocks, warns];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// -- planning ---------------------------------------------------------- //
|
|
277
|
+
/** Build a fully-vetted {@link SwapPlan} without sending anything. */
|
|
278
|
+
async plan(
|
|
279
|
+
tokenIn: string,
|
|
280
|
+
tokenOut: string,
|
|
281
|
+
amountIn: bigint,
|
|
282
|
+
opts: { slippageBps?: number | null; config?: SafetyConfig | null; sender?: string | null } = {},
|
|
283
|
+
): Promise<SwapPlan> {
|
|
284
|
+
const cfg = opts.config ?? this._c.safety;
|
|
285
|
+
const slippageBps = opts.slippageBps == null ? cfg.slippageBps : opts.slippageBps;
|
|
286
|
+
tokenIn = this._a(tokenIn);
|
|
287
|
+
tokenOut = this._a(tokenOut);
|
|
288
|
+
const nativeIn = isNative(tokenIn);
|
|
289
|
+
const value = nativeIn ? amountIn : 0n;
|
|
290
|
+
const sender = opts.sender ?? this._c.address;
|
|
291
|
+
|
|
292
|
+
const wnative = await this.wnative();
|
|
293
|
+
// Routes are quoted in wrapped-native terms; the contract wraps the native
|
|
294
|
+
// value itself at execution. Integrity still maps the native sentinel back.
|
|
295
|
+
const quoteIn = nativeIn ? this._a(wnative) : tokenIn;
|
|
296
|
+
|
|
297
|
+
const cands = await this.candidates(quoteIn, tokenOut, amountIn);
|
|
298
|
+
if (cands.length === 0) {
|
|
299
|
+
throw new SallyError(`No route found ${tokenIn} -> ${tokenOut} for ${amountIn}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// integrity — guards against funds entering the wrong pool
|
|
303
|
+
if (cfg.checkIntegrity) {
|
|
304
|
+
for (const c of cands) {
|
|
305
|
+
c.integrityProblems = c.route.checkIntegrity(tokenIn, tokenOut, wnative);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// simulate each usable candidate end-to-end
|
|
310
|
+
if (cfg.simulate && sender != null) {
|
|
311
|
+
for (const c of cands) {
|
|
312
|
+
if (c.usable) {
|
|
313
|
+
c.simulatedOut = await this.simulateRoute(c.route, amountIn, { value, sender });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const usable = cands.filter((c) => c.usable);
|
|
319
|
+
const pool = usable.length ? usable : cands;
|
|
320
|
+
const best = pool.reduce((a, b) => (b.score > a.score ? b : a));
|
|
321
|
+
|
|
322
|
+
const blockReasons: string[] = [];
|
|
323
|
+
const warnings: string[] = [];
|
|
324
|
+
|
|
325
|
+
if (usable.length === 0) blockReasons.push("no_valid_route");
|
|
326
|
+
if (best.integrityProblems.length) {
|
|
327
|
+
blockReasons.push("integrity");
|
|
328
|
+
warnings.push(...best.integrityProblems);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const realized = best.score;
|
|
332
|
+
const [safety, impact, sblocks, swarns] = await this._safetyPreflight(quoteIn, tokenOut, amountIn, realized, cfg);
|
|
333
|
+
blockReasons.push(...sblocks);
|
|
334
|
+
warnings.push(...swarns);
|
|
335
|
+
|
|
336
|
+
return new SwapPlan(
|
|
337
|
+
tokenIn,
|
|
338
|
+
tokenOut,
|
|
339
|
+
amountIn,
|
|
340
|
+
best.route,
|
|
341
|
+
best.quotedOut,
|
|
342
|
+
best.simulatedOut,
|
|
343
|
+
this.minOut(realized, slippageBps),
|
|
344
|
+
impact,
|
|
345
|
+
safety,
|
|
346
|
+
cands,
|
|
347
|
+
warnings,
|
|
348
|
+
blockReasons,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// -- execution --------------------------------------------------------- //
|
|
353
|
+
/**
|
|
354
|
+
* Plan (vet + simulate), enforce guards, send, verify balance delta.
|
|
355
|
+
*
|
|
356
|
+
* `approval`: `"approve"` (default, exact-amount ERC-20 approve), `"permit"`
|
|
357
|
+
* (EIP-2612 signature where supported, else approve), or `"none"` (assume
|
|
358
|
+
* allowance already set). With `buildOnly=true` nothing is sent: returns a
|
|
359
|
+
* {@link SwapBuild}. Set `force=true` to bypass safety blocks (NOT recommended).
|
|
360
|
+
*/
|
|
361
|
+
async execute(tokenIn: string, tokenOut: string, amountIn: bigint, opts: ExecuteOptions = {}): Promise<any> {
|
|
362
|
+
let cfg = opts.config ?? this._c.safety;
|
|
363
|
+
const referral = opts.referral ?? ZERO;
|
|
364
|
+
const approval = opts.approval ?? "approve";
|
|
365
|
+
const force = opts.force ?? false;
|
|
366
|
+
const buildOnly = opts.buildOnly ?? false;
|
|
367
|
+
const txOpts = opts.tx ?? {};
|
|
368
|
+
|
|
369
|
+
const me = this._c.requireAddress();
|
|
370
|
+
tokenIn = this._a(tokenIn);
|
|
371
|
+
tokenOut = this._a(tokenOut);
|
|
372
|
+
const nativeIn = isNative(tokenIn);
|
|
373
|
+
const value = nativeIn ? amountIn : 0n;
|
|
374
|
+
const wnative = await this.wnative();
|
|
375
|
+
|
|
376
|
+
// 1) authorise the input first so the simulation's transferFrom succeeds.
|
|
377
|
+
let approveTx: Record<string, any> | null = null;
|
|
378
|
+
let needsApproval = false;
|
|
379
|
+
if (!nativeIn && approval !== "none") {
|
|
380
|
+
const spender = this._c.addresses["swap"];
|
|
381
|
+
const amount = cfg.unlimitedApproval ? MAX_UINT256 : amountIn;
|
|
382
|
+
const tok = this._c.token(tokenIn);
|
|
383
|
+
if (buildOnly) {
|
|
384
|
+
// Don't send: build the unsigned approve tx if allowance is short.
|
|
385
|
+
if ((await tok.allowance(me, spender)) < amount) {
|
|
386
|
+
needsApproval = true;
|
|
387
|
+
approveTx = await tok.buildApprove(spender, amount);
|
|
388
|
+
// The swap eth_call can't pull tokens yet → vet via quote, not sim.
|
|
389
|
+
cfg = cfg.replace({ simulate: false });
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
let usedPermit = false;
|
|
393
|
+
if (approval === "permit" && (await tok.supportsPermit())) {
|
|
394
|
+
if ((await tok.allowance(me, spender)) < amount) {
|
|
395
|
+
try {
|
|
396
|
+
await tok.permit(spender, amount);
|
|
397
|
+
usedPermit = true;
|
|
398
|
+
} catch {
|
|
399
|
+
// Some tokens advertise nonces but reject EIP-2612 permits
|
|
400
|
+
// (non-standard impls). Fall back to a plain approve.
|
|
401
|
+
usedPermit = false;
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
usedPermit = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!usedPermit) {
|
|
408
|
+
await tok.ensureAllowance(spender, amount);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 2) plan (unless a fixed path was supplied)
|
|
414
|
+
let plan: SwapPlan;
|
|
415
|
+
if (opts.path == null) {
|
|
416
|
+
plan = await this.plan(tokenIn, tokenOut, amountIn, {
|
|
417
|
+
slippageBps: opts.slippageBps ?? null,
|
|
418
|
+
config: cfg,
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
const path = opts.path;
|
|
422
|
+
const problems = cfg.checkIntegrity ? path.checkIntegrity(tokenIn, tokenOut, wnative) : [];
|
|
423
|
+
if (problems.length && !force) {
|
|
424
|
+
throw new SallyRouteError(`supplied path failed integrity: ${problems}`);
|
|
425
|
+
}
|
|
426
|
+
const sim = cfg.simulate ? await this.simulateRoute(path, amountIn, { value }) : null;
|
|
427
|
+
const sb = opts.slippageBps != null ? opts.slippageBps : cfg.slippageBps;
|
|
428
|
+
// If we simulated and the supplied path REVERTS, block — do not fall back
|
|
429
|
+
// to the caller's unvalidated quote (which could set minOut to 0).
|
|
430
|
+
const reasons: string[] = problems.length ? ["integrity"] : [];
|
|
431
|
+
if (cfg.simulate && sim === null) reasons.push("no_valid_route");
|
|
432
|
+
const floor = sim !== null ? this.minOut(sim, sb) : this.minOut(path, sb);
|
|
433
|
+
// A zero floor disables the on-chain slippage guard — block regardless of
|
|
434
|
+
// which safety toggles are off.
|
|
435
|
+
if (floor === 0n) reasons.push("zero_min_out");
|
|
436
|
+
// Run the SAME honeypot/tax + price-impact preflight as plan().
|
|
437
|
+
const quoteIn = nativeIn ? this._a(wnative) : tokenIn;
|
|
438
|
+
const realized = sim !== null ? sim : path.estimatedAmountOut;
|
|
439
|
+
const [safety, impact, sblocks, swarns] = await this._safetyPreflight(
|
|
440
|
+
quoteIn,
|
|
441
|
+
tokenOut,
|
|
442
|
+
amountIn,
|
|
443
|
+
realized,
|
|
444
|
+
cfg,
|
|
445
|
+
);
|
|
446
|
+
plan = new SwapPlan(
|
|
447
|
+
tokenIn,
|
|
448
|
+
tokenOut,
|
|
449
|
+
amountIn,
|
|
450
|
+
path,
|
|
451
|
+
path.estimatedAmountOut,
|
|
452
|
+
sim,
|
|
453
|
+
floor,
|
|
454
|
+
impact,
|
|
455
|
+
safety,
|
|
456
|
+
[],
|
|
457
|
+
swarns,
|
|
458
|
+
[...reasons, ...sblocks],
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 3) enforce guards
|
|
463
|
+
if (!plan.isSafe && !force) {
|
|
464
|
+
if (plan.blockReasons.includes("integrity") || plan.blockReasons.includes("no_valid_route")) {
|
|
465
|
+
throw new SallyRouteError(`route rejected: ${plan.blockReasons} ${plan.warnings}`);
|
|
466
|
+
}
|
|
467
|
+
throw new SallySafetyError(`swap blocked: ${plan.blockReasons}`, plan.blockReasons);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const minOut = opts.minOutput != null ? opts.minOutput : plan.minOut;
|
|
471
|
+
const deadline = opts.deadline != null ? opts.deadline : this.deadline(cfg.deadlineSecs);
|
|
472
|
+
|
|
473
|
+
// 4) balance-delta baseline (ERC-20 out only). Skip for native AND wrapped-
|
|
474
|
+
// native out: Sally may pay out unwrapped native, so an ERC-20 balanceOf
|
|
475
|
+
// would not move — the contract's own minOut guard still protects there.
|
|
476
|
+
const track = !isNative(tokenOut) && tokenOut.toLowerCase() !== wnative.toLowerCase();
|
|
477
|
+
const before = track ? await this._c.token(tokenOut).balanceOf(me) : 0n;
|
|
478
|
+
|
|
479
|
+
// 5) build the chosen, vetted route
|
|
480
|
+
const fn = this._swap.getFunction("executeHybridSwap");
|
|
481
|
+
const args = [plan.route.toTuple(), amountIn, minOut, deadline, this._a(referral)];
|
|
482
|
+
if (buildOnly) {
|
|
483
|
+
// Sign-externally: return the vetted plan + unsigned txs, send nothing.
|
|
484
|
+
const { SwapBuild } = await import("../previews.js");
|
|
485
|
+
const swapTx = await this._c.send(fn, args, { value, simulate: false, buildOnly: true });
|
|
486
|
+
return new SwapBuild({ plan, swapTx, approveTx, needsApproval });
|
|
487
|
+
}
|
|
488
|
+
const receipt = await this._c.send(fn, args, { value, ...txOpts });
|
|
489
|
+
|
|
490
|
+
// 6) trust the chain: received balance must have grown by >= minOut
|
|
491
|
+
if (track && cfg.balanceDeltaAssert) {
|
|
492
|
+
const gained = (await this._c.token(tokenOut).balanceOf(me)) - before;
|
|
493
|
+
if (gained < minOut) {
|
|
494
|
+
throw new SallySafetyError(
|
|
495
|
+
`received ${gained} < min_out ${minOut} (balance-delta check failed)`,
|
|
496
|
+
["balance_delta"],
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return receipt;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Wallet balance reads with live USD valuation (Lens via swap proxy). */
|
|
2
|
+
|
|
3
|
+
import { Contract, getAddress } from "ethers";
|
|
4
|
+
import type { SallyClient } from "../client.js";
|
|
5
|
+
import { WalletBalance } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export class Wallet {
|
|
8
|
+
private _c: SallyClient;
|
|
9
|
+
private _lens: Contract;
|
|
10
|
+
|
|
11
|
+
constructor(client: SallyClient) {
|
|
12
|
+
this._c = client;
|
|
13
|
+
this._lens = client.lensContract;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private _addrs(tokens: string[]): string[] {
|
|
17
|
+
return tokens.map((t) => getAddress(t));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Balances + USD value for `tokens`. `getWalletBalance`. */
|
|
21
|
+
async balances(wallet: string, tokens: string[]): Promise<WalletBalance[]> {
|
|
22
|
+
const r = await this._c.call(this._lens.getFunction("getWalletBalance"), [getAddress(wallet), this._addrs(tokens)]);
|
|
23
|
+
return [...r].map((x) => WalletBalance.fromRaw(x));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Same but the gas-lean variant. `getWalletBalanceRaw`. */
|
|
27
|
+
async balancesRaw(wallet: string, tokens: string[]): Promise<WalletBalance[]> {
|
|
28
|
+
const r = await this._c.call(this._lens.getFunction("getWalletBalanceRaw"), [getAddress(wallet), this._addrs(tokens)]);
|
|
29
|
+
return [...r].map((x) => WalletBalance.fromRaw(x));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Sum of USD value across `tokens` (float, human units). */
|
|
33
|
+
async totalUsd(wallet: string, tokens: string[]): Promise<number> {
|
|
34
|
+
const bals = await this.balances(wallet, tokens);
|
|
35
|
+
return bals.reduce((acc, b) => acc + b.usdValueFloat, 0);
|
|
36
|
+
}
|
|
37
|
+
}
|