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,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async hybrid swaps — quote, compare, simulate, guard, execute.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the sync {@link Swap}, but quoting and simulation of the candidate
|
|
5
|
+
* routes run concurrently via `Promise.all` (the async client's main payoff,
|
|
6
|
+
* matching the Python `asyncio.gather`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Contract, getAddress } from "ethers";
|
|
10
|
+
import type { AsyncSallyClient } from "../client.js";
|
|
11
|
+
import { SallyError, SallyRouteError, SallySafetyError } from "../../errors.js";
|
|
12
|
+
import { RouteCandidate, SafetyConfig, SwapPlan } from "../../safety.js";
|
|
13
|
+
import { MAX_UINT256, ZERO, isNative } from "../../token.js";
|
|
14
|
+
import { SwapPath, TokenInfo } from "../../types.js";
|
|
15
|
+
import { pyRound } from "../../util.js";
|
|
16
|
+
|
|
17
|
+
const MAX_DEADLINE = 2n ** 256n - 1n;
|
|
18
|
+
|
|
19
|
+
export interface AsyncExecuteOptions {
|
|
20
|
+
slippageBps?: number | null;
|
|
21
|
+
minOutput?: bigint | null;
|
|
22
|
+
path?: SwapPath | null;
|
|
23
|
+
referral?: string;
|
|
24
|
+
deadline?: number | null;
|
|
25
|
+
config?: SafetyConfig | null;
|
|
26
|
+
approval?: "approve" | "permit" | "none";
|
|
27
|
+
force?: boolean;
|
|
28
|
+
tx?: Record<string, any>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AsyncSwap {
|
|
32
|
+
private _c: AsyncSallyClient;
|
|
33
|
+
private _swap: Contract;
|
|
34
|
+
private _wnative: string | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(client: AsyncSallyClient) {
|
|
37
|
+
this._c = client;
|
|
38
|
+
this._swap = client.swapContract;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private _a(x: string): string {
|
|
42
|
+
return getAddress(x);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async wnative(): Promise<string> {
|
|
46
|
+
if (this._wnative === null) {
|
|
47
|
+
this._wnative = String(await this._c.call(this._swap.getFunction("wnative"), []));
|
|
48
|
+
}
|
|
49
|
+
return this._wnative;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// -- quoting ----------------------------------------------------------- //
|
|
53
|
+
async quote(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
54
|
+
return SwapPath.fromRaw(
|
|
55
|
+
await this._c.call(this._swap.getFunction("getBestSwapPath"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async quoteDeep(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<SwapPath> {
|
|
60
|
+
return SwapPath.fromRaw(
|
|
61
|
+
await this._c.call(this._c.lensContract.getFunction("getBestSwapPathDeep"), [
|
|
62
|
+
this._a(tokenIn),
|
|
63
|
+
this._a(tokenOut),
|
|
64
|
+
amountIn,
|
|
65
|
+
]),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async _quoteNamed(
|
|
70
|
+
label: string,
|
|
71
|
+
tokenIn: string,
|
|
72
|
+
tokenOut: string,
|
|
73
|
+
amountIn: bigint,
|
|
74
|
+
): Promise<SwapPath | null> {
|
|
75
|
+
const fns: Record<string, () => Promise<any>> = {
|
|
76
|
+
best: () => this._c.call(this._swap.getFunction("getBestSwapPath"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
77
|
+
deep: () =>
|
|
78
|
+
this._c.call(this._c.lensContract.getFunction("getBestSwapPathDeep"), [
|
|
79
|
+
this._a(tokenIn),
|
|
80
|
+
this._a(tokenOut),
|
|
81
|
+
amountIn,
|
|
82
|
+
]),
|
|
83
|
+
v2: () => this._c.call(this._swap.getFunction("getSwapPathV2"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
84
|
+
v3: () => this._c.call(this._swap.getFunction("getSwapPathV3"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
85
|
+
v4: () => this._c.call(this._swap.getFunction("getSwapPathV4"), [this._a(tokenIn), this._a(tokenOut), amountIn]),
|
|
86
|
+
};
|
|
87
|
+
let r: any;
|
|
88
|
+
try {
|
|
89
|
+
r = await fns[label]();
|
|
90
|
+
} catch (exc) {
|
|
91
|
+
if (exc instanceof SallyError) return null;
|
|
92
|
+
throw exc;
|
|
93
|
+
}
|
|
94
|
+
const route = SwapPath.fromRaw(r);
|
|
95
|
+
return route.stepCount ? route : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async tokenInfo(token: string, opts: { probeValue?: bigint | null } = {}): Promise<TokenInfo> {
|
|
99
|
+
let probeValue = opts.probeValue ?? null;
|
|
100
|
+
if (probeValue === null) probeValue = new SafetyConfig().probeValue;
|
|
101
|
+
const raw = await this._swap.getFunction("getTokenInfos").staticCall(this._a(token), { from: ZERO, value: probeValue });
|
|
102
|
+
return TokenInfo.fromRaw(raw);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
minOut(amountOut: bigint | SwapPath | null, slippageBps: number): bigint {
|
|
106
|
+
const out: bigint = amountOut instanceof SwapPath ? amountOut.estimatedAmountOut : (amountOut ?? 0n);
|
|
107
|
+
const floor = (out * BigInt(10_000 - slippageBps)) / 10_000n;
|
|
108
|
+
return out > 0n ? (floor > 1n ? floor : 1n) : 0n;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deadline(secs: number | null = null): Promise<number> {
|
|
112
|
+
const s = secs !== null ? secs : new SafetyConfig().deadlineSecs;
|
|
113
|
+
return Math.floor(Date.now() / 1000) + s;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async candidates(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<RouteCandidate[]> {
|
|
117
|
+
tokenIn = this._a(tokenIn);
|
|
118
|
+
tokenOut = this._a(tokenOut);
|
|
119
|
+
const labels = ["best", "deep", "v2", "v3", "v4"];
|
|
120
|
+
const routes = await Promise.all(labels.map((x) => this._quoteNamed(x, tokenIn, tokenOut, amountIn)));
|
|
121
|
+
const out: RouteCandidate[] = [];
|
|
122
|
+
const seen = new Set<string>();
|
|
123
|
+
for (let i = 0; i < labels.length; i++) {
|
|
124
|
+
const route = routes[i];
|
|
125
|
+
if (route === null) continue;
|
|
126
|
+
const key = JSON.stringify([
|
|
127
|
+
route.poolAddresses.map((p) => p.toLowerCase()),
|
|
128
|
+
route.stepCount,
|
|
129
|
+
route.estimatedAmountOut.toString(),
|
|
130
|
+
]);
|
|
131
|
+
if (seen.has(key)) continue;
|
|
132
|
+
seen.add(key);
|
|
133
|
+
out.push(new RouteCandidate(labels[i], route, route.estimatedAmountOut));
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async simulateRoute(
|
|
139
|
+
route: SwapPath,
|
|
140
|
+
amountIn: bigint,
|
|
141
|
+
opts: { value?: bigint; sender?: string | null } = {},
|
|
142
|
+
): Promise<bigint | null> {
|
|
143
|
+
const value = opts.value ?? 0n;
|
|
144
|
+
const sender = opts.sender ?? this._c.address;
|
|
145
|
+
if (sender == null) return null;
|
|
146
|
+
try {
|
|
147
|
+
const r = await this._swap
|
|
148
|
+
.getFunction("executeHybridSwap")
|
|
149
|
+
.staticCall(route.toTuple(), amountIn, 0, MAX_DEADLINE, ZERO, { from: sender, value });
|
|
150
|
+
return BigInt(r);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async _priceImpactBps(
|
|
157
|
+
tokenIn: string,
|
|
158
|
+
tokenOut: string,
|
|
159
|
+
amountIn: bigint,
|
|
160
|
+
realized: bigint,
|
|
161
|
+
): Promise<number | null> {
|
|
162
|
+
const probe = amountIn / 1000n > 1n ? amountIn / 1000n : 1n;
|
|
163
|
+
let probeOut: bigint;
|
|
164
|
+
try {
|
|
165
|
+
probeOut = (await this.quote(tokenIn, tokenOut, probe)).estimatedAmountOut;
|
|
166
|
+
} catch (exc) {
|
|
167
|
+
if (exc instanceof SallyError) return null;
|
|
168
|
+
throw exc;
|
|
169
|
+
}
|
|
170
|
+
if (probeOut <= 0n || realized <= 0n) return null;
|
|
171
|
+
const expected = (probeOut * amountIn) / probe;
|
|
172
|
+
return expected > 0n ? Math.max(0, pyRound((Number(expected - realized) / Number(expected)) * 10_000)) : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async plan(
|
|
176
|
+
tokenIn: string,
|
|
177
|
+
tokenOut: string,
|
|
178
|
+
amountIn: bigint,
|
|
179
|
+
opts: { slippageBps?: number | null; config?: SafetyConfig | null; sender?: string | null } = {},
|
|
180
|
+
): Promise<SwapPlan> {
|
|
181
|
+
const cfg = opts.config ?? this._c.safety;
|
|
182
|
+
const slippageBps = opts.slippageBps == null ? cfg.slippageBps : opts.slippageBps;
|
|
183
|
+
tokenIn = this._a(tokenIn);
|
|
184
|
+
tokenOut = this._a(tokenOut);
|
|
185
|
+
const nativeIn = isNative(tokenIn);
|
|
186
|
+
const value = nativeIn ? amountIn : 0n;
|
|
187
|
+
const sender = opts.sender ?? this._c.address;
|
|
188
|
+
const wnative = await this.wnative();
|
|
189
|
+
const quoteIn = nativeIn ? this._a(wnative) : tokenIn;
|
|
190
|
+
|
|
191
|
+
const cands = await this.candidates(quoteIn, tokenOut, amountIn);
|
|
192
|
+
if (cands.length === 0) {
|
|
193
|
+
throw new SallyError(`No route found ${tokenIn} -> ${tokenOut} for ${amountIn}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cfg.checkIntegrity) {
|
|
197
|
+
for (const c of cands) {
|
|
198
|
+
c.integrityProblems = c.route.checkIntegrity(tokenIn, tokenOut, wnative);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (cfg.simulate && sender != null) {
|
|
203
|
+
const sims = await Promise.all(
|
|
204
|
+
cands.map((c) => (c.usable ? this.simulateRoute(c.route, amountIn, { value, sender }) : Promise.resolve(null))),
|
|
205
|
+
);
|
|
206
|
+
cands.forEach((c, i) => {
|
|
207
|
+
if (c.usable) c.simulatedOut = sims[i];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const usable = cands.filter((c) => c.usable);
|
|
212
|
+
const best = (usable.length ? usable : cands).reduce((a, b) => (b.score > a.score ? b : a));
|
|
213
|
+
const blockReasons: string[] = [];
|
|
214
|
+
const warnings: string[] = [];
|
|
215
|
+
if (usable.length === 0) blockReasons.push("no_valid_route");
|
|
216
|
+
if (best.integrityProblems.length) {
|
|
217
|
+
blockReasons.push("integrity");
|
|
218
|
+
warnings.push(...best.integrityProblems);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let safety: TokenInfo | null = null;
|
|
222
|
+
if (cfg.checkHoneypot && !isNative(tokenOut)) {
|
|
223
|
+
try {
|
|
224
|
+
safety = await this.tokenInfo(tokenOut, { probeValue: cfg.probeValue });
|
|
225
|
+
} catch {
|
|
226
|
+
safety = null;
|
|
227
|
+
}
|
|
228
|
+
if (safety !== null && safety.buySuccess) {
|
|
229
|
+
if (safety.isHoneypot) blockReasons.push("honeypot");
|
|
230
|
+
const tax = Math.max(safety.buyTaxBps, safety.sellTaxBps);
|
|
231
|
+
if (tax >= cfg.taxBlockBps) blockReasons.push(`tax_${tax}bps`);
|
|
232
|
+
else if (tax >= cfg.taxWarnBps) warnings.push(`high_tax_${tax}bps`);
|
|
233
|
+
} else {
|
|
234
|
+
warnings.push("safety_probe_unavailable");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const realized = best.score;
|
|
239
|
+
const impact = await this._priceImpactBps(quoteIn, tokenOut, amountIn, realized);
|
|
240
|
+
if (impact !== null) {
|
|
241
|
+
if (impact >= cfg.priceImpactBlockBps) blockReasons.push(`price_impact_${impact}bps`);
|
|
242
|
+
else if (impact >= cfg.priceImpactWarnBps) warnings.push(`price_impact_${impact}bps`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return new SwapPlan(
|
|
246
|
+
tokenIn,
|
|
247
|
+
tokenOut,
|
|
248
|
+
amountIn,
|
|
249
|
+
best.route,
|
|
250
|
+
best.quotedOut,
|
|
251
|
+
best.simulatedOut,
|
|
252
|
+
this.minOut(realized, slippageBps),
|
|
253
|
+
impact,
|
|
254
|
+
safety,
|
|
255
|
+
cands,
|
|
256
|
+
warnings,
|
|
257
|
+
blockReasons,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async execute(tokenIn: string, tokenOut: string, amountIn: bigint, opts: AsyncExecuteOptions = {}): Promise<any> {
|
|
262
|
+
const cfg = opts.config ?? this._c.safety;
|
|
263
|
+
const referral = opts.referral ?? ZERO;
|
|
264
|
+
const approval = opts.approval ?? "approve";
|
|
265
|
+
const force = opts.force ?? false;
|
|
266
|
+
const txOpts = opts.tx ?? {};
|
|
267
|
+
|
|
268
|
+
this._c.requireAddress();
|
|
269
|
+
tokenIn = this._a(tokenIn);
|
|
270
|
+
tokenOut = this._a(tokenOut);
|
|
271
|
+
const nativeIn = isNative(tokenIn);
|
|
272
|
+
const value = nativeIn ? amountIn : 0n;
|
|
273
|
+
const wnative = await this.wnative();
|
|
274
|
+
|
|
275
|
+
// Async approval supports "approve" (exact) and "none"; EIP-2612 permit is
|
|
276
|
+
// sync-only, so "permit" degrades to a plain approve here (made observable).
|
|
277
|
+
if (approval === "permit") {
|
|
278
|
+
console.warn(
|
|
279
|
+
"approval='permit' is not implemented on the async client; " +
|
|
280
|
+
"falling back to a plain approve. Use the sync client for EIP-2612 permit.",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (!nativeIn && approval !== "none") {
|
|
284
|
+
const spender = this._c.addresses["swap"];
|
|
285
|
+
const amount = cfg.unlimitedApproval ? MAX_UINT256 : amountIn;
|
|
286
|
+
await this._c.token(tokenIn).ensureAllowance(spender, amount);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let plan: SwapPlan;
|
|
290
|
+
if (opts.path == null) {
|
|
291
|
+
plan = await this.plan(tokenIn, tokenOut, amountIn, { slippageBps: opts.slippageBps ?? null, config: cfg });
|
|
292
|
+
} else {
|
|
293
|
+
const path = opts.path;
|
|
294
|
+
const problems = cfg.checkIntegrity ? path.checkIntegrity(tokenIn, tokenOut, wnative) : [];
|
|
295
|
+
if (problems.length && !force) {
|
|
296
|
+
throw new SallyRouteError(`supplied path failed integrity: ${problems}`);
|
|
297
|
+
}
|
|
298
|
+
const sim = cfg.simulate ? await this.simulateRoute(path, amountIn, { value }) : null;
|
|
299
|
+
const sb = opts.slippageBps != null ? opts.slippageBps : cfg.slippageBps;
|
|
300
|
+
const reasons: string[] = problems.length ? ["integrity"] : [];
|
|
301
|
+
if (cfg.simulate && sim === null) reasons.push("no_valid_route");
|
|
302
|
+
const floor = sim !== null ? this.minOut(sim, sb) : this.minOut(path, sb);
|
|
303
|
+
plan = new SwapPlan(
|
|
304
|
+
tokenIn,
|
|
305
|
+
tokenOut,
|
|
306
|
+
amountIn,
|
|
307
|
+
path,
|
|
308
|
+
path.estimatedAmountOut,
|
|
309
|
+
sim,
|
|
310
|
+
floor,
|
|
311
|
+
null,
|
|
312
|
+
null,
|
|
313
|
+
[],
|
|
314
|
+
[],
|
|
315
|
+
reasons,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!plan.isSafe && !force) {
|
|
320
|
+
if (plan.blockReasons.includes("integrity") || plan.blockReasons.includes("no_valid_route")) {
|
|
321
|
+
throw new SallyRouteError(`route rejected: ${plan.blockReasons} ${plan.warnings}`);
|
|
322
|
+
}
|
|
323
|
+
throw new SallySafetyError(`swap blocked: ${plan.blockReasons}`, plan.blockReasons);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const minOut = opts.minOutput != null ? opts.minOutput : plan.minOut;
|
|
327
|
+
const deadline = opts.deadline != null ? opts.deadline : await this.deadline(cfg.deadlineSecs);
|
|
328
|
+
|
|
329
|
+
const track = !isNative(tokenOut) && tokenOut.toLowerCase() !== wnative.toLowerCase();
|
|
330
|
+
const before = track ? await this._c.token(tokenOut).balanceOf(this._c.address!) : 0n;
|
|
331
|
+
|
|
332
|
+
const fn = this._swap.getFunction("executeHybridSwap");
|
|
333
|
+
const args = [plan.route.toTuple(), amountIn, minOut, deadline, this._a(referral)];
|
|
334
|
+
const receipt = await this._c.send(fn, args, { value, ...txOpts });
|
|
335
|
+
|
|
336
|
+
if (track && cfg.balanceDeltaAssert) {
|
|
337
|
+
const gained = (await this._c.token(tokenOut).balanceOf(this._c.address!)) - before;
|
|
338
|
+
if (gained < minOut) {
|
|
339
|
+
throw new SallySafetyError(
|
|
340
|
+
`received ${gained} < min_out ${minOut} (balance-delta check failed)`,
|
|
341
|
+
["balance_delta"],
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return receipt;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Async wallet balances. */
|
|
2
|
+
|
|
3
|
+
import { Contract, getAddress } from "ethers";
|
|
4
|
+
import type { AsyncSallyClient } from "../client.js";
|
|
5
|
+
import { WalletBalance } from "../../types.js";
|
|
6
|
+
|
|
7
|
+
export class AsyncWallet {
|
|
8
|
+
private _c: AsyncSallyClient;
|
|
9
|
+
private _lens: Contract;
|
|
10
|
+
|
|
11
|
+
constructor(client: AsyncSallyClient) {
|
|
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
|
+
async balances(wallet: string, tokens: string[]): Promise<WalletBalance[]> {
|
|
21
|
+
const r = await this._c.call(this._lens.getFunction("getWalletBalance"), [getAddress(wallet), this._addrs(tokens)]);
|
|
22
|
+
return [...r].map((x) => WalletBalance.fromRaw(x));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async balancesRaw(wallet: string, tokens: string[]): Promise<WalletBalance[]> {
|
|
26
|
+
const r = await this._c.call(this._lens.getFunction("getWalletBalanceRaw"), [getAddress(wallet), this._addrs(tokens)]);
|
|
27
|
+
return [...r].map((x) => WalletBalance.fromRaw(x));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async totalUsd(wallet: string, tokens: string[]): Promise<number> {
|
|
31
|
+
const bals = await this.balances(wallet, tokens);
|
|
32
|
+
return bals.reduce((acc, b) => acc + b.usdValueFloat, 0);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/aio/token.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Async ERC-20 helper (mirror of {@link Token}). */
|
|
2
|
+
|
|
3
|
+
import { Contract, getAddress } from "ethers";
|
|
4
|
+
import type { AsyncSallyClient } from "./client.js";
|
|
5
|
+
import { ERC20_ABI, MAX_UINT256, TokenAmount } from "../token.js";
|
|
6
|
+
|
|
7
|
+
export class AsyncToken {
|
|
8
|
+
readonly client: AsyncSallyClient;
|
|
9
|
+
readonly address: string;
|
|
10
|
+
readonly contract: Contract;
|
|
11
|
+
private _decimals: number | null = null;
|
|
12
|
+
|
|
13
|
+
constructor(client: AsyncSallyClient, address: string) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
this.address = getAddress(address);
|
|
16
|
+
this.contract = new Contract(this.address, ERC20_ABI as any, client.w3);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async decimals(): Promise<number> {
|
|
20
|
+
if (this._decimals === null) {
|
|
21
|
+
this._decimals = Number(await this.contract.decimals());
|
|
22
|
+
}
|
|
23
|
+
return this._decimals;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async symbol(): Promise<string> {
|
|
27
|
+
try {
|
|
28
|
+
return await this.contract.symbol();
|
|
29
|
+
} catch {
|
|
30
|
+
return "?";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async balanceOf(owner: string): Promise<bigint> {
|
|
35
|
+
return BigInt(await this.contract.balanceOf(getAddress(owner)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async allowance(owner: string, spender: string): Promise<bigint> {
|
|
39
|
+
return BigInt(await this.contract.allowance(getAddress(owner), getAddress(spender)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async toRaw(human: string | number | bigint): Promise<bigint> {
|
|
43
|
+
return TokenAmount.fromHuman(human, await this.decimals()).raw;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async approve(spender: string, amount: bigint = MAX_UINT256, tx: Record<string, any> = {}): Promise<any> {
|
|
47
|
+
const fn = this.contract.getFunction("approve");
|
|
48
|
+
return this.client.send(fn, [getAddress(spender), amount], tx);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Approve exactly `amount` if the current allowance is insufficient. */
|
|
52
|
+
async ensureAllowance(spender: string, amount: bigint, tx: Record<string, any> = {}): Promise<any> {
|
|
53
|
+
const owner = this.client.requireAddress();
|
|
54
|
+
if ((await this.allowance(owner, spender)) >= amount) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return this.approve(spender, amount, tx);
|
|
58
|
+
}
|
|
59
|
+
}
|