argus/src/lib/chain.js
Local Dev a45320db7d Initial commit — Silent Mode baseline (2026-07-29)
Snapshot of the decentralized-web stack at the point of the resolver+Theseus
rebuild deploy. Includes:

- Argus (BNS engine + resolver daemon + Sia gateway)
- AriadneResolver (Windows Inno installer bundle + Android APK sources +
  Firefox extension)
- TheseusNavigator (Electron browser)
- site/ (silentmode.st content, deployed to Sia at bns/silentmode/)
- design docs, roadmap, protocol spec

Secrets excluded via .gitignore: Argus/sia-s3.json, Argus/wallets.json,
Argus/ca/*.key,*.crt. Build outputs, node_modules, and bundled runtimes
also excluded.

Shipped hashes on dl.silentmode.st at this commit:
  AriadneResolver-Setup-0.1.0.exe    5bcb216eef31ea28ed767e4134ab74bd5ac69dfbd365fd249e9e6938e55c986a
  TheseusNavigator-Setup-0.0.1.exe   7c735e88bad2da3347145adba3016c8f626a18b8422289c8c6ba471972e2952b
  TheseusNavigator-0.0.1-portable.exe 008fd84445babeabb401b2bca40ea9466b24b0e6d6c85104da7640c5c5c84521
  ariadne-v0.2.apk                   635c8f04d44ef855a8390b9eeddb8cd2d50622e81cc4e5004e8daffc1bb0425c
2026-07-29 13:54:34 +02:00

141 lines
5.3 KiB
JavaScript

// 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:
* <push sig (71-73 bytes DER, or 65 schnorr)> <push pubkey (33 or 65)>
* 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 };