@ember-finance/sdk 2.11.0-pre.4 → 2.11.0-pre.5
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/dist/src/placements/evm.d.ts +12 -0
- package/dist/src/placements/evm.js +150 -29
- package/package.json +1 -1
|
@@ -11,5 +11,17 @@ export interface JsonRpcEvmReaderOptions {
|
|
|
11
11
|
sleep?: (ms: number) => Promise<void>;
|
|
12
12
|
fetch?: typeof globalThis.fetch;
|
|
13
13
|
onRequest?: (request: TransportRequest) => void;
|
|
14
|
+
multicall?: boolean;
|
|
14
15
|
}
|
|
16
|
+
interface EvmCall {
|
|
17
|
+
to: string;
|
|
18
|
+
data: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
21
|
+
export declare function encodeAggregate3(calls: EvmCall[]): string;
|
|
22
|
+
export declare function decodeAggregate3(data: string, count: number): {
|
|
23
|
+
success: boolean;
|
|
24
|
+
data: string;
|
|
25
|
+
}[];
|
|
15
26
|
export declare function jsonRpcEvmReader(endpoints?: Record<number, string>, options?: JsonRpcEvmReaderOptions): EvmReader;
|
|
27
|
+
export {};
|
|
@@ -16,13 +16,76 @@ export function decodeUint256(hex) {
|
|
|
16
16
|
return 0n;
|
|
17
17
|
return BigInt(hex);
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
20
|
+
const AGGREGATE3_SELECTOR = "0x82ad56cb";
|
|
21
|
+
const RATE_LIMITED = /too many requests|rate ?limit/i;
|
|
22
|
+
function stripHex(value) {
|
|
23
|
+
return value.trim().replace(/^0x/i, "");
|
|
24
|
+
}
|
|
25
|
+
function word(value) {
|
|
26
|
+
return BigInt(value).toString(16).padStart(64, "0");
|
|
27
|
+
}
|
|
28
|
+
function addressWord(address) {
|
|
29
|
+
return stripHex(address).toLowerCase().padStart(64, "0");
|
|
30
|
+
}
|
|
31
|
+
function bytesWords(data) {
|
|
32
|
+
const raw = stripHex(data);
|
|
33
|
+
const length = raw.length / 2;
|
|
34
|
+
const padded = raw.padEnd(Math.ceil(length / 32) * 64, "0");
|
|
35
|
+
return word(length) + padded;
|
|
36
|
+
}
|
|
37
|
+
export function encodeAggregate3(calls) {
|
|
38
|
+
const tuples = calls.map(call => addressWord(call.to) + word(1) + word(0x60) + bytesWords(call.data));
|
|
39
|
+
let offset = calls.length * 32;
|
|
40
|
+
const offsets = tuples.map(tuple => {
|
|
41
|
+
const current = offset;
|
|
42
|
+
offset += tuple.length / 2;
|
|
43
|
+
return word(current);
|
|
44
|
+
});
|
|
45
|
+
return (AGGREGATE3_SELECTOR +
|
|
46
|
+
word(0x20) +
|
|
47
|
+
word(calls.length) +
|
|
48
|
+
offsets.join("") +
|
|
49
|
+
tuples.join(""));
|
|
50
|
+
}
|
|
51
|
+
export function decodeAggregate3(data, count) {
|
|
52
|
+
const raw = stripHex(data);
|
|
53
|
+
const read = (byteOffset) => {
|
|
54
|
+
const slice = raw.slice(byteOffset * 2, byteOffset * 2 + 64);
|
|
55
|
+
if (slice.length !== 64)
|
|
56
|
+
throw new Error("multicall: truncated response");
|
|
57
|
+
return Number(BigInt(`0x${slice}`));
|
|
25
58
|
};
|
|
59
|
+
const arrayOffset = read(0);
|
|
60
|
+
const length = read(arrayOffset);
|
|
61
|
+
if (length !== count)
|
|
62
|
+
throw new Error("multicall: result count mismatch");
|
|
63
|
+
const base = arrayOffset + 32;
|
|
64
|
+
const results = [];
|
|
65
|
+
for (let index = 0; index < length; index += 1) {
|
|
66
|
+
const tupleOffset = base + read(base + index * 32);
|
|
67
|
+
const success = read(tupleOffset) === 1;
|
|
68
|
+
const bytesOffset = tupleOffset + read(tupleOffset + 32);
|
|
69
|
+
const bytesLength = read(bytesOffset);
|
|
70
|
+
const start = (bytesOffset + 32) * 2;
|
|
71
|
+
const body = raw.slice(start, start + bytesLength * 2);
|
|
72
|
+
if (body.length !== bytesLength * 2)
|
|
73
|
+
throw new Error("multicall: truncated response");
|
|
74
|
+
results.push({ success, data: `0x${body}` });
|
|
75
|
+
}
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
function jsonRpcRequest(method, params, id) {
|
|
79
|
+
return { jsonrpc: "2.0", id, method, params };
|
|
80
|
+
}
|
|
81
|
+
function callRequest(call, id) {
|
|
82
|
+
return jsonRpcRequest("eth_call", [{ to: call.to, data: call.data }, "latest"], id);
|
|
83
|
+
}
|
|
84
|
+
function isRateLimited(entry) {
|
|
85
|
+
const error = entry?.error;
|
|
86
|
+
if (!error)
|
|
87
|
+
return false;
|
|
88
|
+
return error.code === -32005 || RATE_LIMITED.test(error.message ?? "");
|
|
26
89
|
}
|
|
27
90
|
function toResult(entry) {
|
|
28
91
|
if (!entry)
|
|
@@ -35,53 +98,111 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
|
|
|
35
98
|
const fetchImpl = options.fetch ??
|
|
36
99
|
((input, init) => globalThis.fetch(input, init));
|
|
37
100
|
const onRequest = options.onRequest;
|
|
101
|
+
const useMulticall = options.multicall ?? true;
|
|
38
102
|
const batchers = new Map();
|
|
39
|
-
const
|
|
103
|
+
const modes = new Map();
|
|
40
104
|
const post = async (endpoint, body, items, attempt) => {
|
|
105
|
+
let response;
|
|
41
106
|
try {
|
|
42
|
-
|
|
107
|
+
response = await fetchImpl(endpoint, {
|
|
43
108
|
method: "POST",
|
|
44
109
|
headers: { "Content-Type": "application/json" },
|
|
45
110
|
body: JSON.stringify(body)
|
|
46
111
|
});
|
|
47
|
-
onRequest?.({ items, attempt, status: response.status });
|
|
48
|
-
return response;
|
|
49
112
|
}
|
|
50
113
|
catch (error) {
|
|
51
114
|
onRequest?.({ items, attempt, status: "network-error" });
|
|
52
115
|
throw new TransientError(`evm rpc: ${error instanceof Error ? error.message : String(error)}`);
|
|
53
116
|
}
|
|
54
|
-
|
|
55
|
-
const send = async (endpoint, calls, attempt) => {
|
|
56
|
-
const batched = calls.length > 1;
|
|
57
|
-
if (batched && singleOnly.has(endpoint)) {
|
|
58
|
-
throw new ChunkTooLargeError("evm rpc: batch unsupported");
|
|
59
|
-
}
|
|
60
|
-
const body = batched
|
|
61
|
-
? calls.map((call, id) => jsonRpcRequest(call, id))
|
|
62
|
-
: jsonRpcRequest(calls[0], 1);
|
|
63
|
-
const response = await post(endpoint, body, calls.length, attempt);
|
|
117
|
+
onRequest?.({ items, attempt, status: response.status });
|
|
64
118
|
if (isTransientStatus(response.status)) {
|
|
65
119
|
throw new TransientError(`evm rpc ${response.status}`, retryAfterMs(response.headers?.get("retry-after")));
|
|
66
120
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
121
|
+
return response;
|
|
122
|
+
};
|
|
123
|
+
const rpc = async (endpoint, method, params, items, attempt) => {
|
|
124
|
+
const response = await post(endpoint, jsonRpcRequest(method, params, 1), items, attempt);
|
|
125
|
+
if (!response.ok)
|
|
72
126
|
throw new Error(`evm rpc ${response.status}`);
|
|
73
|
-
}
|
|
74
127
|
const parsed = (await response.json());
|
|
75
|
-
|
|
76
|
-
|
|
128
|
+
const entry = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
129
|
+
if (isRateLimited(entry))
|
|
130
|
+
throw new TransientError(`evm rpc: ${entry?.error?.message}`);
|
|
131
|
+
return entry ?? {};
|
|
132
|
+
};
|
|
133
|
+
const detectMode = async (endpoint, attempt) => {
|
|
134
|
+
const known = modes.get(endpoint);
|
|
135
|
+
if (known)
|
|
136
|
+
return known;
|
|
137
|
+
let mode = "batch";
|
|
138
|
+
if (useMulticall) {
|
|
139
|
+
try {
|
|
140
|
+
const code = await rpc(endpoint, "eth_getCode", [MULTICALL3_ADDRESS, "latest"], 1, attempt);
|
|
141
|
+
if (!code.error && stripHex(code.result ?? "").length > 2)
|
|
142
|
+
mode = "multicall";
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof TransientError)
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
modes.set(endpoint, mode);
|
|
150
|
+
return mode;
|
|
151
|
+
};
|
|
152
|
+
const sendMulticall = async (endpoint, calls, attempt) => {
|
|
153
|
+
const entry = await rpc(endpoint, "eth_call", [{ to: MULTICALL3_ADDRESS, data: encodeAggregate3(calls) }, "latest"], calls.length, attempt);
|
|
154
|
+
if (entry.error || !entry.result)
|
|
155
|
+
return undefined;
|
|
156
|
+
let decoded;
|
|
157
|
+
try {
|
|
158
|
+
decoded = decodeAggregate3(entry.result, calls.length);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return decoded.map(result => result.success
|
|
164
|
+
? { ok: true, value: result.data === "" ? "0x" : result.data }
|
|
165
|
+
: { ok: false, error: new Error("evm rpc: execution reverted") });
|
|
166
|
+
};
|
|
167
|
+
const sendBatch = async (endpoint, calls, attempt) => {
|
|
168
|
+
const response = await post(endpoint, calls.map((call, id) => callRequest(call, id)), calls.length, attempt);
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
modes.set(endpoint, "single");
|
|
171
|
+
throw new ChunkTooLargeError(`evm rpc ${response.status}`);
|
|
77
172
|
}
|
|
173
|
+
const parsed = (await response.json());
|
|
78
174
|
if (!Array.isArray(parsed)) {
|
|
79
|
-
|
|
175
|
+
modes.set(endpoint, "single");
|
|
80
176
|
throw new ChunkTooLargeError("evm rpc: batch unsupported");
|
|
81
177
|
}
|
|
178
|
+
const limited = parsed.find(isRateLimited);
|
|
179
|
+
if (limited)
|
|
180
|
+
throw new TransientError(`evm rpc: ${limited.error?.message}`);
|
|
82
181
|
const byId = new Map(parsed.map(entry => [Number(entry.id), entry]));
|
|
83
182
|
return calls.map((_, id) => toResult(byId.get(id)));
|
|
84
183
|
};
|
|
184
|
+
const send = async (endpoint, calls, attempt) => {
|
|
185
|
+
if (calls.length === 1) {
|
|
186
|
+
const response = await post(endpoint, callRequest(calls[0], 1), 1, attempt);
|
|
187
|
+
if (!response.ok)
|
|
188
|
+
throw new Error(`evm rpc ${response.status}`);
|
|
189
|
+
const parsed = (await response.json());
|
|
190
|
+
const entry = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
191
|
+
if (isRateLimited(entry))
|
|
192
|
+
throw new TransientError(`evm rpc: ${entry?.error?.message}`);
|
|
193
|
+
return [toResult(entry)];
|
|
194
|
+
}
|
|
195
|
+
const mode = await detectMode(endpoint, attempt);
|
|
196
|
+
if (mode === "single")
|
|
197
|
+
throw new ChunkTooLargeError("evm rpc: batch unsupported");
|
|
198
|
+
if (mode === "multicall") {
|
|
199
|
+
const aggregated = await sendMulticall(endpoint, calls, attempt);
|
|
200
|
+
if (aggregated)
|
|
201
|
+
return aggregated;
|
|
202
|
+
modes.set(endpoint, "batch");
|
|
203
|
+
}
|
|
204
|
+
return sendBatch(endpoint, calls, attempt);
|
|
205
|
+
};
|
|
85
206
|
const batcherFor = (endpoint) => {
|
|
86
207
|
const existing = batchers.get(endpoint);
|
|
87
208
|
if (existing)
|