@augustdigital/sdk 8.12.0 → 8.14.0
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/lib/core/analytics/method-taxonomy.js +2 -0
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/core/constants/swap-router.d.ts +44 -4
- package/lib/core/constants/swap-router.js +49 -5
- package/lib/core/helpers/revert-decode.d.ts +248 -0
- package/lib/core/helpers/revert-decode.js +515 -0
- package/lib/core/helpers/swap-router.d.ts +132 -4
- package/lib/core/helpers/swap-router.js +337 -4
- package/lib/core/helpers/vault-version.d.ts +15 -0
- package/lib/core/helpers/vault-version.js +15 -1
- package/lib/core/helpers/vaults.d.ts +41 -0
- package/lib/core/helpers/vaults.js +53 -0
- package/lib/core/index.d.ts +1 -0
- package/lib/core/index.js +1 -0
- package/lib/main.d.ts +46 -0
- package/lib/main.js +46 -0
- package/lib/modules/vaults/main.d.ts +53 -0
- package/lib/modules/vaults/main.js +73 -0
- package/lib/modules/vaults/utils.js +4 -4
- package/lib/sdk.d.ts +594 -8
- package/package.json +3 -3
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decodeRevertData = decodeRevertData;
|
|
4
|
+
exports.extractRevertFromErrorText = extractRevertFromErrorText;
|
|
5
|
+
exports.lookupSelector = lookupSelector;
|
|
6
|
+
exports.simulateCall = simulateCall;
|
|
7
|
+
exports.readTokenState = readTokenState;
|
|
8
|
+
const ethers_1 = require("ethers");
|
|
9
|
+
const abis_1 = require("../../abis");
|
|
10
|
+
/**
|
|
11
|
+
* Revert-data decoding helpers.
|
|
12
|
+
*
|
|
13
|
+
* Leaf module (depends only on `ethers` and `abis/`) that turns raw EVM revert
|
|
14
|
+
* blobs — the `data="0x…"` payload an ethers v6 `CALL_EXCEPTION` carries — into
|
|
15
|
+
* named errors with decoded arguments. The core decode path
|
|
16
|
+
* ({@link decodeRevertData}, {@link extractRevertFromErrorText}) is pure and
|
|
17
|
+
* offline: no RPC, no network, safe in both browser and Node. The network
|
|
18
|
+
* helpers ({@link lookupSelector}, {@link simulateCall},
|
|
19
|
+
* {@link readTokenState}) are exported separately so tree-shaken offline
|
|
20
|
+
* consumers never pull them in.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
/** ABI selector for the standard `Error(string)` revert (`require` with a message). */
|
|
25
|
+
const ERROR_STRING_SELECTOR = '0x08c379a0';
|
|
26
|
+
/** ABI selector for the compiler-inserted `Panic(uint256)` revert. */
|
|
27
|
+
const PANIC_SELECTOR = '0x4e487b71';
|
|
28
|
+
/**
|
|
29
|
+
* Human-readable names for the Solidity `Panic(uint256)` codes, as defined in
|
|
30
|
+
* the Solidity documentation ("Panic via assert and Error via require").
|
|
31
|
+
*/
|
|
32
|
+
const PANIC_CODE_NAMES = {
|
|
33
|
+
0: 'generic compiler-inserted panic',
|
|
34
|
+
1: 'assertion failed (assert)',
|
|
35
|
+
17: 'arithmetic overflow or underflow',
|
|
36
|
+
18: 'division or modulo by zero',
|
|
37
|
+
33: 'value invalid for enum type',
|
|
38
|
+
34: 'incorrectly encoded storage byte array',
|
|
39
|
+
49: 'pop() on an empty array',
|
|
40
|
+
50: 'array index out of bounds',
|
|
41
|
+
65: 'allocation of too much memory or array too large',
|
|
42
|
+
81: 'call to a zero-initialized variable of internal function type',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Well-known custom-error signatures included in the built-in decode corpus,
|
|
46
|
+
* declared as human-readable fragments:
|
|
47
|
+
*
|
|
48
|
+
* - OpenZeppelin ERC-6093 error sets for ERC-20 / ERC-721 / ERC-1155
|
|
49
|
+
* (`draft-IERC6093.sol`, OpenZeppelin Contracts 5.x).
|
|
50
|
+
* - OpenZeppelin `SafeERC20`, `ERC4626`, `Ownable`, `Pausable`, and
|
|
51
|
+
* `ReentrancyGuard` errors (OpenZeppelin Contracts 5.x).
|
|
52
|
+
* - LayerZero V2 `OApp`/`OFT` errors (`IOAppCore.sol`, `OFTCore.sol`) — the
|
|
53
|
+
* error surface of the SDK's cross-chain OVault flows.
|
|
54
|
+
*/
|
|
55
|
+
const STANDARD_ERROR_SIGNATURES = [
|
|
56
|
+
'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
|
|
57
|
+
'error ERC20InvalidSender(address sender)',
|
|
58
|
+
'error ERC20InvalidReceiver(address receiver)',
|
|
59
|
+
'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
|
|
60
|
+
'error ERC20InvalidApprover(address approver)',
|
|
61
|
+
'error ERC20InvalidSpender(address spender)',
|
|
62
|
+
'error ERC721InvalidOwner(address owner)',
|
|
63
|
+
'error ERC721NonexistentToken(uint256 tokenId)',
|
|
64
|
+
'error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner)',
|
|
65
|
+
'error ERC721InvalidSender(address sender)',
|
|
66
|
+
'error ERC721InvalidReceiver(address receiver)',
|
|
67
|
+
'error ERC721InsufficientApproval(address operator, uint256 tokenId)',
|
|
68
|
+
'error ERC721InvalidApprover(address approver)',
|
|
69
|
+
'error ERC721InvalidOperator(address operator)',
|
|
70
|
+
'error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId)',
|
|
71
|
+
'error ERC1155InvalidSender(address sender)',
|
|
72
|
+
'error ERC1155InvalidReceiver(address receiver)',
|
|
73
|
+
'error ERC1155MissingApprovalForAll(address operator, address owner)',
|
|
74
|
+
'error ERC1155InvalidApprover(address approver)',
|
|
75
|
+
'error ERC1155InvalidOperator(address operator)',
|
|
76
|
+
'error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength)',
|
|
77
|
+
'error SafeERC20FailedOperation(address token)',
|
|
78
|
+
'error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease)',
|
|
79
|
+
'error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max)',
|
|
80
|
+
'error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max)',
|
|
81
|
+
'error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max)',
|
|
82
|
+
'error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max)',
|
|
83
|
+
'error OwnableUnauthorizedAccount(address account)',
|
|
84
|
+
'error OwnableInvalidOwner(address owner)',
|
|
85
|
+
'error EnforcedPause()',
|
|
86
|
+
'error ExpectedPause()',
|
|
87
|
+
'error ReentrancyGuardReentrantCall()',
|
|
88
|
+
'error NoPeer(uint32 eid)',
|
|
89
|
+
'error OnlyPeer(uint32 eid, bytes32 sender)',
|
|
90
|
+
'error OnlyEndpoint(address addr)',
|
|
91
|
+
'error InvalidDelegate()',
|
|
92
|
+
'error InvalidEndpointCall()',
|
|
93
|
+
'error LzTokenUnavailable()',
|
|
94
|
+
'error NotEnoughNative(uint256 msgValue)',
|
|
95
|
+
'error SlippageExceeded(uint256 amountLD, uint256 minAmountLD)',
|
|
96
|
+
'error InvalidLocalDecimals()',
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* ABIs already shipped by the SDK whose `error` fragments join the corpus.
|
|
100
|
+
* Covers the August/Upshift vault estate: tokenized vaults (plus their
|
|
101
|
+
* receipt / permit / whitelist companions), the SwapRouter, lending pools,
|
|
102
|
+
* the fee oracle, the native-deposit wrapper, Multicall3, and the RWA redeem
|
|
103
|
+
* subaccount.
|
|
104
|
+
*/
|
|
105
|
+
const CORPUS_ABIS = [
|
|
106
|
+
STANDARD_ERROR_SIGNATURES,
|
|
107
|
+
abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
108
|
+
abis_1.ABI_TOKENIZED_VAULT_V2_RECEIPT,
|
|
109
|
+
abis_1.ABI_TOKENIZED_VAULT_V2_DEPOSIT_WITH_PERMIT,
|
|
110
|
+
abis_1.ABI_TOKENIZED_VAULT_V2_WHITELISTED_ASSETS,
|
|
111
|
+
abis_1.ABI_TOKENIZED_VAULT_V2_WHITELISTED_ALLOCATION,
|
|
112
|
+
abis_1.ABI_TOKENIZED_VAULT_V2_SENDER_ALLOCATION_WHITELIST,
|
|
113
|
+
abis_1.ABI_MULTI_ASSET_NATIVE_DEPOSIT_WRAPPER,
|
|
114
|
+
abis_1.ABI_SWAP_ROUTER,
|
|
115
|
+
abis_1.ABI_LENDING_POOL_V3,
|
|
116
|
+
abis_1.ABI_FEE_ORACLE,
|
|
117
|
+
abis_1.ABI_MULTICALL3,
|
|
118
|
+
abis_1.RWA_REDEEM_SUBACCOUNT,
|
|
119
|
+
];
|
|
120
|
+
let corpusInterface;
|
|
121
|
+
function getCorpusInterface() {
|
|
122
|
+
if (corpusInterface)
|
|
123
|
+
return corpusInterface;
|
|
124
|
+
const bySelector = new Map();
|
|
125
|
+
for (const abi of CORPUS_ABIS) {
|
|
126
|
+
const iface = new ethers_1.Interface(abi);
|
|
127
|
+
iface.forEachError((fragment) => {
|
|
128
|
+
if (!bySelector.has(fragment.selector)) {
|
|
129
|
+
bySelector.set(fragment.selector, fragment);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
corpusInterface = new ethers_1.Interface([...bySelector.values()]);
|
|
134
|
+
return corpusInterface;
|
|
135
|
+
}
|
|
136
|
+
function decodeWithFragment(iface, fragment, data, selector) {
|
|
137
|
+
const base = {
|
|
138
|
+
kind: 'custom',
|
|
139
|
+
selector,
|
|
140
|
+
name: fragment.name,
|
|
141
|
+
signature: fragment.format('sighash'),
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
const args = iface.decodeErrorResult(fragment, data);
|
|
145
|
+
return { ...base, args: Array.from(args) };
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
reason: `matched ${fragment.format('sighash')} but the argument data is truncated or malformed`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Decode a raw EVM revert blob into a named error with arguments.
|
|
156
|
+
*
|
|
157
|
+
* Pure and offline — no RPC calls, no network. Handles the three revert
|
|
158
|
+
* shapes the EVM produces:
|
|
159
|
+
*
|
|
160
|
+
* 1. `Error(string)` (`0x08c379a0`) — a `require`/`revert` with a message.
|
|
161
|
+
* 2. `Panic(uint256)` (`0x4e487b71`) — compiler-inserted checks, with the
|
|
162
|
+
* numeric code translated to its documented meaning (overflow, division by
|
|
163
|
+
* zero, …).
|
|
164
|
+
* 3. Custom errors — matched against a built-in corpus of OpenZeppelin
|
|
165
|
+
* ERC-6093 / SafeERC20 / ERC4626 / Ownable / Pausable errors, LayerZero V2
|
|
166
|
+
* OApp/OFT errors, and every `error` fragment in the vault ABIs the SDK
|
|
167
|
+
* ships (tokenized vaults, SwapRouter, lending pools, …). Callers can
|
|
168
|
+
* extend the corpus per call via `opts.extraAbis`, which take precedence.
|
|
169
|
+
*
|
|
170
|
+
* Truncated input degrades gracefully: when the 4-byte selector matches a
|
|
171
|
+
* known error but the argument bytes are incomplete (e.g. an alert pipeline
|
|
172
|
+
* clipped the hex), the result still carries `kind: 'custom'`, `name`, and
|
|
173
|
+
* `signature`, with `args` omitted and `reason` explaining the truncation.
|
|
174
|
+
*
|
|
175
|
+
* @param data - The revert blob as a `0x`-prefixed hex string (the `data`
|
|
176
|
+
* field of an ethers v6 `CALL_EXCEPTION`). Anything shorter than a 4-byte
|
|
177
|
+
* selector, or not hex at all, yields `kind: 'unknown'`.
|
|
178
|
+
* @param opts - Optional settings.
|
|
179
|
+
* @param opts.extraAbis - Additional ABIs whose `error` fragments are checked
|
|
180
|
+
* before the built-in corpus.
|
|
181
|
+
* @returns The decoded revert. Never throws; unrecognized selectors return
|
|
182
|
+
* `kind: 'unknown'` with the selector preserved so callers can look it up
|
|
183
|
+
* via {@link lookupSelector}.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* const decoded = decodeRevertData('0xe450d38c…');
|
|
188
|
+
* // {
|
|
189
|
+
* // kind: 'custom',
|
|
190
|
+
* // selector: '0xe450d38c',
|
|
191
|
+
* // name: 'ERC20InsufficientBalance',
|
|
192
|
+
* // signature: 'ERC20InsufficientBalance(address,uint256,uint256)',
|
|
193
|
+
* // args: ['0x9281…', 487711967n, 1261711967n],
|
|
194
|
+
* // }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
function decodeRevertData(data, opts) {
|
|
198
|
+
const hex = typeof data === 'string' ? data.trim() : '';
|
|
199
|
+
if (!/^0x[0-9a-fA-F]*$/.test(hex) || hex.length < 10) {
|
|
200
|
+
return { kind: 'unknown', selector: hex.slice(0, 10).toLowerCase() };
|
|
201
|
+
}
|
|
202
|
+
const selector = hex.slice(0, 10).toLowerCase();
|
|
203
|
+
if (selector === ERROR_STRING_SELECTOR) {
|
|
204
|
+
try {
|
|
205
|
+
const [message] = ethers_1.AbiCoder.defaultAbiCoder().decode(['string'], `0x${hex.slice(10)}`);
|
|
206
|
+
return {
|
|
207
|
+
kind: 'error-string',
|
|
208
|
+
selector,
|
|
209
|
+
name: 'Error',
|
|
210
|
+
signature: 'Error(string)',
|
|
211
|
+
args: [message],
|
|
212
|
+
reason: message,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'error-string',
|
|
218
|
+
selector,
|
|
219
|
+
name: 'Error',
|
|
220
|
+
signature: 'Error(string)',
|
|
221
|
+
reason: 'Error(string) revert with truncated or malformed message data',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (selector === PANIC_SELECTOR) {
|
|
226
|
+
try {
|
|
227
|
+
const [code] = ethers_1.AbiCoder.defaultAbiCoder().decode(['uint256'], `0x${hex.slice(10)}`);
|
|
228
|
+
const named = PANIC_CODE_NAMES[Number(code)];
|
|
229
|
+
return {
|
|
230
|
+
kind: 'panic',
|
|
231
|
+
selector,
|
|
232
|
+
name: 'Panic',
|
|
233
|
+
signature: 'Panic(uint256)',
|
|
234
|
+
args: [code],
|
|
235
|
+
reason: named
|
|
236
|
+
? `panic 0x${code.toString(16).padStart(2, '0')}: ${named}`
|
|
237
|
+
: `panic 0x${code.toString(16)}: unrecognized panic code`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return {
|
|
242
|
+
kind: 'panic',
|
|
243
|
+
selector,
|
|
244
|
+
name: 'Panic',
|
|
245
|
+
signature: 'Panic(uint256)',
|
|
246
|
+
reason: 'Panic(uint256) revert with truncated or malformed code data',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const abi of opts?.extraAbis ?? []) {
|
|
251
|
+
try {
|
|
252
|
+
const iface = new ethers_1.Interface(abi);
|
|
253
|
+
const fragment = iface.getError(selector);
|
|
254
|
+
if (fragment)
|
|
255
|
+
return decodeWithFragment(iface, fragment, hex, selector);
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
}
|
|
259
|
+
const corpus = getCorpusInterface();
|
|
260
|
+
const fragment = corpus.getError(selector);
|
|
261
|
+
if (fragment)
|
|
262
|
+
return decodeWithFragment(corpus, fragment, hex, selector);
|
|
263
|
+
return { kind: 'unknown', selector };
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Parse an ethers v6 error *string* and recover the machine-readable fields
|
|
267
|
+
* embedded in it.
|
|
268
|
+
*
|
|
269
|
+
* ethers v6 serializes rich error objects into a single message of the shape
|
|
270
|
+
* `… (action="estimateGas", data="0x…", reason=…, transaction={ "data":
|
|
271
|
+
* "0x…", "from": "0x…", "to": "0x…" }, …, code=CALL_EXCEPTION, version=…)` —
|
|
272
|
+
* exactly what error-alert pipelines end up carrying once the original error
|
|
273
|
+
* object is stringified. This helper regex-extracts the revert `data`, the
|
|
274
|
+
* `action`, the `code`, and the embedded transaction fields.
|
|
275
|
+
*
|
|
276
|
+
* Pure and offline. Tolerant of truncation: alert transports often cap
|
|
277
|
+
* message length mid-hex, so every field is matched without requiring its
|
|
278
|
+
* closing quote and the result simply omits whatever did not survive.
|
|
279
|
+
*
|
|
280
|
+
* @param text - The error text to parse (an ethers v6 `error.message`, or a
|
|
281
|
+
* log/alert line that embeds one).
|
|
282
|
+
* @returns The recovered fields; an empty object when nothing matched. Never
|
|
283
|
+
* throws.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```ts
|
|
287
|
+
* const ctx = extractRevertFromErrorText(alertErrorLine);
|
|
288
|
+
* if (ctx.revertData) console.log(decodeRevertData(ctx.revertData));
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
function extractRevertFromErrorText(text) {
|
|
292
|
+
const out = {};
|
|
293
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
294
|
+
return out;
|
|
295
|
+
const action = text.match(/\baction="([^"]+)"/);
|
|
296
|
+
if (action)
|
|
297
|
+
out.action = action[1];
|
|
298
|
+
const code = text.match(/\bcode=([A-Z_]+)/);
|
|
299
|
+
if (code)
|
|
300
|
+
out.code = code[1];
|
|
301
|
+
const revertData = text.match(/[(,]\s*data="(0x[0-9a-fA-F]*)/);
|
|
302
|
+
if (revertData)
|
|
303
|
+
out.revertData = revertData[1];
|
|
304
|
+
const txData = text.match(/"data"\s*:\s*"(0x[0-9a-fA-F]*)/);
|
|
305
|
+
const txFrom = text.match(/"from"\s*:\s*"(0x[0-9a-fA-F]*)/);
|
|
306
|
+
const txTo = text.match(/"to"\s*:\s*"(0x[0-9a-fA-F]*)/);
|
|
307
|
+
if (txData || txFrom || txTo) {
|
|
308
|
+
out.tx = {};
|
|
309
|
+
if (txFrom)
|
|
310
|
+
out.tx.from = txFrom[1];
|
|
311
|
+
if (txTo)
|
|
312
|
+
out.tx.to = txTo[1];
|
|
313
|
+
if (txData)
|
|
314
|
+
out.tx.data = txData[1];
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Look up candidate signatures for an unknown 4-byte selector against public
|
|
320
|
+
* signature databases.
|
|
321
|
+
*
|
|
322
|
+
* Network helper — queries [openchain.xyz](https://openchain.xyz) first and
|
|
323
|
+
* falls back to [4byte.directory](https://www.4byte.directory). Kept separate
|
|
324
|
+
* from {@link decodeRevertData} so the core decode path stays offline.
|
|
325
|
+
* Worst case 2 HTTPS requests.
|
|
326
|
+
*
|
|
327
|
+
* @param selector - The selector to look up. Longer hex (a full revert blob)
|
|
328
|
+
* is accepted; only the first 4 bytes are used.
|
|
329
|
+
* @returns Candidate signatures (e.g. `['ERC20InsufficientBalance(address,uint256,uint256)']`),
|
|
330
|
+
* most-likely first. Empty when the input is not hex, no database knows the
|
|
331
|
+
* selector, or both databases were unreachable. Never throws.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* const candidates = await lookupSelector('0xe450d38c');
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
async function lookupSelector(selector) {
|
|
339
|
+
const hex = typeof selector === 'string' ? selector.trim() : '';
|
|
340
|
+
if (!/^0x[0-9a-fA-F]{8,}$/.test(hex))
|
|
341
|
+
return [];
|
|
342
|
+
const sel = hex.slice(0, 10).toLowerCase();
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetch(`https://api.openchain.xyz/signature-database/v1/lookup?function=${sel}&filter=true`);
|
|
345
|
+
if (res.ok) {
|
|
346
|
+
const body = (await res.json());
|
|
347
|
+
const entries = body.result?.function?.[sel];
|
|
348
|
+
const names = (entries ?? [])
|
|
349
|
+
.map((e) => e.name)
|
|
350
|
+
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
351
|
+
if (names.length > 0)
|
|
352
|
+
return [...new Set(names)];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* fall through to 4byte */
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(`https://www.4byte.directory/api/v1/signatures/?hex_signature=${sel}&ordering=created_at`);
|
|
360
|
+
if (res.ok) {
|
|
361
|
+
const body = (await res.json());
|
|
362
|
+
const names = (body.results ?? [])
|
|
363
|
+
.map((r) => r.text_signature)
|
|
364
|
+
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
365
|
+
return [...new Set(names)];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
/* both sources unreachable */
|
|
370
|
+
}
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
function extractRevertDataFromError(error) {
|
|
374
|
+
if (!error || typeof error !== 'object')
|
|
375
|
+
return undefined;
|
|
376
|
+
const e = error;
|
|
377
|
+
const candidates = [e.data, e.error?.data, e.info?.error?.data];
|
|
378
|
+
for (const c of candidates) {
|
|
379
|
+
if (typeof c === 'string' && /^0x[0-9a-fA-F]*$/.test(c) && c.length >= 10) {
|
|
380
|
+
return c;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Replay a call via `eth_call` and decode the revert when it fails.
|
|
387
|
+
*
|
|
388
|
+
* The primary use is re-executing a transaction that failed at `estimateGas`
|
|
389
|
+
* (so no transaction hash ever existed): the node returns the *full* revert
|
|
390
|
+
* blob, which both confirms the failure still reproduces and recovers
|
|
391
|
+
* untruncated revert data even when the original error text was clipped in
|
|
392
|
+
* transit. Passing a historical `blockTag` replays against archival state
|
|
393
|
+
* (requires an archive-capable RPC).
|
|
394
|
+
*
|
|
395
|
+
* Exactly 1 RPC call (`eth_call`).
|
|
396
|
+
*
|
|
397
|
+
* @param provider - A connected `JsonRpcProvider` for the target chain, e.g.
|
|
398
|
+
* from `createProvider(rpcUrl, chainId)`. Note that some listed public RPCs
|
|
399
|
+
* reject keyless `eth_call` — supply a working endpoint via the SDK's RPC
|
|
400
|
+
* precedence (constructor `providers` map / `AUGUST_RPC_<chainId>` env).
|
|
401
|
+
* @param tx - The call to replay: `to` and `data` are required, `from`
|
|
402
|
+
* matters whenever the target checks `msg.sender`.
|
|
403
|
+
* @param blockTag - Optional block to execute against (`'latest'` by
|
|
404
|
+
* default; a number or hex tag replays historical state).
|
|
405
|
+
* @param opts - Optional settings forwarded to {@link decodeRevertData}.
|
|
406
|
+
* @param opts.extraAbis - Additional ABIs checked before the built-in corpus
|
|
407
|
+
* when decoding a revert.
|
|
408
|
+
* @returns `{ ok: true, returnData }` on success; on failure `{ ok: false }`
|
|
409
|
+
* with the fresh `revertData` and its decode when the node supplied one.
|
|
410
|
+
* Never throws on revert — only on programmer error (e.g. no provider).
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```ts
|
|
414
|
+
* const result = await simulateCall(provider, {
|
|
415
|
+
* from: eoa,
|
|
416
|
+
* to: vault,
|
|
417
|
+
* data: calldata,
|
|
418
|
+
* });
|
|
419
|
+
* if (!result.ok && result.decoded) console.log(result.decoded.signature);
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
async function simulateCall(provider, tx, blockTag, opts) {
|
|
423
|
+
try {
|
|
424
|
+
const returnData = await provider.call({
|
|
425
|
+
from: tx.from,
|
|
426
|
+
to: tx.to,
|
|
427
|
+
data: tx.data,
|
|
428
|
+
blockTag,
|
|
429
|
+
});
|
|
430
|
+
return { ok: true, returnData };
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const revertData = extractRevertDataFromError(error);
|
|
434
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
435
|
+
if (!revertData)
|
|
436
|
+
return { ok: false, errorMessage };
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
revertData,
|
|
440
|
+
decoded: decodeRevertData(revertData, opts),
|
|
441
|
+
errorMessage,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const TOKEN_STATE_ABI = [
|
|
446
|
+
'function symbol() view returns (string)',
|
|
447
|
+
'function decimals() view returns (uint8)',
|
|
448
|
+
'function balanceOf(address account) view returns (uint256)',
|
|
449
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
450
|
+
'function isAccountFrozen(address account) view returns (bool)',
|
|
451
|
+
'function isBlacklisted(address account) view returns (bool)',
|
|
452
|
+
'function isFrozen(address account) view returns (bool)',
|
|
453
|
+
];
|
|
454
|
+
/**
|
|
455
|
+
* Read a bounded, fixed set of token-state probes for a holder — the exact
|
|
456
|
+
* facts needed to explain a failed deposit/transfer: what the token is, what
|
|
457
|
+
* the holder has, what the spender may pull, and whether the issuer froze the
|
|
458
|
+
* holder.
|
|
459
|
+
*
|
|
460
|
+
* The probe set is deliberately fixed (no arbitrary calldata): `symbol()`,
|
|
461
|
+
* `decimals()`, `balanceOf(holder)`, `allowance(holder, spender)` when a
|
|
462
|
+
* spender is given, and the compliance getters tried in order —
|
|
463
|
+
* `isAccountFrozen(address)` (Agora AUSD, selector `0xe816d97f`),
|
|
464
|
+
* `isBlacklisted(address)` (USDC-style), `isFrozen(address)`. Probes the
|
|
465
|
+
* token does not implement resolve to `undefined`; this function never
|
|
466
|
+
* throws.
|
|
467
|
+
*
|
|
468
|
+
* Worst case 7 RPC calls (4 base probes + 3 compliance getters), batched by
|
|
469
|
+
* the provider where supported.
|
|
470
|
+
*
|
|
471
|
+
* @param provider - A connected `JsonRpcProvider` for the token's chain.
|
|
472
|
+
* @param token - The ERC-20 token contract address.
|
|
473
|
+
* @param holder - The account whose balance/compliance state to read.
|
|
474
|
+
* @param spender - Optional spender for the `allowance(holder, spender)`
|
|
475
|
+
* probe (e.g. the vault a deposit approves).
|
|
476
|
+
* @returns The token state with every unresolvable probe as `undefined`.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```ts
|
|
480
|
+
* const state = await readTokenState(provider, ausd, eoa, vault);
|
|
481
|
+
* // { symbol: 'AUSD', decimals: 6, balance: 487711967n, allowance: 0n, frozen: false }
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
async function readTokenState(provider, token, holder, spender) {
|
|
485
|
+
const contract = new ethers_1.Contract(token, TOKEN_STATE_ABI, provider);
|
|
486
|
+
const out = {};
|
|
487
|
+
const [symbol, decimals, balance, allowance] = await Promise.allSettled([
|
|
488
|
+
contract.symbol(),
|
|
489
|
+
contract.decimals(),
|
|
490
|
+
contract.balanceOf(holder),
|
|
491
|
+
spender
|
|
492
|
+
? contract.allowance(holder, spender)
|
|
493
|
+
: Promise.reject(new Error('no spender')),
|
|
494
|
+
]);
|
|
495
|
+
if (symbol.status === 'fulfilled')
|
|
496
|
+
out.symbol = symbol.value;
|
|
497
|
+
if (decimals.status === 'fulfilled')
|
|
498
|
+
out.decimals = Number(decimals.value);
|
|
499
|
+
if (balance.status === 'fulfilled')
|
|
500
|
+
out.balance = balance.value;
|
|
501
|
+
if (spender && allowance.status === 'fulfilled') {
|
|
502
|
+
out.allowance = allowance.value;
|
|
503
|
+
}
|
|
504
|
+
const complianceProbes = ['isAccountFrozen', 'isBlacklisted', 'isFrozen'];
|
|
505
|
+
for (const probe of complianceProbes) {
|
|
506
|
+
try {
|
|
507
|
+
const frozen = (await contract[probe](holder));
|
|
508
|
+
out.frozen = Boolean(frozen);
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
catch { }
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
//# sourceMappingURL=revert-decode.js.map
|
|
@@ -12,6 +12,26 @@ import type { IAddress, ISwapRouterDepositResult, ISwapRouterDepositResultOption
|
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
14
|
export declare function getSwapRouterAddress(chainId: number): IAddress | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the human-readable display name of the `SwapRouter` periphery
|
|
17
|
+
* contract for a chain.
|
|
18
|
+
*
|
|
19
|
+
* The contract exposes no name on-chain, so this reads the SDK-maintained
|
|
20
|
+
* {@link SWAP_ROUTER_NAMES} registry. Intended for UI surfaces that label the
|
|
21
|
+
* contract a swap-and-deposit routes through (e.g. a "Router" row in a
|
|
22
|
+
* transaction overview) — display metadata only, never routing logic.
|
|
23
|
+
*
|
|
24
|
+
* @param chainId - EVM chain ID.
|
|
25
|
+
* @returns The display name (e.g. `'Upshift Swap Router'`), or `undefined`
|
|
26
|
+
* when no SwapRouter is deployed on the chain — callers should omit the
|
|
27
|
+
* label rather than show an address.
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const name = getSwapRouterName(1);
|
|
31
|
+
* // 'Upshift Swap Router'
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function getSwapRouterName(chainId: number): string | undefined;
|
|
15
35
|
/**
|
|
16
36
|
* Whether a vault is eligible for the any-token `SwapRouter` deposit surface
|
|
17
37
|
* (the SwapRouter is `enableVault`-ed for it on-chain). Address comparison is
|
|
@@ -23,13 +43,20 @@ export declare function getSwapRouterAddress(chainId: number): IAddress | undefi
|
|
|
23
43
|
*
|
|
24
44
|
* @param vaultAddress - August vault address.
|
|
25
45
|
* @returns `true` when the vault is in {@link SWAP_ROUTER_ELIGIBLE_VAULTS}.
|
|
46
|
+
* @deprecated Superseded by {@link getSwapRouterEligibleVaults}, which resolves
|
|
47
|
+
* eligibility from the router's on-chain `VaultEnabled` history + current
|
|
48
|
+
* `vaultInfo` registration instead of this static snapshot. Kept functional as
|
|
49
|
+
* the zero-RPC sync fallback (seed data, fail-fast checks); migrate UI gates
|
|
50
|
+
* to the async resolver.
|
|
26
51
|
*/
|
|
27
52
|
export declare function isSwapRouterEligible(vaultAddress: IAddress): boolean;
|
|
28
53
|
/**
|
|
29
|
-
* @deprecated Renamed to {@link isSwapRouterEligible}
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the
|
|
54
|
+
* @deprecated Renamed to {@link isSwapRouterEligible} (itself now superseded by
|
|
55
|
+
* {@link getSwapRouterEligibleVaults}, the on-chain-derived resolver). The
|
|
56
|
+
* meaning also changed: it no longer implies `vaultDeposit` routes the vault
|
|
57
|
+
* through the SwapRouter (that behavior was removed) — it only reports
|
|
58
|
+
* swap-router *eligibility* for the opt-in deposit surface. Kept as an alias
|
|
59
|
+
* for one release; migrate to the async resolver.
|
|
33
60
|
*/
|
|
34
61
|
export declare const vaultUsesSwapRouter: typeof isSwapRouterEligible;
|
|
35
62
|
/**
|
|
@@ -81,3 +108,104 @@ export declare function resolveOriginCode(provided?: `0x${string}`): `0x${string
|
|
|
81
108
|
* ```
|
|
82
109
|
*/
|
|
83
110
|
export declare function getSwapRouterDepositResult(signer: Signer | Wallet, { txHash, vault }: ISwapRouterDepositResultOptions): Promise<ISwapRouterDepositResult | null>;
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the ERC-20 tokens currently whitelisted for the `SwapRouter` on a
|
|
113
|
+
* chain — the any-token set a UI can offer as swap-and-deposit inputs.
|
|
114
|
+
*
|
|
115
|
+
* The router exposes its allowlist only as `mapping(address => bool)
|
|
116
|
+
* whitelistedTokens` with **no on-chain enumeration**, so this cannot be a
|
|
117
|
+
* single view call. It runs in two stages:
|
|
118
|
+
* 1. Scan the router's `TokenEnabled(address)` event history (from the chain's
|
|
119
|
+
* {@link SWAP_ROUTER_DEPLOY_BLOCKS} floor, in `eth_getLogs` chunks sized by
|
|
120
|
+
* {@link determineBlockSkipInternal}) to reconstruct the candidate set of
|
|
121
|
+
* every token ever enabled.
|
|
122
|
+
* 2. Re-read `whitelistedTokens(token)` for each candidate — batched via
|
|
123
|
+
* Multicall3 where verified, else per-call — and keep only those the
|
|
124
|
+
* mapping currently returns `true` for. This step makes later disables (and
|
|
125
|
+
* disable-then-re-enable) correct without replaying `TokenDisabled`, and is
|
|
126
|
+
* the fail-closed source of truth: a token surfaces only on a definite
|
|
127
|
+
* on-chain `true`.
|
|
128
|
+
*
|
|
129
|
+
* The resolved list is cached per chain for 5 minutes — the allowlist changes
|
|
130
|
+
* only via an admin `enableToken`/`disableToken` transaction, so a short TTL
|
|
131
|
+
* keeps the (multi-RPC) scan off the hot path without going stale for long.
|
|
132
|
+
*
|
|
133
|
+
* @param chainId - EVM chain ID. Chains without a {@link SWAP_ROUTER_ADDRESSES}
|
|
134
|
+
* entry resolve to `[]` with no RPC.
|
|
135
|
+
* @param options - `rpcUrl` (required) must point at `chainId`'s network;
|
|
136
|
+
* `fromBlock` overrides the default deploy-block scan floor (e.g. to widen a
|
|
137
|
+
* scan on a chain missing from {@link SWAP_ROUTER_DEPLOY_BLOCKS}).
|
|
138
|
+
* @returns Checksummed addresses of the currently-whitelisted tokens, deduped.
|
|
139
|
+
* Empty when no router is deployed or none are enabled. Callers still filter
|
|
140
|
+
* out a given vault's natively-accepted assets (those deposit without a swap).
|
|
141
|
+
* @throws AugustValidationError when `rpcUrl` is missing/empty.
|
|
142
|
+
* @throws AugustSDKError when an `eth_getLogs` chunk fails — surfaced (not
|
|
143
|
+
* swallowed) so a partial scan never masquerades as a complete allowlist.
|
|
144
|
+
* @worstCaseRpcCalls On mainnet, 1 `eth_blockNumber` + ~4 `eth_getLogs` chunks
|
|
145
|
+
* (router-lifetime ÷ 50k-block window) + 1 Multicall3 `eth_call` ≈ 6; scales
|
|
146
|
+
* with the router's on-chain age, not with candidate count.
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const tokens = await getSwapRouterWhitelistedTokens(1, {
|
|
150
|
+
* rpcUrl: 'https://eth-mainnet.example/v2/KEY',
|
|
151
|
+
* });
|
|
152
|
+
* // ['0xA0b8…', '0xdAC1…', '0xC02a…', '0x2260…'] (USDC, USDT, WETH, WBTC)
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function getSwapRouterWhitelistedTokens(chainId: number, options: {
|
|
156
|
+
rpcUrl: string;
|
|
157
|
+
fromBlock?: number;
|
|
158
|
+
}): Promise<IAddress[]>;
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the vaults currently eligible for the any-token `SwapRouter` deposit
|
|
161
|
+
* surface on a chain — i.e. every vault the router has `enableVault`-ed that is
|
|
162
|
+
* still registered on-chain.
|
|
163
|
+
*
|
|
164
|
+
* The router stores registrations as `mapping(address => VaultInfo) vaultInfo`
|
|
165
|
+
* with **no on-chain enumeration**, so (exactly like
|
|
166
|
+
* {@link getSwapRouterWhitelistedTokens}) this runs in two stages:
|
|
167
|
+
* 1. Scan the router's `VaultEnabled(address)` event history (from the chain's
|
|
168
|
+
* {@link SWAP_ROUTER_DEPLOY_BLOCKS} floor, in `eth_getLogs` chunks sized by
|
|
169
|
+
* {@link determineBlockSkipInternal}) to reconstruct the candidate set of
|
|
170
|
+
* every vault ever enabled.
|
|
171
|
+
* 2. Re-read `vaultInfo(vault)` for each candidate — batched via Multicall3
|
|
172
|
+
* where verified, else per-call — and keep only those whose
|
|
173
|
+
* `referenceAsset != address(0)` (an unregistered vault reverts with
|
|
174
|
+
* `InvalidVault` at deposit time). This makes later disables correct
|
|
175
|
+
* without replaying `VaultDisabled`, and is the fail-closed source of
|
|
176
|
+
* truth: a vault surfaces only on a definite on-chain registration.
|
|
177
|
+
*
|
|
178
|
+
* This is the dynamic successor to the static
|
|
179
|
+
* {@link SWAP_ROUTER_ELIGIBLE_VAULTS} snapshot / {@link isSwapRouterEligible}.
|
|
180
|
+
* It reports **on-chain eligibility only** — app-level policy (e.g. excluding
|
|
181
|
+
* OVault-enabled vaults that have their own deposit flow) remains the caller's
|
|
182
|
+
* responsibility, as does the flag gating of the UI surface.
|
|
183
|
+
*
|
|
184
|
+
* The resolved list is cached per chain for 5 minutes — registrations change
|
|
185
|
+
* only via an admin `enableVault`/`disableVault` transaction, so a short TTL
|
|
186
|
+
* keeps the (multi-RPC) scan off the hot path without going stale for long.
|
|
187
|
+
*
|
|
188
|
+
* @param chainId - EVM chain ID. Chains without a {@link SWAP_ROUTER_ADDRESSES}
|
|
189
|
+
* entry resolve to `[]` with no RPC.
|
|
190
|
+
* @param options - `rpcUrl` (required) must point at `chainId`'s network;
|
|
191
|
+
* `fromBlock` overrides the default deploy-block scan floor.
|
|
192
|
+
* @returns Checksummed addresses of the currently-registered eligible vaults,
|
|
193
|
+
* deduped. Empty when no router is deployed or none are registered.
|
|
194
|
+
* @throws AugustValidationError when `rpcUrl` is missing/empty.
|
|
195
|
+
* @throws AugustSDKError when an `eth_getLogs` chunk fails — surfaced (not
|
|
196
|
+
* swallowed) so a partial scan never masquerades as the complete set.
|
|
197
|
+
* @worstCaseRpcCalls On mainnet, 1 `eth_blockNumber` + ~4 `eth_getLogs` chunks
|
|
198
|
+
* (router-lifetime ÷ 50k-block window) + 1 Multicall3 `eth_call` ≈ 6; scales
|
|
199
|
+
* with the router's on-chain age, not with candidate count.
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* const vaults = await getSwapRouterEligibleVaults(1, {
|
|
203
|
+
* rpcUrl: 'https://eth-mainnet.example/v2/KEY',
|
|
204
|
+
* });
|
|
205
|
+
* // ['0x74aD…718b', '0xE9B7…9D32'] (Sentora USD, Upshift Core USDC)
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
export declare function getSwapRouterEligibleVaults(chainId: number, options: {
|
|
209
|
+
rpcUrl: string;
|
|
210
|
+
fromBlock?: number;
|
|
211
|
+
}): Promise<IAddress[]>;
|