Start of the next batch; 0.30.0 is published.
- Tron panel sends signed whatever /wallet/createtransaction returned while
the approval showed the local request. The returned bytes are now decoded
and must be one transfer from this wallet, to that address, for that
amount, with a matching txID - checked at plan time and again at signing.
- Importing a Solana wallet from a seed phrase threw on every attempt (a
mis-parenthesised `new require("crypto").createHmac` plus a bare require
of an ESM-only subpath). SLIP-0010 now uses Node's HMAC, as chain-sol does.
- Imported BCH wallets put token-bearing UTXOs into coin selection. They are
excluded, as in the HD wallet, and the balance counts what can be spent.
- WizardConnect dropped the token from each spent output before signing, so
under SIGHASH_UTXOS every signature of a token transaction was invalid.
- A wallet disposed while a refresh was in flight re-armed its poll timer.
- BCMR registry content is bounded before it reaches the panel: control and
bidi characters stripped, lengths capped, decimals 0-18, icons https/ipfs
only, registries https only.
439 lines
20 KiB
JavaScript
439 lines
20 KiB
JavaScript
// Ethereum (EVM) chain adapter — mainnet + Sepolia testnet. One address per
|
||
// wallet, the same "TronLink shape" as chain-tron.js: BIP44 derivation,
|
||
// secp256k1 → keccak256 address, JSON-RPC backend, EIP-1559 send.
|
||
//
|
||
// Kept intentionally minimal:
|
||
// - Native ETH only. ERC-20 token support is a follow-up: it needs a token
|
||
// registry + eth_call for `balanceOf(address)` per token + a dedicated
|
||
// Send flow that builds an ERC-20 `transfer(to, value)` calldata.
|
||
// - No transaction history: without an indexer (Etherscan V2 / Alchemy)
|
||
// the JSON-RPC alone can't answer "which txs touched this address".
|
||
// The panel shows an empty history with a link to Etherscan.
|
||
// - EIP-1559 only (type 0x02). Legacy type 0x00 works too but isn't
|
||
// needed for mainnet or Sepolia in 2026.
|
||
|
||
const NETWORKS = {
|
||
mainnet: {
|
||
id: "mainnet", label: "Mainnet", chainId: 1,
|
||
// Cloudflare's public Ethereum gateway — no key required, rate-limited
|
||
// but adequate for a per-user wallet. User can override in settings.
|
||
defaultRpc: "https://cloudflare-eth.com",
|
||
explorerTx: "https://etherscan.io/tx/",
|
||
explorerAddr: "https://etherscan.io/address/",
|
||
faucet: null,
|
||
},
|
||
sepolia: {
|
||
id: "sepolia", label: "Sepolia testnet", chainId: 11155111,
|
||
defaultRpc: "https://ethereum-sepolia-rpc.publicnode.com",
|
||
explorerTx: "https://sepolia.etherscan.io/tx/",
|
||
explorerAddr: "https://sepolia.etherscan.io/address/",
|
||
faucet: "https://sepoliafaucet.com/",
|
||
},
|
||
};
|
||
|
||
module.exports = function makeEthAdapter({ HDKey, secp256k1, keccak_256 }) {
|
||
if (!HDKey || !secp256k1 || !keccak_256) throw new Error("chain-eth: missing dep");
|
||
|
||
const toHex = (b) => Buffer.from(b).toString("hex");
|
||
const fromHex = (h) => Uint8Array.from(Buffer.from(String(h).replace(/^0x/i, ""), "hex"));
|
||
const stripHex = (h) => String(h).replace(/^0x/i, "");
|
||
const hexToBig = (h) => BigInt("0x" + (stripHex(h) || "0"));
|
||
const bigToHex = (n) => "0x" + BigInt(n).toString(16);
|
||
const zeroBig = 0n;
|
||
|
||
// ---- addresses -------------------------------------------------------
|
||
// EIP-55 mixed-case checksum: lowercase hex, then flip case per keccak256
|
||
// of the lowercase hex string (a-f digits get uppercased where the keccak
|
||
// nibble is >= 8). Never needed for wire format (RPCs accept lowercase),
|
||
// but it's what wallets show, so we return it that way.
|
||
function eip55(addressLowerHex) {
|
||
const lower = stripHex(addressLowerHex).toLowerCase();
|
||
const hash = toHex(keccak_256(Buffer.from(lower, "utf8")));
|
||
let out = "0x";
|
||
for (let i = 0; i < lower.length; i++) {
|
||
const c = lower[i];
|
||
out += /[0-9]/.test(c) ? c : (parseInt(hash[i], 16) >= 8 ? c.toUpperCase() : c);
|
||
}
|
||
return out;
|
||
}
|
||
function addressFromPubkey(uncompressed) {
|
||
const inner = uncompressed.slice(1);
|
||
const h = keccak_256(inner);
|
||
const h20 = h.slice(h.length - 20);
|
||
return eip55(toHex(h20));
|
||
}
|
||
function decodeAddress(str) {
|
||
const s = String(str || "").trim();
|
||
const hex = stripHex(s);
|
||
if (!/^[0-9a-fA-F]{40}$/.test(hex)) throw new Error("bad Ethereum address");
|
||
// Reject checksum mismatches on mixed-case inputs (all-lower and all-upper
|
||
// pass unconditionally — that's the EIP-55 rule).
|
||
const lower = hex.toLowerCase(), upper = hex.toUpperCase();
|
||
if (hex !== lower && hex !== upper) {
|
||
const want = stripHex(eip55(lower));
|
||
if (hex !== want) throw new Error("EIP-55 checksum failed");
|
||
}
|
||
return "0x" + lower;
|
||
}
|
||
|
||
// ---- RLP encode ------------------------------------------------------
|
||
// Minimal encoder — enough for EIP-1559 tx encoding. Follows the RLP spec
|
||
// (single byte < 0x80 → self; short string ≤ 55 → 0x80 + len + bytes;
|
||
// long string → 0x80 + 55 + lenOfLen + lenBytes + bytes; lists similarly
|
||
// with 0xc0/0xf7).
|
||
function rlpEncodeBytes(bytes) {
|
||
const b = Uint8Array.from(bytes);
|
||
if (b.length === 1 && b[0] < 0x80) return b;
|
||
if (b.length <= 55) return concat(Uint8Array.from([0x80 + b.length]), b);
|
||
const lenBytes = encodeIntBE(b.length);
|
||
return concat(Uint8Array.from([0xb7 + lenBytes.length]), lenBytes, b);
|
||
}
|
||
function rlpEncodeList(items) {
|
||
const encoded = items.map(rlpEncode);
|
||
const body = concat(...encoded);
|
||
if (body.length <= 55) return concat(Uint8Array.from([0xc0 + body.length]), body);
|
||
const lenBytes = encodeIntBE(body.length);
|
||
return concat(Uint8Array.from([0xf7 + lenBytes.length]), lenBytes, body);
|
||
}
|
||
function rlpEncode(item) {
|
||
if (item instanceof Uint8Array) return rlpEncodeBytes(item);
|
||
if (Array.isArray(item)) return rlpEncodeList(item);
|
||
if (typeof item === "bigint") return rlpEncodeBytes(bigToBytes(item));
|
||
if (typeof item === "number") return rlpEncodeBytes(bigToBytes(BigInt(item)));
|
||
if (typeof item === "string") return rlpEncodeBytes(item.startsWith("0x") ? fromHex(item) : Buffer.from(item, "utf8"));
|
||
throw new Error("rlp: unsupported item type " + typeof item);
|
||
}
|
||
function bigToBytes(v) {
|
||
if (v < 0n) throw new Error("negative bigint");
|
||
if (v === 0n) return new Uint8Array(0);
|
||
let hex = v.toString(16);
|
||
if (hex.length % 2) hex = "0" + hex;
|
||
return fromHex(hex);
|
||
}
|
||
function encodeIntBE(n) {
|
||
let hex = n.toString(16);
|
||
if (hex.length % 2) hex = "0" + hex;
|
||
return fromHex(hex);
|
||
}
|
||
function concat(...ps) {
|
||
const n = ps.reduce((a, p) => a + p.length, 0);
|
||
const out = new Uint8Array(n); let k = 0;
|
||
for (const p of ps) { out.set(p, k); k += p.length; }
|
||
return out;
|
||
}
|
||
|
||
// ---- signing ---------------------------------------------------------
|
||
// EIP-1559 signed tx: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas,
|
||
// maxFeePerGas, gasLimit, to, value, data, accessList,
|
||
// yParity, r, s])
|
||
// hash-to-sign: keccak256(0x02 || RLP([...same-without-sig-fields]))
|
||
function signTxEip1559(unsignedFields, privKey) {
|
||
const unsignedRlp = rlpEncodeList(unsignedFields);
|
||
const preimage = concat(Uint8Array.from([0x02]), unsignedRlp);
|
||
const hash = keccak_256(preimage);
|
||
const sig = secp256k1.sign(hash, privKey, { prehash: false, lowS: true, format: "recovered" });
|
||
// noble returns [recid || r(32) || s(32)]; EIP-1559 uses yParity as
|
||
// 0 or 1 (recid directly, no +27 shift).
|
||
const yParity = sig[0];
|
||
const r = sig.subarray(1, 33);
|
||
const s = sig.subarray(33, 65);
|
||
const signedFields = [...unsignedFields, yParity, stripLeadingZeros(r), stripLeadingZeros(s)];
|
||
const signedRlp = rlpEncodeList(signedFields);
|
||
return "0x" + toHex(concat(Uint8Array.from([0x02]), signedRlp));
|
||
}
|
||
function stripLeadingZeros(bytes) {
|
||
let i = 0;
|
||
while (i < bytes.length - 1 && bytes[i] === 0) i++;
|
||
return bytes.subarray(i);
|
||
}
|
||
|
||
// ---- JSON-RPC client -------------------------------------------------
|
||
function makeClient(rpcUrl) {
|
||
let seq = 1;
|
||
async function call(method, params = []) {
|
||
const r = await fetch(rpcUrl, {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({ jsonrpc: "2.0", id: seq++, method, params }),
|
||
});
|
||
if (!r.ok) throw new Error(`${method}: HTTP ${r.status}`);
|
||
const j = await r.json();
|
||
if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`);
|
||
return j.result;
|
||
}
|
||
return { url: rpcUrl, call };
|
||
}
|
||
|
||
// Accept wei as bigint, integer string, hex string or number.
|
||
function toBig(v) {
|
||
if (typeof v === "bigint") return v;
|
||
const s = String(v ?? "0").trim();
|
||
if (/^0x[0-9a-f]+$/i.test(s)) return BigInt(s);
|
||
if (/^-?\d+$/.test(s)) return BigInt(s);
|
||
return BigInt(Math.round(Number(s) || 0));
|
||
}
|
||
// Calldata as "0x" + even-length lowercase hex; "" / null / "0x" → "0x".
|
||
function normalizeData(data) {
|
||
const s = String(data ?? "").trim().toLowerCase();
|
||
if (!s || s === "0x") return "0x";
|
||
const h = s.startsWith("0x") ? s.slice(2) : s;
|
||
if (!/^[0-9a-f]*$/.test(h) || h.length % 2) throw new Error("tx.data must be even-length hex");
|
||
return "0x" + h;
|
||
}
|
||
|
||
// ---- calldata decode (overlay only) -----------------------------------
|
||
// The handful of selectors behind almost every phishing loss. Anything
|
||
// else is shown as "<selector> + N bytes" so the user at least sees that
|
||
// it is a contract call they cannot read.
|
||
const SELECTORS = {
|
||
a9059cbb: { name: "transfer", args: ["to", "amount"] },
|
||
"095ea7b3": { name: "approve", args: ["spender", "amount"] },
|
||
"23b872dd": { name: "transferFrom", args: ["from", "to", "amount"] },
|
||
a22cb465: { name: "setApprovalForAll", args: ["operator", "approved"] },
|
||
"39509351": { name: "increaseAllowance", args: ["spender", "amount"] },
|
||
d505accf: { name: "permit", args: ["owner", "spender", "value", "deadline"] },
|
||
};
|
||
const UINT256_MAX = (1n << 256n) - 1n;
|
||
function decodeCalldata(dataHex) {
|
||
const h = normalizeData(dataHex).slice(2);
|
||
if (h.length < 8) return null;
|
||
const selector = h.slice(0, 8);
|
||
const spec = SELECTORS[selector];
|
||
const words = [];
|
||
for (let i = 8; i + 64 <= h.length; i += 64) words.push(h.slice(i, i + 64));
|
||
const out = { selector, name: spec ? spec.name : null, bytes: h.length / 2 };
|
||
if (!spec || words.length < spec.args.length) return out;
|
||
spec.args.forEach((a, i) => {
|
||
const w = words[i];
|
||
if (a === "amount" || a === "value" || a === "deadline") out[a] = BigInt("0x" + w);
|
||
else if (a === "approved") out[a] = BigInt("0x" + w) !== 0n;
|
||
else out[a] = eip55(w.slice(24));
|
||
});
|
||
const amt = out.amount ?? out.value;
|
||
if (amt != null) out.unlimited = amt >= (1n << 255n) || amt === UINT256_MAX;
|
||
return out;
|
||
}
|
||
|
||
function scopedStorage(storage, keyPrefix) {
|
||
const k = (key) => keyPrefix + key;
|
||
return {
|
||
get: (key, fallback = null) => storage.get(k(key), fallback),
|
||
set: (key, value) => storage.set(k(key), value),
|
||
};
|
||
}
|
||
|
||
// ---- wallet ----------------------------------------------------------
|
||
class EthWallet {
|
||
constructor(root32, networkId, {
|
||
walletId, storage, log = () => {}, onChange = () => {}, rpcUrl,
|
||
customNetwork, // { id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker } for EIP-3085 chains
|
||
} = {}) {
|
||
if (!walletId) throw new Error("chain-eth: walletId required");
|
||
const net = customNetwork || NETWORKS[networkId];
|
||
if (!net) throw new Error(`chain-eth: unknown network ${networkId}`);
|
||
this.walletId = walletId;
|
||
this.chain = "eth";
|
||
this.network = net.id;
|
||
this._net = net;
|
||
this._ticker = net.ticker || "ETH";
|
||
this.log = log;
|
||
this.onChange = onChange;
|
||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||
const master = HDKey.fromMasterSeed(root32);
|
||
// BIP44 for Ethereum: m/44'/60'/0'/0/0 is the canonical first address.
|
||
const node = master.derive("m/44'/60'/0'/0/0");
|
||
this._priv = node.privateKey;
|
||
this._pubUncompressed = secp256k1.getPublicKey(this._priv, false);
|
||
this.address = addressFromPubkey(this._pubUncompressed);
|
||
this._root = new Uint8Array(root32);
|
||
this._client = makeClient(String(rpcUrl || "").trim() || net.defaultRpc);
|
||
this._state = {
|
||
balance: { confirmed: "0", unconfirmed: "0" },
|
||
history: [],
|
||
height: 0,
|
||
scanning: false,
|
||
error: null,
|
||
};
|
||
this._pollTimer = null;
|
||
}
|
||
|
||
setRpcUrl(url) {
|
||
const v = String(url || "").trim() || this._net.defaultRpc;
|
||
this._client = makeClient(v);
|
||
this._emit();
|
||
}
|
||
_emit() { try { this.onChange(); } catch {} }
|
||
|
||
// Wei is 10^18 native units; the panel formats via decimals=18. The
|
||
// ticker follows the chain's nativeCurrency (ETH on mainnet/Sepolia,
|
||
// MATIC on Polygon, etc.) so the send-approval overlay reads correctly.
|
||
snapshot() {
|
||
return {
|
||
chain: "eth", network: this._net.id, ticker: this._ticker, decimals: 18,
|
||
address: this.address, addressIndex: 0,
|
||
addressPath: "m/44'/60'/0'/0/0",
|
||
balance: this._state.balance,
|
||
height: this._state.height,
|
||
history: this._state.history,
|
||
scanning: this._state.scanning,
|
||
error: this._state.error,
|
||
server: this._client.url,
|
||
rpcUrl: this._client.url,
|
||
explorerTx: this._net.explorerTx,
|
||
explorerAddr: this._net.explorerAddr,
|
||
faucet: this._net.faucet,
|
||
chainId: this._net.chainId,
|
||
};
|
||
}
|
||
|
||
async refresh() {
|
||
if (this._state.scanning) return;
|
||
this._state.scanning = true; this._state.error = null; this._emit();
|
||
try {
|
||
const [bal, block] = await Promise.all([
|
||
this._client.call("eth_getBalance", [this.address, "latest"]),
|
||
this._client.call("eth_blockNumber", []),
|
||
]);
|
||
this._state.balance = { confirmed: hexToBig(bal).toString(), unconfirmed: "0" };
|
||
this._state.height = Number(hexToBig(block));
|
||
} catch (e) {
|
||
this._state.error = e?.message || String(e);
|
||
this.log("refresh failed:", this._state.error);
|
||
} finally {
|
||
this._state.scanning = false;
|
||
this._emit();
|
||
}
|
||
}
|
||
schedulePoll(ms = 20_000) {
|
||
clearTimeout(this._pollTimer);
|
||
// dispose() during an in-flight refresh must not re-arm the poll.
|
||
if (this._disposed) return;
|
||
this._pollTimer = setTimeout(() => this.refresh().finally(() => this.schedulePoll(ms)), ms);
|
||
}
|
||
|
||
// Draft a transaction. The panel passes {to, amount, sendMax}; the dapp
|
||
// bridge (eth_sendTransaction) additionally passes data, gasLimit, fee
|
||
// caps and nonce exactly as the dapp supplied them. Every field the dapp
|
||
// set is honoured — the overlay then shows what will really be signed.
|
||
// Zero value is fine when there is calldata (ERC-20 transfer/approve).
|
||
async plan({ to, amount, sendMax, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas, gasPrice, nonce }) {
|
||
const dest = decodeAddress(to);
|
||
const dataHex = normalizeData(data);
|
||
const dataBytes = fromHex(dataHex.slice(2));
|
||
const from = this.address.toLowerCase();
|
||
const [nonceHex, priorityHex, gasPriceHex] = await Promise.all([
|
||
nonce != null ? Promise.resolve(bigToHex(toBig(nonce))) : this._client.call("eth_getTransactionCount", [from, "pending"]),
|
||
this._client.call("eth_maxPriorityFeePerGas", []).catch(() => "0x59682f00"), // fallback: 1.5 gwei
|
||
this._client.call("eth_gasPrice", []),
|
||
]);
|
||
const nonceN = Number(hexToBig(nonceHex));
|
||
const tip = maxPriorityFeePerGas != null ? toBig(maxPriorityFeePerGas) : hexToBig(priorityHex);
|
||
// eth_gasPrice on a 1559 chain already includes a tip, so it is the
|
||
// best single-number estimate of what a block will actually charge.
|
||
const gasPriceNow = hexToBig(gasPriceHex);
|
||
// Cap: dapp-supplied maxFeePerGas (or legacy gasPrice) wins; otherwise
|
||
// 2 × current price + tip so the tx survives a base-fee spike.
|
||
const maxFee = maxFeePerGas != null ? toBig(maxFeePerGas)
|
||
: (gasPrice != null ? toBig(gasPrice) : gasPriceNow * 2n + tip);
|
||
const bal = BigInt(this._state.balance.confirmed || "0");
|
||
let value = sendMax ? 0n : toBig(amount);
|
||
if (value < 0n) throw new Error("amount must be >= 0 wei");
|
||
if (!sendMax && value === 0n && dataBytes.length === 0) throw new Error("amount must be > 0 wei");
|
||
// Gas: dapp value if given; 21000 for a plain transfer; otherwise ask
|
||
// the node. A revert here surfaces as a clear error BEFORE the overlay,
|
||
// which doubles as a cheap "would this even succeed" simulation.
|
||
let gas;
|
||
if (gasLimit != null) gas = toBig(gasLimit);
|
||
else if (dataBytes.length === 0) gas = 21000n;
|
||
else {
|
||
let est;
|
||
try { est = await this._client.call("eth_estimateGas", [{ from, to: dest, value: bigToHex(value), data: dataHex }]); }
|
||
catch (e) { throw new Error("transaction would fail (eth_estimateGas): " + (e?.message || e)); }
|
||
gas = (hexToBig(est) * 12n) / 10n; // 20% headroom, as MetaMask does
|
||
}
|
||
if (gas < 21000n) throw new Error("gas limit below 21000");
|
||
const feeMax = gas * maxFee;
|
||
const feeEstimate = gas * (gasPriceNow < maxFee ? gasPriceNow : maxFee);
|
||
if (sendMax) {
|
||
if (bal <= feeMax) throw new Error("balance does not cover the gas fee");
|
||
value = bal - feeMax;
|
||
} else if (value + feeMax > bal) {
|
||
throw new Error("insufficient funds");
|
||
}
|
||
return {
|
||
_draft: {
|
||
chainId: this._net.chainId, nonce: nonceN, maxPriorityFeePerGas: tip, maxFeePerGas: maxFee,
|
||
gasLimit: gas, to: dest, value, data: dataHex, accessList: [],
|
||
},
|
||
recipients: [{ to: dest, value: value.toString() }],
|
||
fee: feeMax.toString(), // worst case — what the balance check uses
|
||
feeEstimate: feeEstimate.toString(), // what a block will most likely charge
|
||
feeRate: maxFee.toString(),
|
||
gasLimit: gas.toString(),
|
||
data: dataHex,
|
||
inputs: [],
|
||
change: "0",
|
||
total: (value + feeMax).toString(),
|
||
};
|
||
}
|
||
|
||
async signAndBroadcast(plan) {
|
||
const d = plan && plan._draft;
|
||
if (!d) throw new Error("bad plan");
|
||
const unsignedFields = [
|
||
d.chainId, d.nonce, d.maxPriorityFeePerGas, d.maxFeePerGas, d.gasLimit,
|
||
fromHex(d.to.slice(2)), d.value, fromHex(normalizeData(d.data).slice(2)), [],
|
||
];
|
||
const rawTxHex = signTxEip1559(unsignedFields, this._priv);
|
||
const txid = await this._client.call("eth_sendRawTransaction", [rawTxHex]);
|
||
if (typeof txid !== "string" || !/^0x[0-9a-f]{64}$/i.test(txid)) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
||
this.log("broadcast", txid);
|
||
setTimeout(() => this.refresh(), 3000);
|
||
return { txid };
|
||
}
|
||
|
||
// EIP-712: sign a pre-computed typed-data digest with r||s||v (v = 27+recid).
|
||
signTypedDataDigest(digest32) {
|
||
const sig = secp256k1.sign(digest32, this._priv, { prehash: false, lowS: true, format: "recovered" });
|
||
const out = new Uint8Array(65);
|
||
out.set(sig.subarray(1), 0);
|
||
out[64] = sig[0] + 27;
|
||
return { address: this.address, signature: "0x" + toHex(out) };
|
||
}
|
||
// Ethereum personal_sign: keccak256("\x19Ethereum Signed Message:\n" + len + msg).
|
||
// `message` is the exact bytes to sign (Uint8Array) or a plain string;
|
||
// the caller (index.js) is responsible for hex-decoding what dapps send.
|
||
signMessage(message) {
|
||
const enc = new TextEncoder();
|
||
const body = message instanceof Uint8Array ? message : enc.encode(String(message));
|
||
const prefix = enc.encode("\x19Ethereum Signed Message:\n" + body.length);
|
||
const buf = concat(prefix, body);
|
||
const hash = keccak_256(buf);
|
||
const sig = secp256k1.sign(hash, this._priv, { prehash: false, lowS: true, format: "recovered" });
|
||
// personal_sign format: r || s || v where v = 27 + recid.
|
||
const out = new Uint8Array(65);
|
||
out.set(sig.subarray(1), 0);
|
||
out[64] = sig[0] + 27;
|
||
return { address: this.address, signature: "0x" + toHex(out) };
|
||
}
|
||
|
||
recovery() {
|
||
// Ethereum wallets typically expose the raw private key hex; we do too,
|
||
// but only when the caller re-confirms in the approval overlay upstream.
|
||
return {
|
||
accountPath: "m/44'/60'/0'/0/0",
|
||
xpub: "0x" + toHex(this._pubUncompressed),
|
||
xprv: "0x" + toHex(this._priv),
|
||
};
|
||
}
|
||
|
||
dispose() {
|
||
this._disposed = true;
|
||
clearTimeout(this._pollTimer);
|
||
try { this._priv && this._priv.fill(0); } catch {}
|
||
try { this._root && this._root.fill(0); } catch {}
|
||
}
|
||
}
|
||
|
||
return { EthWallet, NETWORKS, addressFromPubkey, decodeAddress, eip55, decodeCalldata, normalizeData, toBig };
|
||
};
|