0xweb
Version:
Contract package manager and other web3 tools
333 lines (294 loc) • 11.9 kB
text/typescript
import type { TAbiItem, TAbiInput } from '@dequanto/types/TAbi';
import { $is } from './$is';
import { $hex } from './$hex';
import { $str } from '@dequanto/solidity/utils/$str';
import { $types } from '@dequanto/solidity/utils/$types';
import { $abiType } from './$abiType';
import { $contract } from './$contract';
import { $abiParser } from './$abiParser';
import { ParamType } from '@dequanto/abi/fragments';
import { $abiCoder } from '@dequanto/abi/$abiCoder';
import { TEth } from '@dequanto/models/TEth';
import alot from 'alot';
export namespace $abiUtils {
export function encodePacked(types: string[], values: any[]): TEth.Hex
export function encodePacked(typeValues: [string, any][]): TEth.Hex
export function encodePacked(types: ReadonlyArray<string | ParamType>, values: ReadonlyArray<any>): TEth.Hex
export function encodePacked(...typeValues: { type: string, value: any }[]): TEth.Hex
export function encodePacked(...typeValues: [string, any][]): TEth.Hex
export function encodePacked(...mix): TEth.Hex {
let val: {type, value}[];
if (arguments.length === 1) {
let arr = arguments[0];
let isTypeValueNestedArray = Array.isArray(mix)
&& mix.length > 0
&& Array.isArray(mix[0])
&& mix[0].length === 2
&& typeof mix[0][0] === 'string';
if (isTypeValueNestedArray) {
val = arr.map(([type, value]) => {
return { type, value };
});
}
}
if (arguments.length === 2 && Array.isArray(mix[0])) {
// && typeof mix[0][0] === 'string'
let [types, values] = mix;
val = types.map((type, i) => {
return { type, value: values[i] };
});
}
// [type, value], [type, value], ....
if (val == null && arguments.length > 0 && Array.isArray(arguments[0]) && arguments[0].length === 2 && typeof arguments[0][0] ==='string') {
val = Array.from(arguments).map(([type, value]) => {
return { type, value };
});
}
if (val == null) {
val = mix;
}
let types = val.map(x => x.type);
let values = val.map(x => x.value);
return $abiCoder.encodePacked(types, values);
}
export function encode(typeValues: [string, any][]): TEth.Hex
export function encode(types: ReadonlyArray<string | TAbiInput>, values: any[]): TEth.Hex
export function encode(mix: [string, any][] | ReadonlyArray<string | TAbiInput>, values?: any[]): TEth.Hex {
let types: (string | ParamType)[];
if (Array.isArray(mix) && mix.length > 0 && mix[0].length === 2 && typeof mix[0][0] === 'string') {
types = mix.map(x => x[0]);
values = mix.map(x => x[1]);
} else {
types = mix as any;
}
if (types.length === 0) {
return '0x' as TEth.Hex;
}
return $abiCoder.encode(types, values)
}
export function decode(types: (string | ParamType | TAbiInput)[], data: string) {
let arr: any[] = $abiCoder.decode(types, data);
// Add parameters as dictionary, to be compatible with web3js, but consider to remove this
let params: { [ key: string ]: any }
let asObject = types.every(x => x != null && typeof x === 'object' && x.name != null);
if (asObject) {
params = alot(types as ParamType[]).map((x, i) => {
return { key: x.name, value: arr[i] }
}).toDictionary(x => x.key, x => x.value);
}
return {
args: arr,
params,
};
}
export function decodePacked <T = any>(types: string | string[] | ParamType[] | TAbiInput | TAbiInput[] | ParamType, data: string) {
return DecodePacked.decodePacked (types, data) as T;
}
/** Returns complete method/event hash */
export function getMethodHash(mix: string | TAbiItem) {
let abi = typeof mix === 'string'
? $abiParser.parseMethod(mix)
: mix;
let types = abi.inputs?.map(serializeMethodSignatureArgumentType) ?? [];
let signature = `${abi.name}(${types.join(',')})`;
let hash = $contract.keccak256(signature);
return hash;
}
export function getMethodSignature(mix: string | TAbiItem) {
let abi = typeof mix === 'string'
? $abiParser.parseMethod(mix)
: mix;
let types = abi.inputs?.map(serializeMethodSignatureArgumentType) ?? [];
let signature = `${abi.name}(${types.join(',')})`;
let hash = $contract.keccak256(signature);
return hash.substring(0, 10);
}
export function serializeMethodCallData(abi: string | TAbiItem, params?: any[]) {
if (typeof abi === 'string') {
abi = $abiParser.parseMethod(abi);
}
let sig = abi.signature ?? $abiUtils.getMethodSignature(abi);
let data = $abiUtils.encode(abi.inputs, params ?? []);
return (sig + data.substring(2)) as TEth.Hex;
}
export function parseMethodCallData(mixAbi: string | TAbiItem | TAbiItem[], mixInput: TEth.BufferLike | Pick<TEth.TxLike, 'data' | 'input' | 'value'>) {
if (typeof mixInput === 'string' || mixInput instanceof Uint8Array) {
mixInput = { data: mixInput } as any;
return parseMethodCallData(mixAbi, mixInput);
}
let abis: TAbiItem[];
if (typeof mixAbi === 'string') {
abis = [ $abiParser.parseMethod(mixAbi) ];
} else if (Array.isArray(mixAbi)) {
abis = mixAbi;
} else {
abis = [ mixAbi ];
}
let tx = mixInput;
let input = tx.input?? tx.data;
let str = $hex.ensure(input);
let methodHex = `${str.substring(0, 10)}`;
let bytesHex = `0x${str.substring(10)}`;
let abiFns = abis.filter(x => x.type === 'function');
let abi = abiFns.find(abi => {
let sig = getMethodSignature(abi);
return sig === methodHex;
});
if (abi == null) {
console.log(`Could not find the ABI for ${methodHex}; Available ${ abiFns.map(x => x.name).join(', ') }`);
return null;
}
let { args, params } = decode(abi.inputs, bytesHex);
return {
name: abi.name,
args,
params,
value: tx.value,
};
}
export function getTopicSignature(abi: TAbiItem) {
if ($is.Hex(abi.name)) {
// anonymous event
return abi.name;
}
let types = abi.inputs?.map(serializeMethodSignatureArgumentType) ?? [];
let signature = `${abi.name}(${types.join(',')})`;
let hash = $contract.keccak256(signature);
return hash;
}
export function checkInterfaceOf(abi: TAbiItem[], iface: TAbiItem[]): { ok: boolean, missing?: string } {
if (iface == null || iface.length === 0) {
return { ok: false };
}
for (let item of iface) {
if (item.type === 'constructor') {
continue;
}
let inAbi = abi.some(x => abiEquals(x, item));
if (inAbi === false) {
return { ok: false, missing: item.name };
}
}
return { ok: true };
}
export function isDynamicType(type: string) {
if (type === 'string' || type === 'bytes') {
return true;
}
if (/\[\]$/.test(type)) {
return true;
}
if (type.includes('mapping')) {
return true;
}
return false;
}
export function isReadMethod (abi: TAbiItem): boolean {
return abi.type === 'function' && ['view', 'pure', null].includes(abi.stateMutability);
}
export function fromAliasIfAny(type: string) {
if (type === 'uint') {
return 'uint256';
}
if (type === 'byte') {
return 'bytes1';
}
return type;
}
function abiEquals(a: TAbiItem, b: TAbiItem) {
if (a.name !== b.name) {
return false;
}
let aInputs = a.inputs ?? [];
let bInputs = b.inputs ?? [];
if (aInputs.length !== bInputs.length) {
return false;
}
//@TODO: may be better TAbiInput comparison?
for (let i = 0; i < aInputs.length; i++) {
let aInput = aInputs[i];
let bInput = bInputs[i];
if (aInput?.type !== bInput?.type) {
return false;
}
}
return true;
}
function serializeMethodSignatureArgumentType(input: TAbiItem['inputs'][0]) {
if (input.type === 'tuple') {
return serializeComponents(input.components);
}
if (input.type === 'tuple[]') {
return serializeComponents(input.components) + '[]';
}
let type = fromAliasIfAny(input.type);
return type;
}
function serializeComponents(components: TAbiInput[]) {
let types = components.map(x => serializeMethodSignatureArgumentType(x));
return `(${types.join(',')})`;
}
}
namespace DecodePacked {
export function decodePacked(mix: string | string[] | ParamType[] | TAbiInput | TAbiInput[] | ParamType, hex: string) {
let size = $hex.getBytesLength(hex);
let buffer = { hex, cursor: 0, size }
if (Array.isArray(mix) === false) {
return decodeSingle(mix as ParamType, buffer)?.value
}
if (Array.isArray(mix) && typeof mix[0] === 'string') {
mix = mix.map(type => ({ type })) as ParamType[];
}
let types = mix as ParamType[];
let arr = types.map(type => decodeSingle(type, buffer)?.value);
return arr;
}
function decodeSingle(type: ParamType, buffer: { hex: string, cursor: number, size: number }): { value, cursor } {
let t = type.type;
if ($types.isArray(t)) {
let size = $abiType.array.getLength(t);
let tuple = { ...type, type: 'tuple' } as ParamType;
let arr = [];
while (arr.length < size && buffer.cursor < buffer.size) {
let { value, cursor } = decodeSingle(tuple, buffer);
arr.push(value);
buffer.cursor = cursor;
}
return { value: arr, cursor: buffer.cursor };
}
if ($types.isMapping(t)) {
throw new Error(`Mappings are not supported for decoding packed data`);
}
if (t === 'tuple') {
let asObject = type.components.every(field => $str.isNullOrWhiteSpace(field.name) === false);
let outputObj = asObject ? {} : null;
let outputArr = asObject ? null : [];
for (let field of type.components) {
let { value, cursor } = decodeSingle(field, buffer);
if (asObject) {
outputObj[field.name] = value;
} else {
outputArr.push(value);
}
buffer.cursor = cursor;
}
return { value: outputObj ?? outputArr, cursor: buffer.cursor };
}
let bits = $types.sizeOf(t);
if (bits === Infinity) {
let lengthSize = 32;
let size = readBuffer(buffer, lengthSize);
bits = Number(size) * 8;
}
let value = readBuffer(buffer, bits / 8);
return {
value: $hex.convert(value, t),
cursor: buffer.cursor
};
}
function readBuffer (buffer: { hex: string, cursor: number, size: number }, size: number) {
let bytes = $hex.getBytes(buffer.hex, buffer.cursor, size);
buffer.cursor += size;
return bytes;
}
}