@@ -65,15 +65,67 @@ export interface MulticallResult {
65
65
returnData : string ;
66
66
}
67
67
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
+ */
69
73
function isEthersResult ( x : any ) : x is EthersResult {
70
74
return (
71
75
x != null &&
76
+ // Must have a toObject method (used for named structs)
72
77
typeof x . toObject === "function" &&
78
+ // Must have a toArray method (used for unnamed tuples/arrays)
73
79
typeof x . toArray === "function"
74
80
) ;
75
81
}
76
82
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
+
77
129
/**
78
130
* Multicall wraps the Multicall3 Solidity contract for batching on-chain calls.
79
131
*/
@@ -172,12 +224,22 @@ class Multicall {
172
224
if ( ! r . success ) return [ false , r . returnData ] as [ boolean , any ] ;
173
225
174
226
try {
175
- const dec = calls [ i ] . contract . interface . decodeFunctionResult (
227
+ const decoded = calls [ i ] . contract . interface . decodeFunctionResult (
176
228
calls [ i ] . functionFragment ,
177
229
r . returnData
178
230
) ;
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 ] ;
181
243
} catch ( err : any ) {
182
244
return [ false , `Data handling error: ${ err . message } ` ] as [ boolean , any ] ;
183
245
}
@@ -215,12 +277,22 @@ class Multicall {
215
277
if ( ! r . success ) return [ false , r . returnData ] as [ boolean , any ] ;
216
278
217
279
try {
218
- const dec = calls [ i ] . contract . interface . decodeFunctionResult (
280
+ const decodedResult = calls [ i ] . contract . interface . decodeFunctionResult (
219
281
calls [ i ] . functionFragment ,
220
282
r . returnData
221
283
) ;
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 ] ;
224
296
} catch ( err : any ) {
225
297
return [ false , `Data handling error: ${ err . message } ` ] as [ boolean , any ] ;
226
298
}
@@ -242,9 +314,16 @@ class Multicall {
242
314
* Aggregates calls allowing individual failures via allowFailure flag.
243
315
* Mirrors: function aggregate3(Call3[] calldata calls)
244
316
* @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.
246
324
*/
247
325
async aggregate3 ( calls : Call3 [ ] ) : Promise < Array < [ boolean , any ] > > {
326
+ // Build the multicall payload
248
327
const payload = calls . map (
249
328
( { contract, functionFragment, args, allowFailure } ) => ( {
250
329
target : contract . target ,
@@ -253,44 +332,31 @@ class Multicall {
253
332
} )
254
333
) ;
255
334
335
+ // Execute the RPC
256
336
const [ rawResults ] = await this . rpcCall < [ MulticallResult [ ] ] > ( "aggregate3" , [
257
337
payload ,
258
338
] ) ;
259
339
340
+ // Decode + unwrap each return
260
341
return rawResults . map ( ( r , i ) => {
261
342
if ( ! r . success ) return [ false , r . returnData ] as [ boolean , any ] ;
343
+
262
344
try {
263
- // decodeFunctionResult gives us a Result proxy
345
+ // decodeFunctionResult gives us a ` Result` proxy
264
346
const decoded = calls [ i ] . contract . interface . decodeFunctionResult (
265
347
calls [ i ] . functionFragment ,
266
348
r . returnData
267
349
) ;
268
350
269
351
// If function has a *single* output, decoded.length === 1
270
- const raw =
352
+ const rawOutput =
271
353
decoded . length === 1
272
354
? decoded [ 0 ]
273
355
: // multiple outputs → toArray() gives [v0, v1, ...]
274
356
decoded . toArray ( true ) ;
275
357
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 ) ;
294
360
295
361
return [ true , plain ] as [ boolean , any ] ;
296
362
} catch ( err : any ) {
@@ -328,12 +394,22 @@ class Multicall {
328
394
if ( ! r . success ) return [ false , r . returnData ] as [ boolean , any ] ;
329
395
330
396
try {
331
- const dec = calls [ i ] . contract . interface . decodeFunctionResult (
397
+ const decoded = calls [ i ] . contract . interface . decodeFunctionResult (
332
398
calls [ i ] . functionFragment ,
333
399
r . returnData
334
400
) ;
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 ] ;
337
413
} catch ( err : any ) {
338
414
return [ false , `Data handling error: ${ err . message } ` ] as [ boolean , any ] ;
339
415
}
0 commit comments