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
package/src/client.ts
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one object users hold: {@link SallyClient}.
|
|
3
|
+
*
|
|
4
|
+
* Every read is a direct `eth_call` to the on-chain contracts through your RPC —
|
|
5
|
+
* no API, no subgraph / Graph Node, no backend in the data path.
|
|
6
|
+
*
|
|
7
|
+
* Reads work with just an RPC URL. Writes need a signer — pass a `privateKey`, a
|
|
8
|
+
* 12/24-word `mnemonic`, or an ethers `Wallet`/`HDNodeWallet`. To sign with your
|
|
9
|
+
* **own** wallet instead, pass only `address` (no key) and use the build/preview
|
|
10
|
+
* API: every action can return a fully-populated, ABI-decoded, simulated
|
|
11
|
+
* *unsigned* transaction you sign and broadcast yourself.
|
|
12
|
+
*
|
|
13
|
+
* Feature namespaces:
|
|
14
|
+
*
|
|
15
|
+
* client.prices USD / spot pricing + token safety
|
|
16
|
+
* client.swap quote + execute hybrid swaps (+ build/preview)
|
|
17
|
+
* client.wallet balances with live USD valuation
|
|
18
|
+
* client.liquidity add / remove / lock / harvest / positions
|
|
19
|
+
* client.fees referral + batch fee accounting
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
Contract,
|
|
24
|
+
HDNodeWallet,
|
|
25
|
+
JsonRpcProvider,
|
|
26
|
+
Network,
|
|
27
|
+
Wallet,
|
|
28
|
+
getAddress,
|
|
29
|
+
type Signer,
|
|
30
|
+
} from "ethers";
|
|
31
|
+
|
|
32
|
+
import * as deployment from "./deployment.js";
|
|
33
|
+
import { SallyConfigError, SallyError, SallyRevert, wrapWeb3Error } from "./errors.js";
|
|
34
|
+
import { SafetyConfig } from "./safety.js";
|
|
35
|
+
import { Token } from "./token.js";
|
|
36
|
+
import { TxPreview } from "./previews.js";
|
|
37
|
+
import { Permit2 } from "./permit2.js";
|
|
38
|
+
import { Prices } from "./modules/prices.js";
|
|
39
|
+
import { Swap } from "./modules/swap.js";
|
|
40
|
+
import { Wallet as WalletModule } from "./modules/wallet.js";
|
|
41
|
+
import { Liquidity } from "./modules/liquidity.js";
|
|
42
|
+
import { Fees } from "./modules/fees.js";
|
|
43
|
+
|
|
44
|
+
// Process-wide nonce locks keyed by signer address, so concurrent sends from the
|
|
45
|
+
// same account — even across different SallyClient instances — serialize.
|
|
46
|
+
const _NONCE_LOCKS = new Map<string, AsyncMutex>();
|
|
47
|
+
|
|
48
|
+
function lockFor(address: string | null): AsyncMutex {
|
|
49
|
+
if (address === null) return new AsyncMutex(); // read-only client: lock unused
|
|
50
|
+
const key = address.toLowerCase();
|
|
51
|
+
let lock = _NONCE_LOCKS.get(key);
|
|
52
|
+
if (!lock) {
|
|
53
|
+
lock = new AsyncMutex();
|
|
54
|
+
_NONCE_LOCKS.set(key, lock);
|
|
55
|
+
}
|
|
56
|
+
return lock;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Minimal async mutex: serialize an async critical section. */
|
|
60
|
+
class AsyncMutex {
|
|
61
|
+
private _chain: Promise<void> = Promise.resolve();
|
|
62
|
+
|
|
63
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
64
|
+
const prev = this._chain;
|
|
65
|
+
let release!: () => void;
|
|
66
|
+
this._chain = new Promise<void>((res) => (release = res));
|
|
67
|
+
await prev;
|
|
68
|
+
try {
|
|
69
|
+
return await fn();
|
|
70
|
+
} finally {
|
|
71
|
+
release();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SallyClientOptions {
|
|
77
|
+
privateKey?: string | null;
|
|
78
|
+
mnemonic?: string | null;
|
|
79
|
+
accountIndex?: number;
|
|
80
|
+
account?: Signer | null;
|
|
81
|
+
address?: string | null;
|
|
82
|
+
poa?: boolean | null;
|
|
83
|
+
safety?: SafetyConfig | null;
|
|
84
|
+
privateRpcUrl?: string | null;
|
|
85
|
+
gasMultiplier?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A contract method handle (from `contract.getFunction(name)`). */
|
|
89
|
+
type Fn = any;
|
|
90
|
+
|
|
91
|
+
export class SallyClient {
|
|
92
|
+
readonly chainKey: string;
|
|
93
|
+
readonly chainId: number;
|
|
94
|
+
safety: SafetyConfig;
|
|
95
|
+
privateRpcUrl: string | null;
|
|
96
|
+
private _privateW3: JsonRpcProvider | null = null;
|
|
97
|
+
w3: JsonRpcProvider;
|
|
98
|
+
gasMultiplier: number;
|
|
99
|
+
account: Signer | null = null;
|
|
100
|
+
protected _sender: string | null = null;
|
|
101
|
+
private _sendLock: AsyncMutex;
|
|
102
|
+
addresses: Record<string, string>;
|
|
103
|
+
|
|
104
|
+
// lazy namespace + contract caches
|
|
105
|
+
private _cache: Record<string, any> = {};
|
|
106
|
+
|
|
107
|
+
constructor(chain: string | number = "base", rpcUrl?: string | null, opts: SallyClientOptions = {}) {
|
|
108
|
+
this.chainKey = deployment.normalizeChain(chain);
|
|
109
|
+
this.chainId = deployment.chainId(this.chainKey);
|
|
110
|
+
this.safety = opts.safety ?? new SafetyConfig();
|
|
111
|
+
this.privateRpcUrl = opts.privateRpcUrl ?? process.env.SALLY_PRIVATE_RPC_URL ?? null;
|
|
112
|
+
this.gasMultiplier = opts.gasMultiplier ?? 1.25;
|
|
113
|
+
|
|
114
|
+
rpcUrl = rpcUrl ?? process.env.SALLY_RPC_URL ?? null;
|
|
115
|
+
if (!rpcUrl) {
|
|
116
|
+
throw new SallyConfigError(
|
|
117
|
+
"No rpcUrl given and SALLY_RPC_URL is unset. " +
|
|
118
|
+
"Pass rpcUrl='https://…' or set the env var.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Static network so construction never triggers a chainId auto-detect call.
|
|
122
|
+
const network = Network.from(this.chainId);
|
|
123
|
+
this.w3 = new JsonRpcProvider(rpcUrl, network, { staticNetwork: network });
|
|
124
|
+
|
|
125
|
+
// POA: ethers handles non-standard extraData natively; flag kept for parity.
|
|
126
|
+
void (opts.poa ?? this.chainKey === "bsc");
|
|
127
|
+
|
|
128
|
+
// Signer resolution: account > privateKey > mnemonic. `address` (no key)
|
|
129
|
+
// enables sign-externally mode.
|
|
130
|
+
const key = opts.privateKey ?? process.env.SALLY_PRIVATE_KEY ?? null;
|
|
131
|
+
const phrase = opts.mnemonic ?? process.env.SALLY_MNEMONIC ?? null;
|
|
132
|
+
if (opts.account != null) {
|
|
133
|
+
this.account = opts.account;
|
|
134
|
+
} else if (key) {
|
|
135
|
+
this.account = new Wallet(key);
|
|
136
|
+
} else if (phrase) {
|
|
137
|
+
const idx = opts.accountIndex ?? 0;
|
|
138
|
+
this.account = HDNodeWallet.fromPhrase(phrase, undefined, `m/44'/60'/0'/0/${idx}`);
|
|
139
|
+
}
|
|
140
|
+
this._sender = this.account
|
|
141
|
+
? this._accountAddress(this.account)
|
|
142
|
+
: opts.address
|
|
143
|
+
? getAddress(opts.address)
|
|
144
|
+
: null;
|
|
145
|
+
this._sendLock = lockFor(this._sender);
|
|
146
|
+
|
|
147
|
+
this.addresses = Object.fromEntries(
|
|
148
|
+
Object.entries(deployment.addresses(this.chainKey)).map(([k, v]) => [
|
|
149
|
+
k,
|
|
150
|
+
v.startsWith("0x") ? getAddress(v) : v,
|
|
151
|
+
]),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private _accountAddress(acc: Signer): string {
|
|
156
|
+
// Wallet/HDNodeWallet expose `.address` synchronously.
|
|
157
|
+
return getAddress((acc as any).address);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ------------------------------------------------------------------ //
|
|
161
|
+
// Connection / identity
|
|
162
|
+
// ------------------------------------------------------------------ //
|
|
163
|
+
async isConnected(): Promise<boolean> {
|
|
164
|
+
try {
|
|
165
|
+
await this.w3.getBlockNumber();
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Sender address — from the signer, or the keyless `address` mode. */
|
|
173
|
+
get address(): string | null {
|
|
174
|
+
return this._sender;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get canSign(): boolean {
|
|
178
|
+
return this.account !== null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The sender address (needs a signer OR keyless `address`). */
|
|
182
|
+
requireAddress(): string {
|
|
183
|
+
if (!this._sender) {
|
|
184
|
+
throw new SallyConfigError(
|
|
185
|
+
"This action needs a sender. Create the client with privateKey=…, " +
|
|
186
|
+
"mnemonic=…, or address='0x…' (sign-externally mode).",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return this._sender;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The local signing account (needs a key/mnemonic, not just `address`). */
|
|
193
|
+
requireSigner(): Signer {
|
|
194
|
+
if (this.account === null) {
|
|
195
|
+
throw new SallyConfigError(
|
|
196
|
+
"This action signs a transaction and needs a key. Pass privateKey=… " +
|
|
197
|
+
"or mnemonic=…, or use buildOnly=true / .preview() to sign externally.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return this.account;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Return a new client on the same RPC/config but with a different signer. */
|
|
204
|
+
withAccount(privateKey: string): SallyClient {
|
|
205
|
+
const c: SallyClient = Object.create(SallyClient.prototype);
|
|
206
|
+
Object.assign(c, this);
|
|
207
|
+
(c as any).account = new Wallet(privateKey);
|
|
208
|
+
(c as any)._sender = c._accountAddress(c.account!);
|
|
209
|
+
(c as any)._sendLock = lockFor(c._sender);
|
|
210
|
+
(c as any)._privateW3 = null;
|
|
211
|
+
(c as any)._cache = {};
|
|
212
|
+
return c;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ------------------------------------------------------------------ //
|
|
216
|
+
// Contracts (proxy-fallback aware)
|
|
217
|
+
// ------------------------------------------------------------------ //
|
|
218
|
+
private _contract(address: string, component: string): Contract {
|
|
219
|
+
return new Contract(getAddress(address), deployment.abi(this.chainKey, component) as any, this.w3);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private _cached<T>(key: string, make: () => T): T {
|
|
223
|
+
if (!(key in this._cache)) this._cache[key] = make();
|
|
224
|
+
return this._cache[key];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get swapContract(): Contract {
|
|
228
|
+
return this._cached("swapContract", () => this._contract(this.addresses["swap"], "swap"));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get liquidityContract(): Contract {
|
|
232
|
+
return this._cached("liquidityContract", () => this._contract(this.addresses["liquidity"], "liquidity"));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Lens ABI bound to the **swap proxy** (v2.0.2 read-fallback). */
|
|
236
|
+
get lensContract(): Contract {
|
|
237
|
+
return this._cached("lensContract", () => this._contract(this.addresses["lens"], "lens"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Sidecar ABI bound to the **liq proxy** (v2.0.2 read-fallback). */
|
|
241
|
+
get sidecarContract(): Contract {
|
|
242
|
+
return this._cached("sidecarContract", () => this._contract(this.addresses["sidecar"], "sidecar"));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
token(address: string): Token {
|
|
246
|
+
return new Token(this, address);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ------------------------------------------------------------------ //
|
|
250
|
+
// Call / simulate / send
|
|
251
|
+
// ------------------------------------------------------------------ //
|
|
252
|
+
/** Execute a read (`eth_call`). Decodes reverts into SallyRevert. */
|
|
253
|
+
async call(fn: Fn, args: any[] = [], opts: { value?: bigint; sender?: string | null } = {}): Promise<any> {
|
|
254
|
+
const overrides: Record<string, any> = {};
|
|
255
|
+
if (opts.value) overrides.value = opts.value;
|
|
256
|
+
const sender = opts.sender ?? this.address;
|
|
257
|
+
if (sender) overrides.from = sender;
|
|
258
|
+
try {
|
|
259
|
+
return await fn.staticCall(...args, overrides);
|
|
260
|
+
} catch (exc) {
|
|
261
|
+
throw wrapWeb3Error(this.chainKey, exc);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Dry-run a state-changing call without sending (revert-safe preview). */
|
|
266
|
+
async simulate(fn: Fn, args: any[] = [], opts: { value?: bigint } = {}): Promise<any> {
|
|
267
|
+
return this.call(fn, args, { value: opts.value, sender: this.address });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async estimateGas(fn: Fn, args: any[] = [], opts: { value?: bigint } = {}): Promise<bigint> {
|
|
271
|
+
const overrides: Record<string, any> = { from: this.requireAddress() };
|
|
272
|
+
if (opts.value) overrides.value = opts.value;
|
|
273
|
+
try {
|
|
274
|
+
return BigInt(await fn.estimateGas(...args, overrides));
|
|
275
|
+
} catch (exc) {
|
|
276
|
+
throw wrapWeb3Error(this.chainKey, exc);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Build a fully-populated, signable EIP-1559 transaction. */
|
|
281
|
+
async buildTx(fn: Fn, args: any[] = [], opts: { value?: bigint; gas?: bigint | null } = {}): Promise<Record<string, any>> {
|
|
282
|
+
const sender = this.requireAddress();
|
|
283
|
+
const overrides: Record<string, any> = { from: sender };
|
|
284
|
+
if (opts.value) overrides.value = opts.value;
|
|
285
|
+
let tx: Record<string, any>;
|
|
286
|
+
try {
|
|
287
|
+
tx = { ...(await fn.populateTransaction(...args, overrides)) };
|
|
288
|
+
} catch (exc) {
|
|
289
|
+
throw wrapWeb3Error(this.chainKey, exc);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
tx.nonce = await this.w3.getTransactionCount(sender, "pending");
|
|
293
|
+
tx.chainId = this.chainId;
|
|
294
|
+
if (opts.value) tx.value = opts.value;
|
|
295
|
+
|
|
296
|
+
// EIP-1559 fees (fall back to legacy gasPrice on chains without baseFee).
|
|
297
|
+
const latest = await this.w3.getBlock("latest");
|
|
298
|
+
const feeData = await this.w3.getFeeData();
|
|
299
|
+
if (latest && latest.baseFeePerGas != null) {
|
|
300
|
+
let tip = feeData.maxPriorityFeePerGas;
|
|
301
|
+
if (tip == null) tip = 1_000_000_000n; // 1 gwei fallback
|
|
302
|
+
tx.maxPriorityFeePerGas = tip;
|
|
303
|
+
tx.maxFeePerGas = tip + 2n * latest.baseFeePerGas;
|
|
304
|
+
tx.type = 2;
|
|
305
|
+
delete tx.gasPrice;
|
|
306
|
+
} else {
|
|
307
|
+
tx.gasPrice = feeData.gasPrice ?? 0n;
|
|
308
|
+
tx.type = 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Gas estimate + safety buffer (state-dependent txs can cost more at
|
|
312
|
+
// execution than at estimation). Explicit `gas` overrides below.
|
|
313
|
+
let gas = opts.gas ?? null;
|
|
314
|
+
if (gas === null) {
|
|
315
|
+
gas = await this.estimateGas(fn, args, { value: opts.value });
|
|
316
|
+
if (this.gasMultiplier && this.gasMultiplier !== 1.0) {
|
|
317
|
+
gas = (gas * BigInt(Math.round(this.gasMultiplier * 1000))) / 1000n;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
tx.gasLimit = gas;
|
|
321
|
+
delete tx.gas;
|
|
322
|
+
return tx;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Build -> sign -> send a transaction.
|
|
327
|
+
*
|
|
328
|
+
* With `buildOnly=true` returns the fully-populated **unsigned** tx (no key
|
|
329
|
+
* required — only a sender `address`); sign and broadcast it with your own
|
|
330
|
+
* wallet. Otherwise: with `wait=true` (default) returns the mined receipt and
|
|
331
|
+
* raises SallyRevert on a failed (status 0) tx; with `wait=false` returns the
|
|
332
|
+
* tx hash. `simulate=true` does a cheap `eth_call` first so reverts surface
|
|
333
|
+
* *before* you spend gas.
|
|
334
|
+
*
|
|
335
|
+
* `private` broadcasts through `privateRpcUrl` (a private mempool / MEV-relay).
|
|
336
|
+
* `timeout` (seconds) bounds the receipt wait.
|
|
337
|
+
*/
|
|
338
|
+
async send(
|
|
339
|
+
fn: Fn,
|
|
340
|
+
args: any[] = [],
|
|
341
|
+
opts: {
|
|
342
|
+
value?: bigint;
|
|
343
|
+
gas?: bigint | null;
|
|
344
|
+
wait?: boolean;
|
|
345
|
+
simulate?: boolean;
|
|
346
|
+
private?: boolean | null;
|
|
347
|
+
timeout?: number;
|
|
348
|
+
buildOnly?: boolean;
|
|
349
|
+
} = {},
|
|
350
|
+
): Promise<any> {
|
|
351
|
+
const value = opts.value ?? 0n;
|
|
352
|
+
const wait = opts.wait ?? true;
|
|
353
|
+
const simulate = opts.simulate ?? true;
|
|
354
|
+
const timeout = opts.timeout ?? 180;
|
|
355
|
+
|
|
356
|
+
if (simulate) {
|
|
357
|
+
await this.simulate(fn, args, { value });
|
|
358
|
+
}
|
|
359
|
+
if (opts.buildOnly) {
|
|
360
|
+
return this.buildTx(fn, args, { value, gas: opts.gas ?? null });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const account = this.requireSigner();
|
|
364
|
+
const usePrivate = opts.private == null ? this.privateRpcUrl !== null : opts.private;
|
|
365
|
+
// Hold the per-signer lock across nonce-fetch → broadcast so concurrent sends
|
|
366
|
+
// from one account get distinct, sequential nonces.
|
|
367
|
+
const txh: string = await this._sendLock.run(async () => {
|
|
368
|
+
const tx = await this.buildTx(fn, args, { value, gas: opts.gas ?? null });
|
|
369
|
+
const raw = await account.signTransaction(tx as any);
|
|
370
|
+
let hash: string;
|
|
371
|
+
if (usePrivate) {
|
|
372
|
+
hash = await this._broadcastPrivate(raw);
|
|
373
|
+
} else {
|
|
374
|
+
const resp = await this.w3.broadcastTransaction(raw);
|
|
375
|
+
hash = resp.hash;
|
|
376
|
+
}
|
|
377
|
+
return hash;
|
|
378
|
+
});
|
|
379
|
+
if (!wait) return txh;
|
|
380
|
+
|
|
381
|
+
let receipt;
|
|
382
|
+
try {
|
|
383
|
+
receipt = await this.w3.waitForTransaction(txh, 1, timeout * 1000);
|
|
384
|
+
} catch (exc) {
|
|
385
|
+
receipt = null;
|
|
386
|
+
}
|
|
387
|
+
if (receipt === null) {
|
|
388
|
+
const where = usePrivate ? "private relay" : "public mempool";
|
|
389
|
+
throw new SallyError(
|
|
390
|
+
`tx ${txh} not mined within ${timeout}s via ${where}. It may have been ` +
|
|
391
|
+
`dropped/not included — check the hash before resubmitting.`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (receipt.status !== 1) {
|
|
395
|
+
// A receipt carries no revert data; replay the tx at its block to recover it.
|
|
396
|
+
throw await this._decodeFailedTx(txh, receipt);
|
|
397
|
+
}
|
|
398
|
+
return receipt;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async _decodeFailedTx(txh: string, receipt: any): Promise<SallyError> {
|
|
402
|
+
try {
|
|
403
|
+
const tx = await this.w3.getTransaction(txh);
|
|
404
|
+
if (tx) {
|
|
405
|
+
const callTx = {
|
|
406
|
+
from: tx.from,
|
|
407
|
+
to: tx.to,
|
|
408
|
+
data: tx.data,
|
|
409
|
+
value: tx.value,
|
|
410
|
+
gasLimit: tx.gasLimit,
|
|
411
|
+
};
|
|
412
|
+
await this.w3.call({ ...callTx, blockTag: receipt.blockNumber - 1 });
|
|
413
|
+
}
|
|
414
|
+
} catch (exc: any) {
|
|
415
|
+
const err = wrapWeb3Error(this.chainKey, exc);
|
|
416
|
+
return new SallyError(`tx ${txh} reverted on-chain: ${err.toString()}`);
|
|
417
|
+
}
|
|
418
|
+
return new SallyRevert("Error", [], null, `tx ${txh} reverted on-chain`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ------------------------------------------------------------------ //
|
|
422
|
+
// Preview (sign-externally): build + simulate + ABI-decode, no key
|
|
423
|
+
// ------------------------------------------------------------------ //
|
|
424
|
+
/** ABI-decode a contract call into `[functionName, { arg: value }]`. */
|
|
425
|
+
decodeCall(fn: Fn, args: any[] = []): [string, Record<string, any>] {
|
|
426
|
+
const fragment = fn.fragment ?? {};
|
|
427
|
+
const inputs = fragment.inputs ?? [];
|
|
428
|
+
const out: Record<string, any> = {};
|
|
429
|
+
inputs.forEach((inp: any, n: number) => {
|
|
430
|
+
out[inp.name || `arg${n}`] = args[n];
|
|
431
|
+
});
|
|
432
|
+
return [fragment.name ?? "?", out];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Build an unsigned tx + simulate + decode — see it before you broadcast.
|
|
437
|
+
*
|
|
438
|
+
* Returns a {@link TxPreview} with the unsigned transaction (sign it with your
|
|
439
|
+
* own wallet), the decoded function/args, the simulated `eth_call` result, the
|
|
440
|
+
* decoded revert reason if it would fail, and the gas estimate. Needs only a
|
|
441
|
+
* sender `address` — no key.
|
|
442
|
+
*/
|
|
443
|
+
async preview(fn: Fn, args: any[] = [], opts: { value?: bigint } = {}): Promise<TxPreview> {
|
|
444
|
+
const value = opts.value ?? 0n;
|
|
445
|
+
const sender = this.requireAddress();
|
|
446
|
+
const [name, decodedArgs] = this.decodeCall(fn, args);
|
|
447
|
+
|
|
448
|
+
let simulated: any = null;
|
|
449
|
+
let revert: string | null = null;
|
|
450
|
+
let gas: bigint | null = null;
|
|
451
|
+
let to = this.addresses["swap"];
|
|
452
|
+
try {
|
|
453
|
+
simulated = await this.call(fn, args, { value, sender });
|
|
454
|
+
gas = await this.estimateGas(fn, args, { value });
|
|
455
|
+
} catch (exc: any) {
|
|
456
|
+
if (exc instanceof SallyConfigError) throw exc;
|
|
457
|
+
revert = String(exc); // Python: str(exc) — SallyRevert renders "<name>: <detail>"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let unsigned: Record<string, any> = {};
|
|
461
|
+
if (revert === null) {
|
|
462
|
+
unsigned = await this.buildTx(fn, args, { value });
|
|
463
|
+
if (unsigned.to) to = unsigned.to;
|
|
464
|
+
}
|
|
465
|
+
return new TxPreview({
|
|
466
|
+
unsignedTx: unsigned,
|
|
467
|
+
sender,
|
|
468
|
+
to: String(to),
|
|
469
|
+
function: name,
|
|
470
|
+
args: decodedArgs,
|
|
471
|
+
value,
|
|
472
|
+
simulatedReturn: simulated,
|
|
473
|
+
revert,
|
|
474
|
+
gas,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** POST the raw tx to the private relay; return the tx hash. */
|
|
479
|
+
private async _broadcastPrivate(rawTx: string): Promise<string> {
|
|
480
|
+
if (!this.privateRpcUrl) {
|
|
481
|
+
throw new SallyConfigError(
|
|
482
|
+
"private=true but no privateRpcUrl configured. Pass " +
|
|
483
|
+
"new SallyClient(..., { privateRpcUrl: 'https://rpc.flashbots.net/fast' }).",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (this._privateW3 === null) {
|
|
487
|
+
this._privateW3 = new JsonRpcProvider(this.privateRpcUrl, Network.from(this.chainId), {
|
|
488
|
+
staticNetwork: Network.from(this.chainId),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const resp = await this._privateW3.broadcastTransaction(rawTx);
|
|
492
|
+
return resp.hash;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ------------------------------------------------------------------ //
|
|
496
|
+
// Namespaces (lazy)
|
|
497
|
+
// ------------------------------------------------------------------ //
|
|
498
|
+
get prices(): Prices {
|
|
499
|
+
return this._cached("prices", () => new Prices(this));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
get swap(): Swap {
|
|
503
|
+
return this._cached("swap", () => new Swap(this));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
get wallet(): WalletModule {
|
|
507
|
+
return this._cached("wallet", () => new WalletModule(this));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
get liquidity(): Liquidity {
|
|
511
|
+
return this._cached("liquidity", () => new Liquidity(this));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
get fees(): Fees {
|
|
515
|
+
return this._cached("fees", () => new Fees(this));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
get permit2(): Permit2 {
|
|
519
|
+
return this._cached("permit2", () => new Permit2(this));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
toString(): string {
|
|
523
|
+
const who = this.address ?? "read-only";
|
|
524
|
+
return `<SallyClient chain=${this.chainKey} id=${this.chainId} signer=${who}>`;
|
|
525
|
+
}
|
|
526
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Well-known token addresses, handy for examples and tests.
|
|
3
|
+
*
|
|
4
|
+
* These are convenience constants only — the SDK itself never depends on them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NATIVE, ZERO } from "./token.js";
|
|
8
|
+
|
|
9
|
+
// Base (chain 8453)
|
|
10
|
+
export const Base = {
|
|
11
|
+
WETH: "0x4200000000000000000000000000000000000006",
|
|
12
|
+
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
13
|
+
USDbC: "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
|
|
14
|
+
DAI: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
|
15
|
+
cbBTC: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
// BSC (chain 56)
|
|
19
|
+
export const Bsc = {
|
|
20
|
+
WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
21
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955",
|
|
22
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
23
|
+
CAKE: "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export const NATIVE_TOKEN = NATIVE;
|
|
27
|
+
export const ZERO_ADDRESS = ZERO;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Known private-mempool / MEV-protection relay RPCs.
|
|
31
|
+
*
|
|
32
|
+
* Pass one as `new SallyClient(..., { privateRpcUrl: PrivateRelays.X })`.
|
|
33
|
+
* Flashbots Protect is Ethereum-L1 only; BSC has 48.club / bloXroute; Base's
|
|
34
|
+
* sequencer is centralized (limited public-mempool MEV), so a private relay
|
|
35
|
+
* matters most on L1 — verify your chain actually has one before relying on it.
|
|
36
|
+
*/
|
|
37
|
+
export const PrivateRelays = {
|
|
38
|
+
FLASHBOTS_FAST: "https://rpc.flashbots.net/fast", // ETH mainnet
|
|
39
|
+
FLASHBOTS: "https://rpc.flashbots.net", // ETH mainnet
|
|
40
|
+
MEVBLOCKER: "https://rpc.mevblocker.io", // ETH mainnet
|
|
41
|
+
BSC_48CLUB: "https://rpc.48.club", // BSC
|
|
42
|
+
BSC_BLOXROUTE: "https://bsc.rpc.blxrbdn.com", // BSC
|
|
43
|
+
} as const;
|