// Chain queries for the registration flow: address -> UTXOs, broadcast, lookup. // libauth only (no node builtins) so it runs in the browser and in Node. import { cashAddressToLockingBytecode, lockingBytecodeToCashAddress, sha256, hash160, binToHex, hexToBin, } from "@bitauth/libauth"; // Electrum indexes by "scripthash": sha256(lockingBytecode), byte-reversed, hex. export function lockingBytecodeToScripthash(lockingBytecode) { return binToHex(sha256.hash(lockingBytecode).slice().reverse()); } export function addressToLockingBytecode(address) { const r = cashAddressToLockingBytecode(address); if (typeof r === "string") throw new Error(`bad address "${address}": ${r}`); return r.bytecode; } export function addressToScripthash(address) { return lockingBytecodeToScripthash(addressToLockingBytecode(address)); } // Re-encode any cashaddr as its token-aware ("token address") form, which is // what a certificate output must be sent to. Same locking bytecode either way; // the flag only tells wallets the address is safe to receive CashTokens on. export function toTokenAddress(address) { const bytecode = addressToLockingBytecode(address); const prefix = String(address).includes(":") ? address.split(":")[0] : "bchtest"; const r = lockingBytecodeToCashAddress({ bytecode, prefix, tokenSupport: true }); if (typeof r === "string") throw new Error(`cannot token-encode "${address}": ${r}`); return r.address; } export function stripTokenAddress(address) { const bytecode = addressToLockingBytecode(address); const prefix = String(address).includes(":") ? address.split(":")[0] : "bchtest"; const r = lockingBytecodeToCashAddress({ bytecode, prefix, tokenSupport: false }); if (typeof r === "string") throw new Error(`cannot encode "${address}": ${r}`); return r.address; } // Fetch spendable outputs for one address. // Returns [{ txid, vout, satoshis: bigint, address, lockingBytecode, token? }] // `token` is present (and the UTXO is NOT safe to spend as plain change) when // the output carries CashTokens. export async function getUtxos(electrum, address) { const lockingBytecode = addressToLockingBytecode(address); const scripthash = lockingBytecodeToScripthash(lockingBytecode); const list = await electrum.call("blockchain.scripthash.listunspent", [scripthash, "include_tokens"]); return list.map((u) => ({ txid: u.tx_hash, vout: u.tx_pos, satoshis: BigInt(u.value), height: u.height, address, lockingBytecode, token: u.token_data ? { category: u.token_data.category, amount: BigInt(u.token_data.amount ?? 0), nft: u.token_data.nft ? { capability: u.token_data.nft.capability, commitment: u.token_data.nft.commitment } : undefined, } : undefined, })); } export async function getUtxosForAddresses(electrum, addresses) { const out = []; for (const a of addresses) out.push(...(await getUtxos(electrum, a))); return out; } export async function broadcast(electrum, txHex) { return electrum.call("blockchain.transaction.broadcast", [txHex]); } export async function getTransaction(electrum, txid, verbose = true) { return electrum.call("blockchain.transaction.get", [txid, verbose]); } /** * Who paid us — recovered from the transaction itself. * * A refund needs a destination, and the only address a payer has *provably* * controlled is the one they spent from: to create the input they signed with * its key. So rather than asking (and being told a typo, or nothing at all), * read the compressed public key straight out of the P2PKH unlocking script and * derive the address from it. * * A standard P2PKH unlocking script is exactly two pushes: * * so the last push is the public key. Anything else — multisig, a covenant, a * non-standard script — returns null rather than a guess, because refunding to a * wrong address is worse than not refunding automatically. */ export function payerAddressFromInput(scriptSigHex, prefix = "bchtest") { if (!scriptSigHex) return null; const script = hexToBin(scriptSigHex); const pushes = []; let i = 0; while (i < script.length) { const op = script[i++]; let len; if (op >= 0x01 && op <= 0x4b) len = op; else if (op === 0x4c) len = script[i++]; else if (op === 0x4d) { len = script[i] | (script[i + 1] << 8); i += 2; } else return null; // opcodes other than pushes -> not a plain P2PKH spend if (i + len > script.length) return null; pushes.push(script.slice(i, i + len)); i += len; } if (pushes.length !== 2) return null; const pubkey = pushes[1]; if (pubkey.length !== 33 && pubkey.length !== 65) return null; const r = lockingBytecodeToCashAddress({ bytecode: Uint8Array.from([0x76, 0xa9, 0x14, ...hash160(pubkey), 0x88, 0xac]), prefix, tokenSupport: false, }); return typeof r === "string" ? null : r.address; } /** The paying address of a transaction, read from its first spendable input. */ export async function payerAddressOf(electrum, txid, prefix = "bchtest") { const tx = await getTransaction(electrum, txid, true); for (const vin of tx?.vin ?? []) { const addr = payerAddressFromInput(vin?.scriptSig?.hex, prefix); if (addr) return addr; } return null; } export { binToHex, hexToBin };