@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.
@@ -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
- function jsonRpcRequest(call, id) {
20
- return {
21
- jsonrpc: "2.0",
22
- id,
23
- method: "eth_call",
24
- params: [{ to: call.to, data: call.data }, "latest"]
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 singleOnly = new Set();
103
+ const modes = new Map();
40
104
  const post = async (endpoint, body, items, attempt) => {
105
+ let response;
41
106
  try {
42
- const response = await fetchImpl(endpoint, {
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
- if (!response.ok) {
68
- if (batched) {
69
- singleOnly.add(endpoint);
70
- throw new ChunkTooLargeError(`evm rpc ${response.status}`);
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
- if (!batched) {
76
- return [toResult(Array.isArray(parsed) ? parsed[0] : parsed)];
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
- singleOnly.add(endpoint);
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)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ember-finance/sdk",
3
3
  "description": "Ember Protocol SDK",
4
- "version": "2.11.0-pre.4",
4
+ "version": "2.11.0-pre.5",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",