Skip to content

Commit 735dc9c

Browse files
authored
Enhance SDK return types
Merge pull request #1 from EVMlord/dev
2 parents 94d365c + d4dfd4a commit 735dc9c

File tree

3 files changed

+110
-33
lines changed

3 files changed

+110
-33
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"name": "@evmlord/multicall-sdk",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"description": "A multichain lightweight library to process multiple calls via a single eth_call using Ethers JS.",
55
"scripts": {
6+
"alpha": "npm publish --tag alpha",
67
"build": "tsc",
78
"typecheck": "tsc --noEmit",
89
"prepublishOnly": "yarn test && yarn build",

src/ethers-multicall.ts

Lines changed: 107 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,67 @@ export interface MulticallResult {
6565
returnData: string;
6666
}
6767

68-
/** Type-guard for ethers.Result */
68+
/**
69+
* Type-guard for ethers.Result
70+
* A small “duck-typing” check for anything that looks like an ethers Result.
71+
* In v6, a `Result` satisfies both of these at runtime:
72+
*/
6973
function isEthersResult(x: any): x is EthersResult {
7074
return (
7175
x != null &&
76+
// Must have a toObject method (used for named structs)
7277
typeof x.toObject === "function" &&
78+
// Must have a toArray method (used for unnamed tuples/arrays)
7379
typeof x.toArray === "function"
7480
);
7581
}
7682

83+
/**
84+
* Recursively “unwrap” anything returned by ethers.decodeFunctionResult (which is either
85+
* • an `EthersResult` proxy (for named structs, or tuple/tuple[]), or
86+
* • a plain JS array of primitive values, or
87+
* • a primitive (string, bigint, number, boolean, etc.)
88+
*
89+
* We want to end up with either:
90+
* • a plain { ... } object (for a named struct), OR
91+
* • a plain Array of primitives/objects, OR
92+
* • a plain primitive.
93+
*
94+
* NOTE: ethers.Result wants you to call `.toObject(true)` in order to convert a *named* struct
95+
* into a plain object with its named fields. If you call `.toObject(true)` on something
96+
* that is *not* a named struct (e.g. it’s a `tuple[]`), ethers will throw, so we catch
97+
* that and fall back to `.toArray(false)`, which returns a plain Array of child Results.
98+
*/
99+
export function _fullyUnwrap(x: any): any {
100+
// 1) Check: is `x` an ethers.Result (possibly a Proxy)?
101+
if (isEthersResult(x)) {
102+
try {
103+
// --- CASE 1: named struct (or named‐struct[]) ---
104+
// If `x` really is a named struct (or array of named structs),
105+
// .toObject(true) will succeed:
106+
return x.toObject(/* deep= */ true);
107+
} catch (_err) {
108+
// --- CASE 99: unnamed tuple or tuple[] ---
109+
// It wasn’t a named struct, so treat it as an unnamed tuple or tuple[]:
110+
const arr = x.toArray(/* deep= */ false);
111+
112+
// Recurse into each child, but pass along the same `cases` array:
113+
return arr.map((child) => _fullyUnwrap(child));
114+
}
115+
}
116+
117+
// 2) If `x` is a plain JS array (Array.isArray(x) === true), we want to
118+
// recurse *into* each element. **Do not return `x` outright.**
119+
if (Array.isArray(x)) {
120+
// Recurse on every element, passing down the same `cases` array:
121+
return x.map((child) => _fullyUnwrap(child));
122+
}
123+
124+
// 3) Otherwise `x` is a primitive (string, bigint, number, boolean, etc.).
125+
// There’s nothing more to unwrap, so just return it:
126+
return x;
127+
}
128+
77129
/**
78130
* Multicall wraps the Multicall3 Solidity contract for batching on-chain calls.
79131
*/
@@ -172,12 +224,22 @@ class Multicall {
172224
if (!r.success) return [false, r.returnData] as [boolean, any];
173225

174226
try {
175-
const dec = calls[i].contract.interface.decodeFunctionResult(
227+
const decoded = calls[i].contract.interface.decodeFunctionResult(
176228
calls[i].functionFragment,
177229
r.returnData
178230
);
179-
const vals = Array.isArray(dec) ? dec : Object.values(dec);
180-
return [true, vals.length === 1 ? vals[0] : vals] as [boolean, any];
231+
232+
// If function has a *single* output, decoded.length === 1
233+
const rawOutput =
234+
decoded.length === 1
235+
? decoded[0]
236+
: // multiple outputs → toArray() gives [v0, v1, ...]
237+
decoded.toArray(true);
238+
239+
// Now fully unwrap rawOutput into a plain JS primitive/object/array:
240+
const plain = _fullyUnwrap(rawOutput);
241+
242+
return [true, plain] as [boolean, any];
181243
} catch (err: any) {
182244
return [false, `Data handling error: ${err.message}`] as [boolean, any];
183245
}
@@ -215,12 +277,22 @@ class Multicall {
215277
if (!r.success) return [false, r.returnData] as [boolean, any];
216278

217279
try {
218-
const dec = calls[i].contract.interface.decodeFunctionResult(
280+
const decodedResult = calls[i].contract.interface.decodeFunctionResult(
219281
calls[i].functionFragment,
220282
r.returnData
221283
);
222-
const vals = Array.isArray(dec) ? dec : Object.values(dec);
223-
return [true, vals.length === 1 ? vals[0] : vals] as [boolean, any];
284+
285+
// If function has a *single* output, decoded.length === 1
286+
const rawOutput =
287+
decodedResult.length === 1
288+
? decodedResult[0]
289+
: // multiple outputs → toArray() gives [v0, v1, ...]
290+
decodedResult.toArray(true);
291+
292+
// Now fully unwrap rawOutput into a plain JS primitive/object/array:
293+
const plain = _fullyUnwrap(rawOutput);
294+
295+
return [true, plain] as [boolean, any];
224296
} catch (err: any) {
225297
return [false, `Data handling error: ${err.message}`] as [boolean, any];
226298
}
@@ -242,9 +314,16 @@ class Multicall {
242314
* Aggregates calls allowing individual failures via allowFailure flag.
243315
* Mirrors: function aggregate3(Call3[] calldata calls)
244316
* @param calls - Array of Call3 objects.
245-
* @returns Array of tuples [success, decodedResult or raw hex].
317+
* @returns An array of [success:boolean, data:any].
318+
* - If success===true, data is already plain JS:
319+
* • primitive (bigint, string, etc)
320+
* • tuple → JS array of [v0,v1,…]
321+
* • struct → JS object {field1, field2,…}
322+
* • struct[] → JS array of objects
323+
* - If success===false, data is the raw hex revertData string.
246324
*/
247325
async aggregate3(calls: Call3[]): Promise<Array<[boolean, any]>> {
326+
// Build the multicall payload
248327
const payload = calls.map(
249328
({ contract, functionFragment, args, allowFailure }) => ({
250329
target: contract.target,
@@ -253,44 +332,31 @@ class Multicall {
253332
})
254333
);
255334

335+
// Execute the RPC
256336
const [rawResults] = await this.rpcCall<[MulticallResult[]]>("aggregate3", [
257337
payload,
258338
]);
259339

340+
// Decode + unwrap each return
260341
return rawResults.map((r, i) => {
261342
if (!r.success) return [false, r.returnData] as [boolean, any];
343+
262344
try {
263-
// decodeFunctionResult gives us a Result proxy
345+
// decodeFunctionResult gives us a `Result` proxy
264346
const decoded = calls[i].contract.interface.decodeFunctionResult(
265347
calls[i].functionFragment,
266348
r.returnData
267349
);
268350

269351
// If function has a *single* output, decoded.length === 1
270-
const raw =
352+
const rawOutput =
271353
decoded.length === 1
272354
? decoded[0]
273355
: // multiple outputs → toArray() gives [v0, v1, ...]
274356
decoded.toArray(true);
275357

276-
// Now: if raw is a struct proxy → raw.toObject(true)
277-
// if raw is an array of struct proxies → raw.map(r => r.toObject(true))
278-
let plain: any;
279-
280-
if (isEthersResult(raw)) {
281-
// single struct → plain object
282-
plain = raw.toObject(true);
283-
} else if (
284-
Array.isArray(raw) &&
285-
raw.length > 0 &&
286-
isEthersResult(raw[0])
287-
) {
288-
// array of structs → plain array of objects
289-
plain = (raw as EthersResult[]).map((r) => r.toObject(true));
290-
} else {
291-
// primitives or tuples → already plain
292-
plain = raw;
293-
}
358+
// Now fully unwrap rawOutput into a plain JS primitive/object/array:
359+
const plain = _fullyUnwrap(rawOutput);
294360

295361
return [true, plain] as [boolean, any];
296362
} catch (err: any) {
@@ -328,12 +394,22 @@ class Multicall {
328394
if (!r.success) return [false, r.returnData] as [boolean, any];
329395

330396
try {
331-
const dec = calls[i].contract.interface.decodeFunctionResult(
397+
const decoded = calls[i].contract.interface.decodeFunctionResult(
332398
calls[i].functionFragment,
333399
r.returnData
334400
);
335-
const vals = Array.isArray(dec) ? dec : Object.values(dec);
336-
return [true, vals.length === 1 ? vals[0] : vals] as [boolean, any];
401+
402+
// If function has a *single* output, decoded.length === 1
403+
const rawOutput =
404+
decoded.length === 1
405+
? decoded[0]
406+
: // multiple outputs → toArray() gives [v0, v1, ...]
407+
decoded.toArray(true);
408+
409+
// Now fully unwrap rawOutput into a plain JS primitive/object/array:
410+
const plain = _fullyUnwrap(rawOutput);
411+
412+
return [true, plain] as [boolean, any];
337413
} catch (err: any) {
338414
return [false, `Data handling error: ${err.message}`] as [boolean, any];
339415
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"declarationDir": "./dist/dts" /* Specify the output directory for generated declaration files. */,
7979

8080
/* Interop Constraints */
81-
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
81+
// "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
8282
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
8383
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
8484
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */

0 commit comments

Comments
 (0)