Skip to content

Commit

Permalink
optimise abi encoder to support more situation
Browse files Browse the repository at this point in the history
  • Loading branch information
Eason Smith committed Dec 30, 2024
1 parent bb9a592 commit 64c52d4
Show file tree
Hide file tree
Showing 3 changed files with 276 additions and 50 deletions.
6 changes: 3 additions & 3 deletions pages/ethers/erc20.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import abi from './usdt.abi.json';
export default function Erc20() {

const usdt = new ContractEncoder(abi as AbiFunctionItem[]);

const data = usdt.transfer(
'0x1111111111111111111111111111111111111111', // receiver address
10**17 // 0.1 USDT
Expand Down Expand Up @@ -35,7 +35,7 @@ export default function Erc20() {
from: from,
to: contractAddress,
value: '0x0',
data: data,
data,
};

const txHash = await window.ethereum.request({
Expand All @@ -54,7 +54,7 @@ export default function Erc20() {
}, [data, contractAddress]);

return (
<Box>
<Box width='$full' minHeight='100vh' display='flex' flexDirection='column' alignItems='center' justifyContent='center'>
<Button onClick={sendTransaction}>Send USDT</Button>
</Box>
);
Expand Down
54 changes: 33 additions & 21 deletions utils/ethereum/ContractEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,53 @@ import { encodeParameters, getFunctionSelector } from './abiEncoder';
export interface AbiFunctionItem {
type: 'function' | string;
name: string;
inputs: { name?: string; type: string }[];
// outputs, stateMutability...
inputs: Array<{ name?: string; type: string }>;
}

export class ContractEncoder {
[key: string]: any;
private methods: Map<string, AbiFunctionItem> = new Map();

private functionsMap: Map<string, AbiFunctionItem[]> = new Map();

constructor(private abi: AbiFunctionItem[]) {
constructor(abi: AbiFunctionItem[]) {
for (const item of abi) {
if (item.type === 'function') {
this.methods.set(item.name, item);
const fnName = item.name;
if (!this.functionsMap.has(fnName)) {
this.functionsMap.set(fnName, []);
}
this.functionsMap.get(fnName)!.push(item);
}
}

return new Proxy(this, {
get: (target: ContractEncoder, propertyKey: string | symbol, receiver: any) => {
if (typeof propertyKey === 'string' && target.methods.has(propertyKey)) {
return (...args: unknown[]) => {
const abiItem = target.methods.get(propertyKey)!;
const inputTypes = abiItem.inputs.map((i) => i.type);

const signature =
abiItem.name + '(' + inputTypes.join(',') + ')';

const selector = getFunctionSelector(signature);

const encodedArgs = encodeParameters(inputTypes, args);

return '0x' + selector + encodedArgs;
get: (target, propertyKey, receiver) => {
if (typeof propertyKey === 'string' && target.functionsMap.has(propertyKey)) {
return (...args: any[]) => {
const candidates = target.functionsMap.get(propertyKey)!;
const matched = candidates.filter(c => c.inputs.length === args.length);
if (matched.length === 0) {
throw new Error(`No matching overload for function "${propertyKey}" with ${args.length} args`);
} else if (matched.length === 1) {
return target.encodeCall(matched[0], args);
} else {
throw new Error(`Multiple overloads for function "${propertyKey}" with ${args.length} args. You need more strict matching logic.`);
}
};
}

return Reflect.get(target, propertyKey, receiver);
},
}
});
}

private encodeCall(fnAbi: AbiFunctionItem, args: any[]): string {
const types = fnAbi.inputs.map(i => i.type);
const signature = fnAbi.name + '(' + types.join(',') + ')';

const selector = getFunctionSelector(signature);

const encodedArgs = encodeParameters(types, args);

return '0x' + selector + encodedArgs;
}
}
266 changes: 240 additions & 26 deletions utils/ethereum/abiEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,258 @@ function leftPadZeros(value: string, length: number): string {
return value.padStart(length, '0');
}

function encodeSingle(type: string, value: unknown): string {
switch (true) {
case /^(u?int)([0-9]*)$/.test(type):
{
let hexValue = Number(value).toString(16);
hexValue = leftPadZeros(hexValue, 64);
return hexValue;
}
function toHex(value: number | bigint | string): string {
if (typeof value === 'number' || typeof value === 'bigint') {
return value.toString(16);
} else if (typeof value === 'string') {
if (value.startsWith('0x')) {
return value.slice(2);
} else {
const num = BigInt(value);
return num.toString(16);
}
} else {
throw new Error(`Cannot convert value=${value} to hex`);
}
}

function encodeUint256(value: number | bigint | string): string {
let hexValue = toHex(value);
hexValue = leftPadZeros(hexValue, 64);
return hexValue;
}

function encodeBoolean(value: boolean): string {
return leftPadZeros(value ? '1' : '0', 64);
}

function encodeAddress(addr: string): string {
const without0x = addr.replace(/^0x/i, '');
return leftPadZeros(without0x.toLowerCase(), 64);
}


interface EncodedParameter {
headLength: number;

encodeHead(offsetInBytes: number): string;

encodeTail(): string;
}

function encodeSingleParameter(type: string, value: any): EncodedParameter {
const arrayMatch = type.match(/^(.*)\[(.*?)\]$/);
if (arrayMatch) {
const baseType = arrayMatch[1];
const lengthInType = arrayMatch[2];

case /^address$/.test(type):
{
let addr = String(value).replace(/^0x/i, '').toLowerCase();
addr = leftPadZeros(addr, 64);
return addr;
if (lengthInType === '') {
return encodeDynamicArray(baseType, value);
} else {
const fixedLen = parseInt(lengthInType, 10);
return encodeFixedArray(baseType, fixedLen, value);
}
}

if (type.startsWith('bytes')) {
const match = type.match(/^bytes([0-9]+)$/);
if (match) {
// bytesN (1 <= N <= 32)
const n = parseInt(match[1]);
return encodeFixedBytes(value, n);
} else {
return encodeDynamicBytes(value);
}
}

if (type === 'string') {
return encodeDynamicString(value);
}

if (type === 'bool') {
const headVal = encodeBoolean(Boolean(value));
return {
headLength: 32,
encodeHead: () => headVal,
encodeTail: () => '',
};
}

if (type === 'address') {
const headVal = encodeAddress(value);
return {
headLength: 32,
encodeHead: () => headVal,
encodeTail: () => '',
};
}

if (/^(u?int)([0-9]*)$/.test(type)) {
const headVal = encodeUint256(value);
return {
headLength: 32,
encodeHead: () => headVal,
encodeTail: () => '',
};
}

throw new Error(`Unsupported or unrecognized type: ${type}`);
}

function encodeDynamicBytes(raw: string | Uint8Array): EncodedParameter {
let byteArray: Uint8Array;
if (typeof raw === 'string') {
if (raw.startsWith('0x')) {
const hex = raw.slice(2);
const arr = hex.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
byteArray = new Uint8Array(arr);
} else {
byteArray = new TextEncoder().encode(raw);
}
} else {
byteArray = raw;
}

const lengthHex = encodeUint256(byteArray.length);
const dataHex = Buffer.from(byteArray).toString('hex');
const mod = dataHex.length % 64;
const padLength = mod === 0 ? 0 : 64 - mod;

const tailHex = dataHex + '0'.repeat(padLength);

return {
headLength: 32,
encodeHead: (offsetInBytes: number) => {
const offsetHex = leftPadZeros(offsetInBytes.toString(16), 64);
return offsetHex;
},
encodeTail: () => {
// [length(32bytes)] + [data(N bytes + padding)]
return lengthHex + tailHex;
},
};
}

function encodeDynamicString(text: string): EncodedParameter {
return encodeDynamicBytes(text);
}

function encodeDynamicArray(baseType: string, arr: any[]): EncodedParameter {
const encodedItems = arr.map((item) => encodeSingleParameter(baseType, item));
return {
headLength: 32,
encodeHead: (offsetInBytes: number) => {
const offsetHex = leftPadZeros(offsetInBytes.toString(16), 64);
return offsetHex;
},
encodeTail: () => {
let tail = encodeUint256(arr.length);
let totalHeadLength = 32 * arr.length;
const encodedHeads: string[] = [];
const encodedTails: string[] = [];

let currentOffset = 32 * arr.length;

for (let i = 0; i < encodedItems.length; i++) {
const enc = encodedItems[i];
// head
const headHex = enc.encodeHead(currentOffset);
encodedHeads.push(headHex);

// tail
const tailHex = enc.encodeTail();
encodedTails.push(tailHex);

const tailBytes = tailHex.length / 2; // 2 hex = 1 byte
currentOffset += tailBytes;
}

case /^bool$/.test(type):
{
// true -> 1, false -> 0
const boolHex = value ? '1' : '0';
return leftPadZeros(boolHex, 64);
tail += encodedHeads.join('');
tail += encodedTails.join('');
return tail;
},
};
}


function encodeFixedArray(baseType: string, length: number, arr: any[]): EncodedParameter {
if (arr.length !== length) {
throw new Error(`Fixed array length mismatch: expect ${length}, got ${arr.length}`);
}
const encodedItems = arr.map((item) => encodeSingleParameter(baseType, item));

let totalHeadLength = 0;
for (const enc of encodedItems) {
totalHeadLength += enc.headLength;
}

return {
headLength: totalHeadLength,
encodeHead: (offsetInBytes: number) => {
let heads = '';
let currentOffset = 0;
for (const enc of encodedItems) {
const headHex = enc.encodeHead(offsetInBytes + totalHeadLength + currentOffset);
heads += headHex;
currentOffset += enc.encodeTail().length / 2;
}
return heads;
},
encodeTail: () => {
let tails = '';
for (const enc of encodedItems) {
tails += enc.encodeTail();
}
return tails;
},
};
}


default:
throw new Error(`Unsupported type: ${type}`);
function encodeFixedBytes(value: string, length: number): EncodedParameter {
let hex = value.replace(/^0x/i, '');
const maxLen = length * 2; // N bytes => 2*N hex
if (hex.length > maxLen) {
hex = hex.slice(0, maxLen);
} else if (hex.length < maxLen) {
hex = hex.padEnd(maxLen, '0');
}
hex = hex.padEnd(64, '0');

return {
headLength: 32,
encodeHead: () => hex,
encodeTail: () => '',
};
}

export function encodeParameters(types: string[], values: unknown[]): string {
export function encodeParameters(types: string[], values: any[]): string {
if (types.length !== values.length) {
throw new Error('Types and values length mismatch');
throw new Error('Types count and values count do not match');
}

let encoded = '';
for (let i = 0; i < types.length; i++) {
encoded += encodeSingle(types[i], values[i]);
const encodedList = types.map((t, i) => encodeSingleParameter(t, values[i]));

let totalHeadLength = 0;
for (const enc of encodedList) {
totalHeadLength += enc.headLength;
}
return encoded;

let heads = '';
let tails = '';
let currentOffset = 0;

for (const enc of encodedList) {
const headHex = enc.encodeHead(totalHeadLength + currentOffset);
heads += headHex;

const tailHex = enc.encodeTail();
tails += tailHex;

currentOffset += tailHex.length / 2;
}

return heads + tails;
}

export function getFunctionSelector(signature: string): string {
Expand Down

0 comments on commit 64c52d4

Please sign in to comment.