// Ethereum signing with no dependencies, for the faucet function (site/api/faucet.mjs). The site project installs // nothing at build time (its install command is `echo skip`), so viem is not an option there; this is the minimum: // keccak-256, RLP, secp256k1 ECDSA with RFC 6979 nonces, the EIP-1559 envelope and the address of a key. // Checked against the live devnet node (eth_sendRawTransaction accepts the transaction and the receipt's `from` is // this key's address) and against known keccak vectors in site/api/faucet.test.mjs. import { createHmac } from 'node:crypto'; // ---- keccak-256 (the original Keccak padding 0x01..0x80, not SHA-3's 0x06) ------------------------------------------ const RC = [ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an, 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an, 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n, ]; // rotation offsets r[x + 5y] const ROT = [0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14]; const M64 = (1n << 64n) - 1n; const rotl = (v, n) => n === 0 ? v : (((v << BigInt(n)) | (v >> BigInt(64 - n))) & M64); function keccakF(A) { const C = new Array(5), D = new Array(5), B = new Array(25); for (let round = 0; round < 24; round++) { for (let x = 0; x < 5; x++) C[x] = A[x] ^ A[x + 5] ^ A[x + 10] ^ A[x + 15] ^ A[x + 20]; for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rotl(C[(x + 1) % 5], 1); for (let i = 0; i < 25; i++) A[i] ^= D[i % 5]; for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(A[x + 5 * y], ROT[x + 5 * y]); for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) A[x + 5 * y] = B[x + 5 * y] ^ ((~B[(x + 1) % 5 + 5 * y] & M64) & B[(x + 2) % 5 + 5 * y]); A[0] ^= RC[round]; } } export function keccak256(bytes) { const rate = 136; const padded = new Uint8Array(Math.ceil((bytes.length + 1) / rate) * rate); padded.set(bytes); padded[bytes.length] ^= 0x01; padded[padded.length - 1] ^= 0x80; const A = new Array(25).fill(0n); for (let off = 0; off < padded.length; off += rate) { for (let i = 0; i < rate / 8; i++) { let lane = 0n; for (let b = 7; b >= 0; b--) lane = (lane << 8n) | BigInt(padded[off + i * 8 + b]); A[i] ^= lane; } keccakF(A); } const out = new Uint8Array(32); for (let i = 0; i < 4; i++) { let lane = A[i]; for (let b = 0; b < 8; b++) { out[i * 8 + b] = Number(lane & 0xffn); lane >>= 8n; } } return out; } // ---- bytes and hex -------------------------------------------------------------------------------------------------- export const hex = bytes => '0x' + Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); export function unhex(s) { const h = String(s).replace(/^0x/i, ''); if (h.length % 2 || /[^0-9a-f]/i.test(h)) throw new Error(`bad hex ${s}`); const out = new Uint8Array(h.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return out; } const bigToBytes = (v, len) => { const h = v.toString(16).padStart(len * 2, '0'); return unhex(h); }; const bytesToBig = b => { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; }; const concat = (...parts) => { const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; }; const utf8 = s => new TextEncoder().encode(s); // ---- RLP ----------------------------------------------------------------------------------------------------------- // an item is a Uint8Array, a bigint or number (minimal big-endian, 0 = empty), a hex string, or an array of items function toBytes(item) { if (item instanceof Uint8Array) return item; if (typeof item === 'bigint' || typeof item === 'number') { const v = BigInt(item); if (v < 0n) throw new Error('negative'); if (v === 0n) return new Uint8Array(0); let h = v.toString(16); if (h.length % 2) h = '0' + h; return unhex(h); } if (typeof item === 'string') return unhex(item); throw new Error('rlp: unsupported item'); } function rlpLength(len, offset) { if (len < 56) return new Uint8Array([offset + len]); const l = toBytes(len); return concat(new Uint8Array([offset + 55 + l.length]), l); } export function rlp(item) { if (Array.isArray(item)) { const body = concat(...item.map(rlp)); return concat(rlpLength(body.length, 0xc0), body); } const b = toBytes(item); if (b.length === 1 && b[0] < 0x80) return b; return concat(rlpLength(b.length, 0x80), b); } // ---- secp256k1 ------------------------------------------------------------------------------------------------------ const P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn; export const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; const G = [0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n]; const mod = (a, m) => { const r = a % m; return r < 0n ? r + m : r; }; function inv(a, m) { // extended Euclid let lm = 1n, hm = 0n, low = mod(a, m), high = m; if (low === 0n) throw new Error('no inverse of 0'); while (low > 1n) { const r = high / low; [lm, hm] = [hm - lm * r, lm]; [low, high] = [high - low * r, low]; } return mod(lm, m); } function pointAdd(a, b) { if (!a) return b; if (!b) return a; const [x1, y1] = a, [x2, y2] = b; if (x1 === x2) { if (mod(y1 + y2, P) === 0n) return null; return pointDouble(a); } const l = mod((y2 - y1) * inv(x2 - x1, P), P); const x3 = mod(l * l - x1 - x2, P); return [x3, mod(l * (x1 - x3) - y1, P)]; } function pointDouble(a) { const [x, y] = a; const l = mod(3n * x * x * inv(2n * y, P), P); const x3 = mod(l * l - 2n * x, P); return [x3, mod(l * (x - x3) - y, P)]; } function pointMul(k, point = G) { let r = null, q = point; for (let e = mod(k, N); e > 0n; e >>= 1n) { if (e & 1n) r = pointAdd(r, q); q = pointDouble(q); } return r; } export function publicKey(priv) { // uncompressed, 64 bytes (x || y), no 0x04 prefix const d = bytesToBig(unhex(priv)); if (d <= 0n || d >= N) throw new Error('private key out of range'); const [x, y] = pointMul(d); return concat(bigToBytes(x, 32), bigToBytes(y, 32)); } export function addressOf(priv) { return hex(keccak256(publicKey(priv)).slice(12)); } // RFC 6979 nonce for secp256k1 with HMAC-SHA256 (deterministic: the same key and hash give the same signature) function rfc6979(privBytes, hash) { const hmac = (k, ...msgs) => { const h = createHmac('sha256', k); for (const m of msgs) h.update(m); return new Uint8Array(h.digest()); }; let v = new Uint8Array(32).fill(1), k = new Uint8Array(32); const x = privBytes, h1 = bigToBytes(mod(bytesToBig(hash), N), 32); k = hmac(k, v, new Uint8Array([0]), x, h1); v = hmac(k, v); k = hmac(k, v, new Uint8Array([1]), x, h1); v = hmac(k, v); for (;;) { v = hmac(k, v); const t = bytesToBig(v); if (t >= 1n && t < N) return t; k = hmac(k, v, new Uint8Array([0])); v = hmac(k, v); } } // returns {r, s, v} with low s and the recovery id v (0 or 1) export function sign(hash, priv) { const privBytes = unhex(priv); const d = bytesToBig(privBytes); if (d <= 0n || d >= N) throw new Error('private key out of range'); const z = mod(bytesToBig(hash), N); for (let attempt = 0; attempt < 8; attempt++) { const k = rfc6979(privBytes, attempt ? keccak256(concat(hash, new Uint8Array([attempt]))) : hash); const R = pointMul(k); const r = mod(R[0], N); if (r === 0n) continue; let s = mod(inv(k, N) * (z + r * d), N); if (s === 0n) continue; let v = Number(R[1] & 1n); if (s > N / 2n) { s = N - s; v ^= 1; } return { r, s, v }; } throw new Error('no signature'); } // ---- the EIP-1559 transaction ------------------------------------------------------------------------------------------ // fields: chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to (hex), value, data (hex, default 0x) export function signTransaction(tx, priv) { const fields = [BigInt(tx.chainId), BigInt(tx.nonce), BigInt(tx.maxPriorityFeePerGas), BigInt(tx.maxFeePerGas), BigInt(tx.gas), unhex(tx.to), BigInt(tx.value), unhex(tx.data || '0x'), []]; const signingHash = keccak256(concat(new Uint8Array([2]), rlp(fields))); const { r, s, v } = sign(signingHash, priv); const raw = concat(new Uint8Array([2]), rlp([...fields, BigInt(v), r, s])); return { raw: hex(raw), hash: hex(keccak256(raw)), v, r, s }; } export const toWei = ign => BigInt(Math.round(Number(ign) * 1e6)) * 10n ** 12n; // whole-number IGN or up to 6 decimals export const isAddress = s => typeof s === 'string' && /^0x[0-9a-fA-F]{40}$/.test(s); export { utf8, bytesToBig, bigToBytes };