Development Artifact Cleanup: ✅ BROTHER_NODE REORGANIZATION: Moved development test node to appropriate location - dev/test-nodes/brother_node/: Moved from root directory for better organization - Contains development configuration, test logs, and test chain data - No impact on production systems - purely development/testing artifact ✅ DEVELOPMENT ARTIFACTS IDENTIFIED: - Chain ID: aitbc-brother-chain (test/development chain) - Ports: 8010 (P2P) and 8011 (RPC) - different from production - Environment: .env file with test configuration - Logs: rpc.log and node.log from development testing session (March 15, 2026) ✅ ROOT DIRECTORY CLEANUP: Removed development clutter from production directory - brother_node/ moved to dev/test-nodes/brother_node/ - Root directory now contains only production-ready components - Development artifacts properly organized in dev/ subdirectory DIRECTORY STRUCTURE IMPROVEMENT: 📁 dev/test-nodes/: Development and testing node configurations 🏗️ Root Directory: Clean production structure with only essential components 🧪 Development Isolation: Test environments separated from production BENEFITS: ✅ Clean Production Directory: No development artifacts in root ✅ Better Organization: Development nodes grouped in dev/ subdirectory ✅ Clear Separation: Production vs development environments clearly distinguished ✅ Maintainability: Easier to identify and manage development components RESULT: Successfully moved brother_node development artifact to dev/test-nodes/ subdirectory, cleaning up the root directory while preserving development testing environment for future use.
447 lines
18 KiB
JavaScript
Executable File
447 lines
18 KiB
JavaScript
Executable File
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Decoder = void 0;
|
|
exports.mapComponent = mapComponent;
|
|
exports.mapArgs = mapArgs;
|
|
exports.evSigHash = evSigHash;
|
|
exports.fnSigHash = fnSigHash;
|
|
exports.createContract = createContract;
|
|
exports.deployContract = deployContract;
|
|
exports.events = events;
|
|
const sha3_1 = require("@noble/hashes/sha3");
|
|
const utils_1 = require("@noble/hashes/utils");
|
|
const P = require("micro-packed");
|
|
const utils_ts_1 = require("../utils.js");
|
|
/*
|
|
There is NO network code in the file. However, a user can pass
|
|
NetProvider instance to createContract, and the method would do
|
|
network requests with the api.
|
|
|
|
There is some really crazy stuff going on here with Typescript types.
|
|
*/
|
|
function EPad(p) {
|
|
return P.padLeft(32, p, P.ZeroPad);
|
|
}
|
|
// Main difference between regular array: length stored outside and offsets calculated without length
|
|
function ethArray(inner) {
|
|
return P.wrap({
|
|
size: undefined,
|
|
encodeStream: (w, value) => {
|
|
U256BE_LEN.encodeStream(w, value.length);
|
|
w.bytes(P.array(value.length, inner).encode(value));
|
|
},
|
|
decodeStream: (r) => P.array(U256BE_LEN.decodeStream(r), inner).decodeStream(r.offsetReader(r.pos)),
|
|
});
|
|
}
|
|
const PTR = EPad(P.U32BE);
|
|
const ARRAY_RE = /(.+)(\[(\d+)?\])$/; // TODO: is this correct?
|
|
// Because u32 in eth is not real u32, just U256BE with limits...
|
|
const ethInt = (bits, signed = false) => {
|
|
if (!Number.isSafeInteger(bits) || bits <= 0 || bits % 8 !== 0 || bits > 256)
|
|
throw new Error('ethInt: invalid numeric type');
|
|
const _bits = BigInt(bits);
|
|
const inner = P.bigint(32, false, signed);
|
|
return P.validate(P.wrap({
|
|
size: inner.size,
|
|
encodeStream: (w, value) => inner.encodeStream(w, value),
|
|
decodeStream: (r) => inner.decodeStream(r),
|
|
}), (value) => {
|
|
// TODO: validate useful for narrowing types, need to add support in types?
|
|
if (typeof value === 'number')
|
|
value = BigInt(value);
|
|
P.utils.checkBounds(value, _bits, !!signed);
|
|
return value;
|
|
});
|
|
};
|
|
// Ugly hack, because tuple of pointers considered "dynamic" without any reason.
|
|
function isDyn(args) {
|
|
let res = false;
|
|
if (Array.isArray(args)) {
|
|
for (let arg of args)
|
|
if (arg.size === undefined)
|
|
res = true;
|
|
}
|
|
else {
|
|
for (let arg in args)
|
|
if (args[arg].size === undefined)
|
|
res = true;
|
|
}
|
|
return res;
|
|
}
|
|
// Re-use ptr for len. u32 should be enough.
|
|
const U256BE_LEN = PTR;
|
|
// NOTE: we need as const if we want to access string as values inside types :(
|
|
function mapComponent(c) {
|
|
// Arrays (should be first one, since recursive)
|
|
let m;
|
|
if ((m = ARRAY_RE.exec(c.type))) {
|
|
const inner = mapComponent({ ...c, type: m[1] });
|
|
if (inner.size === 0)
|
|
throw new Error('mapComponent: arrays of zero-size elements disabled (possible DoS attack)');
|
|
// Static array
|
|
if (m[3] !== undefined) {
|
|
const m3 = Number.parseInt(m[3]);
|
|
if (!Number.isSafeInteger(m3))
|
|
throw new Error(`mapComponent: wrong array size=${m[3]}`);
|
|
let out = P.array(m3, inner);
|
|
// Static array of dynamic values should be behind pointer too, again without reason.
|
|
if (inner.size === undefined)
|
|
out = P.pointer(PTR, out);
|
|
return out;
|
|
}
|
|
else {
|
|
// Dynamic array
|
|
return P.pointer(PTR, ethArray(inner));
|
|
}
|
|
}
|
|
if (c.type === 'tuple') {
|
|
const components = c.components;
|
|
let hasNames = true;
|
|
const args = [];
|
|
for (let comp of components) {
|
|
if (!comp.name)
|
|
hasNames = false;
|
|
args.push(mapComponent(comp));
|
|
}
|
|
let out;
|
|
// If there is names for all fields -- return struct, otherwise tuple
|
|
if (hasNames) {
|
|
const struct = {};
|
|
for (const arg of components) {
|
|
if (struct[arg.name])
|
|
throw new Error(`mapType: same field name=${arg.name}`);
|
|
struct[arg.name] = mapComponent(arg);
|
|
}
|
|
out = P.struct(struct);
|
|
}
|
|
else
|
|
out = P.tuple(args);
|
|
// If tuple has dynamic elements it becomes dynamic too, without reason.
|
|
if (isDyn(args))
|
|
out = P.pointer(PTR, out);
|
|
return out;
|
|
}
|
|
if (c.type === 'string')
|
|
return P.pointer(PTR, P.padRight(32, P.string(U256BE_LEN), P.ZeroPad));
|
|
if (c.type === 'bytes')
|
|
return P.pointer(PTR, P.padRight(32, P.bytes(U256BE_LEN), P.ZeroPad));
|
|
if (c.type === 'address')
|
|
return EPad(P.hex(20, { isLE: false, with0x: true }));
|
|
if (c.type === 'bool')
|
|
return EPad(P.bool);
|
|
if ((m = /^(u?)int([0-9]+)?$/.exec(c.type)))
|
|
return ethInt(m[2] ? +m[2] : 256, m[1] !== 'u');
|
|
if ((m = /^bytes([0-9]{1,2})$/.exec(c.type))) {
|
|
const parsed = +m[1];
|
|
if (!parsed || parsed > 32)
|
|
throw new Error('wrong bytes<N> type');
|
|
return P.padRight(32, P.bytes(parsed), P.ZeroPad);
|
|
}
|
|
throw new Error(`mapComponent: unknown component=${c}`);
|
|
}
|
|
// Because args and output are not tuple
|
|
// TODO: try merge with mapComponent
|
|
function mapArgs(args) {
|
|
// More ergonomic input/output
|
|
if (args.length === 1)
|
|
return mapComponent(args[0]);
|
|
let hasNames = true;
|
|
for (const arg of args)
|
|
if (!arg.name)
|
|
hasNames = false;
|
|
if (hasNames) {
|
|
const out = {};
|
|
for (const arg of args) {
|
|
const name = arg.name;
|
|
if (out[name])
|
|
throw new Error(`mapArgs: same field name=${name}`);
|
|
out[name] = mapComponent(arg);
|
|
}
|
|
return P.struct(out);
|
|
}
|
|
else
|
|
return P.tuple(args.map(mapComponent));
|
|
}
|
|
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
|
|
function evSigHash(o) {
|
|
return (0, utils_1.bytesToHex)((0, sha3_1.keccak_256)(fnSignature(o)));
|
|
}
|
|
function fnSigHash(o) {
|
|
return evSigHash(o).slice(0, 8);
|
|
}
|
|
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) => (0, utils_1.concatBytes)((0, utils_1.hexToBytes)(sh), inputs ? inputs.encode(v) : new Uint8Array());
|
|
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 = (0, utils_ts_1.add0x)((0, utils_1.bytesToHex)(encodeInput(args)));
|
|
const callArgs = Object.assign({ to: contract, data }, overrides);
|
|
return decodeOutput((0, utils_1.hexToBytes)((0, utils_ts_1.strip0x)(await net.ethCall(callArgs))));
|
|
};
|
|
res[name].estimateGas = async (args, overrides = {}) => {
|
|
if (!contract && !overrides.to)
|
|
throw new Error('No contract address');
|
|
const data = (0, utils_ts_1.add0x)((0, utils_1.bytesToHex)(encodeInput(args)));
|
|
const callArgs = Object.assign({ to: contract, data }, overrides);
|
|
return await net.estimateGas(callArgs);
|
|
};
|
|
}
|
|
return res;
|
|
}
|
|
function deployContract(abi, bytecodeHex, ...args) {
|
|
const bytecode = utils_ts_1.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]) : new Uint8Array();
|
|
}
|
|
if (!consCall)
|
|
throw new Error('constructor not found');
|
|
return utils_ts_1.ethHex.encode((0, utils_1.concatBytes)(bytecode, consCall));
|
|
}
|
|
// TODO: try to simplify further
|
|
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) => (0, utils_ts_1.omit)(i, 'name')));
|
|
const sigHash = evSigHash(elm);
|
|
res[elm.name] = {
|
|
decode(topics, _data) {
|
|
const data = (0, utils_1.hexToBytes)((0, utils_ts_1.strip0x)(_data));
|
|
if (!elm.anonymous) {
|
|
if (!topics[0])
|
|
throw new Error('No signature on non-anonymous event');
|
|
if ((0, utils_ts_1.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((0, utils_1.hexToBytes)((0, utils_ts_1.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 (0, utils_ts_1.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((0, utils_ts_1.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 = (0, utils_1.bytesToHex)(packer.encode(value));
|
|
else if (['string', 'bytes'].includes(input.type))
|
|
topic = (0, utils_1.bytesToHex)((0, sha3_1.keccak_256)(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 = (0, utils_1.bytesToHex)((0, sha3_1.keccak_256)((0, utils_1.concatBytes)(...parts)));
|
|
}
|
|
res.push((0, utils_ts_1.add0x)(topic));
|
|
ii++;
|
|
}
|
|
return res;
|
|
},
|
|
};
|
|
}
|
|
return res;
|
|
}
|
|
class Decoder {
|
|
constructor() {
|
|
this.contracts = {};
|
|
this.sighashes = {};
|
|
this.evContracts = {};
|
|
this.evSighashes = {};
|
|
}
|
|
add(contract, abi) {
|
|
const ev = events(abi);
|
|
contract = (0, utils_ts_1.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 = (0, utils_ts_1.strip0x)(contract).toLowerCase();
|
|
const sh = (0, utils_1.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 = (0, utils_ts_1.strip0x)(contract).toLowerCase();
|
|
const sh = (0, utils_1.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: (0, utils_ts_1.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 = (0, utils_ts_1.strip0x)(contract).toLowerCase();
|
|
if (!topics.length)
|
|
return;
|
|
const sh = (0, utils_ts_1.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: (0, utils_ts_1.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;
|
|
}
|
|
}
|
|
exports.Decoder = Decoder;
|
|
//# sourceMappingURL=decoder.js.map
|