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
141 lines
5.3 KiB
JavaScript
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 };
|