2022-09-05 23:14:43 +03:00
2022-09-09 06:21:08 +03:00
import {
defineProperties , concat , getBytesCopy , getNumber , hexlify ,
2022-12-10 02:21:45 +03:00
toBeArray , toBigInt , toNumber ,
2023-01-26 19:32:56 +03:00
assert , assertArgument
2022-09-09 06:21:08 +03:00
} from "../../utils/index.js" ;
2022-09-05 23:14:43 +03:00
import type { BigNumberish , BytesLike } from "../../utils/index.js" ;
2023-01-26 19:32:56 +03:00
/ * *
* @_ignore :
* /
2022-11-28 05:54:49 +03:00
export const WordSize : number = 32 ;
2022-09-05 23:14:43 +03:00
const Padding = new Uint8Array ( WordSize ) ;
// Properties used to immediate pass through to the underlying object
// - `then` is used to detect if an object is a Promise for await
const passProperties = [ "then" ] ;
const _guard = { } ;
2023-01-26 19:32:56 +03:00
function throwError ( name : string , error : Error ) : never {
const wrapped = new Error ( ` deferred error during ABI decoding triggered accessing ${ name } ` ) ;
( < any > wrapped ) . error = error ;
throw wrapped ;
}
2022-11-28 05:54:49 +03:00
/ * *
* A [ [ Result ] ] is a sub - class of Array , which allows accessing any
* of its values either positionally by its index or , if keys are
* provided by its name .
*
* @_docloc : api / abi
* /
2022-09-05 23:14:43 +03:00
export class Result extends Array < any > {
2023-01-26 19:32:56 +03:00
readonly # names : ReadonlyArray < null | string > ;
2022-09-05 23:14:43 +03:00
[ K : string | number ] : any
2022-11-28 05:54:49 +03:00
/ * *
* @private
* /
2023-01-26 19:32:56 +03:00
constructor ( . . . args : Array < any > ) {
// To properly sub-class Array so the other built-in
// functions work, the constructor has to behave fairly
// well. So, in the event we are created via fromItems()
// we build the read-only Result object we want, but on
// any other input, we use the default constructor
// constructor(guard: any, items: Array<any>, keys?: Array<null | string>);
const guard = args [ 0 ] ;
let items : Array < any > = args [ 1 ] ;
let names : Array < null | string > = ( args [ 2 ] || [ ] ) . slice ( ) ;
let wrap = true ;
if ( guard !== _guard ) {
items = args ;
names = [ ] ;
wrap = false ;
2022-09-05 23:14:43 +03:00
}
2023-01-26 19:32:56 +03:00
// Can't just pass in ...items since an array of length 1
// is a special case in the super.
super ( items . length ) ;
items . forEach ( ( item , index ) = > { this [ index ] = item ; } ) ;
// Find all unique keys
const nameCounts = names . reduce ( ( accum , name ) = > {
if ( typeof ( name ) === "string" ) {
accum . set ( name , ( accum . get ( name ) || 0 ) + 1 ) ;
}
return accum ;
} , < Map < string , number > > ( new Map ( ) ) ) ;
// Remove any key thats not unique
this . # names = Object . freeze ( items . map ( ( item , index ) = > {
const name = names [ index ] ;
if ( name != null && nameCounts . get ( name ) === 1 ) {
return name ;
}
return null ;
} ) ) ;
if ( ! wrap ) { return ; }
// A wrapped Result is immutable
2022-09-05 23:14:43 +03:00
Object . freeze ( this ) ;
2023-01-26 19:32:56 +03:00
// Proxy indices and names so we can trap deferred errors
2022-09-05 23:14:43 +03:00
return new Proxy ( this , {
get : ( target , prop , receiver ) = > {
if ( typeof ( prop ) === "string" ) {
2023-01-26 19:32:56 +03:00
// Index accessor
2022-09-05 23:14:43 +03:00
if ( prop . match ( /^[0-9]+$/ ) ) {
2022-09-09 06:21:08 +03:00
const index = getNumber ( prop , "%index" ) ;
2022-09-05 23:14:43 +03:00
if ( index < 0 || index >= this . length ) {
throw new RangeError ( "out of result range" ) ;
}
const item = target [ index ] ;
if ( item instanceof Error ) {
2023-01-26 19:32:56 +03:00
throwError ( ` index ${ index } ` , item ) ;
2022-09-05 23:14:43 +03:00
}
return item ;
}
// Pass important checks (like `then` for Promise) through
2023-01-26 19:32:56 +03:00
if ( passProperties . indexOf ( prop ) >= 0 ) {
2022-09-05 23:14:43 +03:00
return Reflect . get ( target , prop , receiver ) ;
}
2023-01-26 19:32:56 +03:00
const value = target [ prop ] ;
if ( value instanceof Function ) {
// Make sure functions work with private variables
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#no_private_property_forwarding
return function ( this : any , . . . args : Array < any > ) {
return value . apply ( ( this === receiver ) ? target : this , args ) ;
} ;
} else if ( ! ( prop in target ) ) {
// Possible name accessor
return target . getValue . apply ( ( this === receiver ) ? target : this , [ prop ] ) ;
2022-09-05 23:14:43 +03:00
}
}
return Reflect . get ( target , prop , receiver ) ;
}
} ) ;
}
2023-01-26 19:32:56 +03:00
/ * *
* Returns the Result as a normal Array .
*
* This will throw if there are any outstanding deferred
* errors .
* /
toArray ( ) : Array < any > {
2023-02-19 05:42:01 +03:00
const result : Array < any > = [ ] ;
2023-01-26 19:32:56 +03:00
this . forEach ( ( item , index ) = > {
2023-02-19 05:42:01 +03:00
if ( item instanceof Error ) { throwError ( ` index ${ index } ` , item ) ; }
result . push ( item ) ;
2023-01-26 19:32:56 +03:00
} ) ;
2023-02-19 05:42:01 +03:00
return result ;
2023-01-26 19:32:56 +03:00
}
/ * *
* Returns the Result as an Object with each name - value pair .
*
* This will throw if any value is unnamed , or if there are
* any outstanding deferred errors .
* /
toObject ( ) : Record < string , any > {
return this . # names . reduce ( ( accum , name , index ) = > {
assert ( name != null , "value at index ${ index } unnamed" , "UNSUPPORTED_OPERATION" , {
operation : "toObject()"
} ) ;
// Add values for names that don't conflict
if ( ! ( name in accum ) ) {
accum [ name ] = this . getValue ( name ) ;
}
return accum ;
} , < Record < string , any > > { } ) ;
2022-09-05 23:14:43 +03:00
}
2022-11-28 05:54:49 +03:00
/ * *
* @_ignore
* /
2023-01-26 19:32:56 +03:00
slice ( start? : number | undefined , end? : number | undefined ) : Result {
2022-09-05 23:14:43 +03:00
if ( start == null ) { start = 0 ; }
2023-02-19 05:42:01 +03:00
if ( start < 0 ) {
start += this . length ;
if ( start < 0 ) { start = 0 ; }
}
2022-09-05 23:14:43 +03:00
if ( end == null ) { end = this . length ; }
2023-02-19 05:42:01 +03:00
if ( end < 0 ) {
end += this . length ;
if ( end < 0 ) { end = 0 ; }
}
if ( end > this . length ) { end = this . length ; }
2022-09-05 23:14:43 +03:00
2023-08-15 03:00:45 +03:00
const result : Array < any > = [ ] , names : Array < null | string > = [ ] ;
2022-09-05 23:14:43 +03:00
for ( let i = start ; i < end ; i ++ ) {
2023-01-26 19:32:56 +03:00
result . push ( this [ i ] ) ;
names . push ( this . # names [ i ] ) ;
2022-09-05 23:14:43 +03:00
}
2023-01-26 19:32:56 +03:00
return new Result ( _guard , result , names ) ;
2022-09-05 23:14:43 +03:00
}
2023-01-26 19:32:56 +03:00
/ * *
* @_ignore
* /
filter ( callback : ( el : any , index : number , array : Result ) = > boolean , thisArg? : any ) : Result {
2023-08-15 03:00:45 +03:00
const result : Array < any > = [ ] , names : Array < null | string > = [ ] ;
2023-01-26 19:32:56 +03:00
for ( let i = 0 ; i < this . length ; i ++ ) {
const item = this [ i ] ;
if ( item instanceof Error ) {
throwError ( ` index ${ i } ` , item ) ;
}
if ( callback . call ( thisArg , item , i , this ) ) {
result . push ( item ) ;
names . push ( this . # names [ i ] ) ;
}
}
return new Result ( _guard , result , names ) ;
2022-09-05 23:14:43 +03:00
}
2023-05-16 00:18:46 +03:00
/ * *
* @_ignore
* /
map < T extends any = any > ( callback : ( el : any , index : number , array : Result ) = > T , thisArg? : any ) : Array < T > {
const result : Array < T > = [ ] ;
for ( let i = 0 ; i < this . length ; i ++ ) {
const item = this [ i ] ;
if ( item instanceof Error ) {
throwError ( ` index ${ i } ` , item ) ;
}
result . push ( callback . call ( thisArg , item , i , this ) ) ;
}
return result ;
}
2023-01-26 19:32:56 +03:00
2022-11-28 05:54:49 +03:00
/ * *
* Returns the value for % % name % % .
*
* Since it is possible to have a key whose name conflicts with
* a method on a [ [ Result ] ] or its superclass Array , or any
* JavaScript keyword , this ensures all named values are still
* accessible by name .
* /
2022-09-05 23:14:43 +03:00
getValue ( name : string ) : any {
2023-01-26 19:32:56 +03:00
const index = this . # names . indexOf ( name ) ;
if ( index === - 1 ) { return undefined ; }
const value = this [ index ] ;
if ( value instanceof Error ) {
throwError ( ` property ${ JSON . stringify ( name ) } ` , ( < any > value ) . error ) ;
2022-09-05 23:14:43 +03:00
}
2023-01-26 19:32:56 +03:00
return value ;
2022-09-05 23:14:43 +03:00
}
2022-11-28 05:54:49 +03:00
/ * *
* Creates a new [ [ Result ] ] for % % items % % with each entry
* also accessible by its corresponding name in % % keys % % .
* /
static fromItems ( items : Array < any > , keys? : Array < null | string > ) : Result {
2022-09-05 23:14:43 +03:00
return new Result ( _guard , items , keys ) ;
}
}
2022-11-28 05:54:49 +03:00
/ * *
* Returns all errors found in a [ [ Result ] ] .
*
* Since certain errors encountered when creating a [ [ Result ] ] do
* not impact the ability to continue parsing data , they are
* deferred until they are actually accessed . Hence a faulty string
* in an Event that is never used does not impact the program flow .
*
* However , sometimes it may be useful to access , identify or
* validate correctness of a [ [ Result ] ] .
*
* @_docloc api / abi
* /
2022-09-05 23:14:43 +03:00
export function checkResultErrors ( result : Result ) : Array < { path : Array < string | number > , error : Error } > {
// Find the first error (if any)
const errors : Array < { path : Array < string | number > , error : Error } > = [ ] ;
const checkErrors = function ( path : Array < string | number > , object : any ) : void {
if ( ! Array . isArray ( object ) ) { return ; }
for ( let key in object ) {
const childPath = path . slice ( ) ;
childPath . push ( key ) ;
try {
checkErrors ( childPath , object [ key ] ) ;
} catch ( error : any ) {
errors . push ( { path : childPath , error : error } ) ;
}
}
}
checkErrors ( [ ] , result ) ;
return errors ;
}
function getValue ( value : BigNumberish ) : Uint8Array {
2022-12-10 02:21:45 +03:00
let bytes = toBeArray ( value ) ;
2022-09-05 23:14:43 +03:00
2022-11-05 01:08:37 +03:00
assert ( bytes . length <= WordSize , "value out-of-bounds" ,
"BUFFER_OVERRUN" , { buffer : bytes , length : WordSize , offset : bytes.length } ) ;
2022-09-05 23:14:43 +03:00
if ( bytes . length !== WordSize ) {
2022-09-09 06:21:08 +03:00
bytes = getBytesCopy ( concat ( [ Padding . slice ( bytes . length % WordSize ) , bytes ] ) ) ;
2022-09-05 23:14:43 +03:00
}
return bytes ;
}
2022-11-28 05:54:49 +03:00
/ * *
* @_ignore
* /
2022-09-05 23:14:43 +03:00
export abstract class Coder {
// The coder name:
// - address, uint256, tuple, array, etc.
readonly name ! : string ;
// The fully expanded type, including composite types:
// - address, uint256, tuple(address,bytes), uint256[3][4][], etc.
readonly type ! : string ;
// The localName bound in the signature, in this example it is "baz":
// - tuple(address foo, uint bar) baz
readonly localName ! : string ;
// Whether this type is dynamic:
// - Dynamic: bytes, string, address[], tuple(boolean[]), etc.
// - Not Dynamic: address, uint256, boolean[3], tuple(address, uint8)
readonly dynamic ! : boolean ;
constructor ( name : string , type : string , localName : string , dynamic : boolean ) {
defineProperties < Coder > ( this , { name , type , localName , dynamic } , {
name : "string" , type : "string" , localName : "string" , dynamic : "boolean"
} ) ;
}
_throwError ( message : string , value : any ) : never {
2022-10-25 11:06:00 +03:00
assertArgument ( false , message , this . localName , value ) ;
2022-09-05 23:14:43 +03:00
}
abstract encode ( writer : Writer , value : any ) : number ;
abstract decode ( reader : Reader ) : any ;
abstract defaultValue ( ) : any ;
}
2022-11-28 05:54:49 +03:00
/ * *
* @_ignore
* /
2022-09-05 23:14:43 +03:00
export class Writer {
// An array of WordSize lengthed objects to concatenation
# data : Array < Uint8Array > ;
# dataLength : number ;
constructor ( ) {
this . # data = [ ] ;
this . # dataLength = 0 ;
}
get data ( ) : string {
return concat ( this . # data ) ;
}
get length ( ) : number { return this . # dataLength ; }
# writeData ( data : Uint8Array ) : number {
this . # data . push ( data ) ;
this . # dataLength += data . length ;
return data . length ;
}
appendWriter ( writer : Writer ) : number {
2022-09-09 06:21:08 +03:00
return this . # writeData ( getBytesCopy ( writer . data ) ) ;
2022-09-05 23:14:43 +03:00
}
// Arrayish item; pad on the right to *nearest* WordSize
writeBytes ( value : BytesLike ) : number {
2022-09-09 06:21:08 +03:00
let bytes = getBytesCopy ( value ) ;
2022-09-05 23:14:43 +03:00
const paddingOffset = bytes . length % WordSize ;
if ( paddingOffset ) {
2022-09-09 06:21:08 +03:00
bytes = getBytesCopy ( concat ( [ bytes , Padding . slice ( paddingOffset ) ] ) )
2022-09-05 23:14:43 +03:00
}
return this . # writeData ( bytes ) ;
}
// Numeric item; pad on the left *to* WordSize
writeValue ( value : BigNumberish ) : number {
return this . # writeData ( getValue ( value ) ) ;
}
// Inserts a numeric place-holder, returning a callback that can
// be used to asjust the value later
writeUpdatableValue ( ) : ( value : BigNumberish ) = > void {
const offset = this . # data . length ;
this . # data . push ( Padding ) ;
this . # dataLength += WordSize ;
return ( value : BigNumberish ) = > {
this . # data [ offset ] = getValue ( value ) ;
} ;
}
}
2022-11-28 05:54:49 +03:00
/ * *
* @_ignore
* /
2022-09-05 23:14:43 +03:00
export class Reader {
// Allows incomplete unpadded data to be read; otherwise an error
// is raised if attempting to overrun the buffer. This is required
// to deal with an old Solidity bug, in which event data for
// external (not public thoguh) was tightly packed.
readonly allowLoose ! : boolean ;
readonly # data : Uint8Array ;
# offset : number ;
2024-01-13 03:45:26 +03:00
# bytesRead : number ;
# parent : null | Reader ;
# maxInflation : number ;
constructor ( data : BytesLike , allowLoose? : boolean , maxInflation? : number ) {
2022-09-05 23:14:43 +03:00
defineProperties < Reader > ( this , { allowLoose : ! ! allowLoose } ) ;
2022-09-09 06:21:08 +03:00
this . # data = getBytesCopy ( data ) ;
2024-01-13 03:45:26 +03:00
this . # bytesRead = 0 ;
this . # parent = null ;
this . # maxInflation = ( maxInflation != null ) ? maxInflation : 1024 ;
2022-09-05 23:14:43 +03:00
this . # offset = 0 ;
}
get data ( ) : string { return hexlify ( this . # data ) ; }
get dataLength ( ) : number { return this . # data . length ; }
get consumed ( ) : number { return this . # offset ; }
get bytes ( ) : Uint8Array { return new Uint8Array ( this . # data ) ; }
2024-01-13 03:45:26 +03:00
# incrementBytesRead ( count : number ) : void {
if ( this . # parent ) { return this . # parent . # incrementBytesRead ( count ) ; }
this . # bytesRead += count ;
// Check for excessive inflation (see: #4537)
assert ( this . # maxInflation < 1 || this . # bytesRead <= this . # maxInflation * this . dataLength , ` compressed ABI data exceeds inflation ratio of ${ this . # maxInflation } ( see: https:/ \ /github.com/ethers-io/ethers.js/issues/4537 ) ` , "BUFFER_OVERRUN" , {
buffer : getBytesCopy ( this . # data ) , offset : this. # offset ,
length : count , info : {
bytesRead : this. # bytesRead ,
dataLength : this.dataLength
}
} ) ;
}
2022-09-05 23:14:43 +03:00
# peekBytes ( offset : number , length : number , loose? : boolean ) : Uint8Array {
let alignedLength = Math . ceil ( length / WordSize ) * WordSize ;
if ( this . # offset + alignedLength > this . # data . length ) {
if ( this . allowLoose && loose && this . # offset + length <= this . # data . length ) {
alignedLength = length ;
} else {
2022-11-05 01:08:37 +03:00
assert ( false , "data out-of-bounds" , "BUFFER_OVERRUN" , {
2022-09-09 06:21:08 +03:00
buffer : getBytesCopy ( this . # data ) ,
2022-09-05 23:14:43 +03:00
length : this. # data . length ,
offset : this. # offset + alignedLength
} ) ;
}
}
return this . # data . slice ( this . # offset , this . # offset + alignedLength )
}
// Create a sub-reader with the same underlying data, but offset
subReader ( offset : number ) : Reader {
2024-01-13 03:45:26 +03:00
const reader = new Reader ( this . # data . slice ( this . # offset + offset ) , this . allowLoose , this . # maxInflation ) ;
reader . # parent = this ;
return reader ;
2022-09-05 23:14:43 +03:00
}
// Read bytes
readBytes ( length : number , loose? : boolean ) : Uint8Array {
let bytes = this . # peekBytes ( 0 , length , ! ! loose ) ;
2024-01-13 03:45:26 +03:00
this . # incrementBytesRead ( length ) ;
2022-09-05 23:14:43 +03:00
this . # offset += bytes . length ;
// @TODO: Make sure the length..end bytes are all 0?
return bytes . slice ( 0 , length ) ;
}
// Read a numeric values
readValue ( ) : bigint {
return toBigInt ( this . readBytes ( WordSize ) ) ;
}
readIndex ( ) : number {
return toNumber ( this . readBytes ( WordSize ) ) ;
}
}