micro-eth-signer
Version:
Minimal library for Ethereum transactions, addresses and smart contracts
285 lines • 12 kB
JavaScript
import { keccak_256 } from '@noble/hashes/sha3.js';
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
import * as P from 'micro-packed';
import { add0x, ethHex, omit, strip0x, zip, } from "../utils.js";
import { ARRAY_RE, mapArgs, mapComponent, } from "./abi-mapper.js";
function fnSignature(o) {
if (!o.type)
throw new Error('ABI.fnSignature wrong argument');
if (o.type === 'function' || o.type === 'event')
return `${o.name || 'function'}(${(o.inputs || []).map((i) => fnSignature(i)).join(',')})`;
if (o.type.startsWith('tuple')) {
if (!o.components || !o.components.length)
throw new Error('ABI.fnSignature wrong tuple');
return `(${o.components.map((i) => fnSignature(i)).join(',')})${o.type.slice(5)}`;
}
return o.type;
}
// Function signature hash
export function evSigHash(o) {
return bytesToHex(keccak_256(utf8ToBytes(fnSignature(o))));
}
export function fnSigHash(o) {
return evSigHash(o).slice(0, 8);
}
export function createContract(abi, net, contract) {
// Find non-uniq function names so we can handle overloads
let nameCnt = {};
for (let fn of abi) {
if (fn.type !== 'function')
continue;
const name = fn.name || 'function';
if (!nameCnt[name])
nameCnt[name] = 1;
else
nameCnt[name]++;
}
const res = {};
for (let fn of abi) {
if (fn.type !== 'function')
continue;
let name = fn.name || 'function';
if (nameCnt[name] > 1)
name = fnSignature(fn);
const sh = fnSigHash(fn);
const inputs = fn.inputs && fn.inputs.length ? mapArgs(fn.inputs) : undefined;
const outputs = fn.outputs ? mapArgs(fn.outputs) : undefined;
const decodeOutput = (b) => outputs && outputs.decode(b);
const encodeInput = (v) => concatBytes(hexToBytes(sh), inputs ? inputs.encode(v) : Uint8Array.of());
res[name] = { decodeOutput, encodeInput };
// .call and .estimateGas call network, when net is available
if (!net)
continue;
res[name].call = async (args, overrides = {}) => {
if (!contract && !overrides.to)
throw new Error('No contract address');
const data = add0x(bytesToHex(encodeInput(args)));
const callArgs = Object.assign({ to: contract, data }, overrides);
return decodeOutput(hexToBytes(strip0x(await net.ethCall(callArgs))));
};
res[name].estimateGas = async (args, overrides = {}) => {
if (!contract && !overrides.to)
throw new Error('No contract address');
const data = add0x(bytesToHex(encodeInput(args)));
const callArgs = Object.assign({ to: contract, data }, overrides);
return await net.estimateGas(callArgs);
};
}
return res;
}
export function deployContract(abi, bytecodeHex, ...args) {
const bytecode = ethHex.decode(bytecodeHex);
let consCall;
for (let fn of abi) {
if (fn.type !== 'constructor')
continue;
const inputs = fn.inputs && fn.inputs.length ? mapArgs(fn.inputs) : undefined;
if (inputs === undefined && args !== undefined && args.length)
throw new Error('arguments to constructor without any');
consCall = inputs ? inputs.encode(args[0]) : Uint8Array.of();
}
if (!consCall)
throw new Error('constructor not found');
return ethHex.encode(concatBytes(bytecode, consCall));
}
// TODO: try to simplify further
export function events(abi) {
let res = {};
for (let elm of abi) {
// Only named events supported
if (elm.type !== 'event' || !elm.name)
continue;
const inputs = elm.inputs || [];
let hasNames = true;
for (let i of inputs)
if (!i.name)
hasNames = false;
const plainInp = inputs.filter((i) => !i.indexed);
const indexedInp = inputs.filter((i) => i.indexed);
const indexed = indexedInp.map((i) => !['string', 'bytes', 'tuple'].includes(i.type) && !ARRAY_RE.exec(i.type)
? mapArgs([i])
: null);
const parser = mapArgs(hasNames ? plainInp : plainInp.map((i) => omit(i, 'name')));
const sigHash = evSigHash(elm);
res[elm.name] = {
decode(topics, _data) {
const data = hexToBytes(strip0x(_data));
if (!elm.anonymous) {
if (!topics[0])
throw new Error('No signature on non-anonymous event');
if (strip0x(topics[0]).toLowerCase() !== sigHash)
throw new Error('Wrong signature');
topics = topics.slice(1);
}
if (topics.length !== indexed.length)
throw new Error('Wrong topics length');
let parsed = parser ? parser.decode(data) : hasNames ? {} : [];
const indexedParsed = indexed.map((p, i) => p ? p.decode(hexToBytes(strip0x(topics[i]))) : topics[i]);
if (plainInp.length === 1)
parsed = hasNames ? { [plainInp[0].name]: parsed } : [parsed];
if (hasNames) {
let res = { ...parsed };
for (let [a, p] of zip(indexedInp, indexedParsed))
res[a.name] = p;
return res;
}
else
return inputs.map((i) => (!i.indexed ? parsed : indexedParsed).shift());
},
topics(values) {
let res = [];
if (!elm.anonymous)
res.push(add0x(sigHash));
// We require all keys to be set, even if they are null, to be sure nothing is accidentaly missed
if ((hasNames ? Object.keys(values) : values).length !== inputs.length)
throw new Error('Wrong topics args');
for (let i = 0, ii = 0; i < inputs.length && ii < indexed.length; i++) {
const [input, packer] = [inputs[i], indexed[ii]];
if (!input.indexed)
continue;
const value = values[Array.isArray(values) ? i : inputs[i].name];
if (value === null) {
res.push(null);
continue;
}
let topic;
if (packer)
topic = bytesToHex(packer.encode(value));
else if (['string', 'bytes'].includes(input.type))
topic = bytesToHex(keccak_256(typeof value === 'string' ? utf8ToBytes(value) : value));
else {
let m, parts;
if ((m = ARRAY_RE.exec(input.type)))
parts = value.map((j) => mapComponent({ type: m[1] }).encode(j));
else if (input.type === 'tuple' && input.components)
parts = input.components.map((j) => mapArgs([j]).encode(value[j.name]));
else
throw new Error('Unknown unsized type');
topic = bytesToHex(keccak_256(concatBytes(...parts)));
}
res.push(add0x(topic));
ii++;
}
return res;
},
};
}
return res;
}
export class Decoder {
contracts = {};
sighashes = {};
evContracts = {};
evSighashes = {};
add(contract, abi) {
const ev = events(abi);
contract = strip0x(contract).toLowerCase();
if (!this.contracts[contract])
this.contracts[contract] = {};
if (!this.evContracts[contract])
this.evContracts[contract] = {};
for (let fn of abi) {
if (fn.type === 'function') {
const selector = fnSigHash(fn);
const value = {
name: fn.name || 'function',
signature: fnSignature(fn),
packer: fn.inputs && fn.inputs.length ? mapArgs(fn.inputs) : undefined,
hint: fn.hint,
hook: fn.hook,
};
this.contracts[contract][selector] = value;
if (!this.sighashes[selector])
this.sighashes[selector] = [];
this.sighashes[selector].push(value);
}
else if (fn.type === 'event') {
if (fn.anonymous || !fn.name)
continue;
const selector = evSigHash(fn);
const value = {
name: fn.name,
signature: fnSignature(fn),
decoder: ev[fn.name]?.decode,
hint: fn.hint,
};
this.evContracts[contract][selector] = value;
if (!this.evSighashes[selector])
this.evSighashes[selector] = [];
this.evSighashes[selector].push(value);
}
}
}
method(contract, data) {
contract = strip0x(contract).toLowerCase();
const sh = bytesToHex(data.slice(0, 4));
if (!this.contracts[contract] || !this.contracts[contract][sh])
return;
const { name } = this.contracts[contract][sh];
return name;
}
// Returns: exact match, possible options of matches (array) or undefined.
// Note that empty value possible if there is no arguments in call.
decode(contract, _data, opt) {
contract = strip0x(contract).toLowerCase();
const sh = bytesToHex(_data.slice(0, 4));
const data = _data.slice(4);
if (this.contracts[contract] && this.contracts[contract][sh]) {
let { name, signature, packer, hint, hook } = this.contracts[contract][sh];
const value = packer ? packer.decode(data) : undefined;
let res = { name, signature, value };
// NOTE: hint && hook fn is used only on exact match of contract!
if (hook)
res = hook(this, contract, res, opt);
try {
if (hint)
res.hint = hint(value, Object.assign({ contract: add0x(contract) }, opt));
}
catch (e) { }
return res;
}
if (!this.sighashes[sh] || !this.sighashes[sh].length)
return;
let res = [];
for (let { name, signature, packer } of this.sighashes[sh]) {
try {
res.push({ name, signature, value: packer ? packer.decode(data) : undefined });
}
catch (err) { }
}
if (res.length)
return res;
return;
}
decodeEvent(contract, topics, data, opt) {
contract = strip0x(contract).toLowerCase();
if (!topics.length)
return;
const sh = strip0x(topics[0]);
const event = this.evContracts[contract];
if (event && event[sh]) {
let { name, signature, decoder, hint } = event[sh];
const value = decoder(topics, data);
let res = { name, signature, value };
try {
if (hint)
res.hint = hint(value, Object.assign({ contract: add0x(contract) }, opt));
}
catch (e) { }
return res;
}
if (!this.evSighashes[sh] || !this.evSighashes[sh].length)
return;
let res = [];
for (let { name, signature, decoder } of this.evSighashes[sh]) {
try {
res.push({ name, signature, value: decoder(topics, data) });
}
catch (err) { }
}
if (res.length)
return res;
return;
}
}
//# sourceMappingURL=abi-decoder.js.map