// Generated, do not edit. sha256:a16dd8b1a3974fb4acdf7f84e8ff322821ad367d6c5137af68317f24a7ee0d3d import { HDKey, base58check, bech32, bech32m, secp256k1, sha256, ripemd160, hexToBytes, concatBytes, utf8ToBytes } from '../vendor/crypto/noble-scure.js'; import { addressTxs, fetchJson, explorerBase } from './btc-explorer.js'; const b58 = base58check(sha256); const hash160 = bytes => ripemd160(sha256(bytes)); const XPUB_VERSION = hexToBytes('0488b21e'); const TAP_TWEAK_TAG = sha256(utf8ToBytes('TapTweak')); const bytesToBig = b => { let n = 0n; for (const x of b) n = (n << 8n) | BigInt(x); return n; }; const bigTo32 = n => { const o = new Uint8Array(32); for (let i = 31; i >= 0; i--) { o[i] = Number(n & 0xffn); n >>= 8n; } return o; }; function taprootOutputKey(pub33) { const xonly = pub33.slice(1); const t = bytesToBig(sha256(concatBytes(TAP_TWEAK_TAG, TAP_TWEAK_TAG, xonly))) % secp256k1.Point.Fn.ORDER; const P = secp256k1.Point.fromBytes(concatBytes(Uint8Array.of(0x02), xonly)); const Q = P.add(secp256k1.Point.BASE.multiply(t)); return bigTo32(Q.toAffine().x); } const SCAN = { GAP_LIMIT: 20, DELAY_MS: 250, }; const apiBase = async () => (await explorerBase()) + '/api'; const wacht = ms => new Promise(r => setTimeout(r, ms)); function typeUitPrefix(key) { if (key.startsWith('ypub')) return 'p2sh-p2wpkh'; if (key.startsWith('zpub') || key.startsWith('xpub')) return 'p2wpkh'; return 'p2pkh'; } function naarXpub(extKey) { const data = b58.decode(extKey); const out = new Uint8Array(data.length); out.set(XPUB_VERSION, 0); out.set(data.slice(4), 4); return b58.encode(out); } function adresVanPubkey(pubkey, type) { if (type === 'p2tr') { return bech32m.encode('bc', [1, ...bech32m.toWords(taprootOutputKey(pubkey))]); } const h = hash160(pubkey); if (type === 'p2wpkh') { return bech32.encode('bc', [0, ...bech32.toWords(h)]); } if (type === 'p2sh-p2wpkh') { const redeem = new Uint8Array(22); redeem[0] = 0x00; redeem[1] = 0x14; redeem.set(h, 2); const sh = hash160(redeem); const payload = new Uint8Array(21); payload[0] = 0x05; payload.set(sh, 1); return b58.encode(payload); } const payload = new Uint8Array(21); payload[0] = 0x00; payload.set(h, 1); return b58.encode(payload); } function maakDeriver(extKey, scriptType) { const type = scriptType || typeUitPrefix(extKey); const hd = HDKey.fromExtendedKey(naarXpub(extKey)); return (chain, index) => { const child = hd.deriveChild(chain).deriveChild(index); return adresVanPubkey(child.publicKey, type); }; } export function firstReceiveAddress(extKey, scriptType) { return maakDeriver(extKey, scriptType)(0, 0); } async function haalAdresInfoOp(adres) { const info = await fetchJson(`${await apiBase()}/address/${adres}`, `Adres ${adres}`); const bevestigd = info.chain_stats.funded_txo_sum - info.chain_stats.spent_txo_sum; const onbevestigd = info.mempool_stats.funded_txo_sum - info.mempool_stats.spent_txo_sum; const tx_aantal = info.chain_stats.tx_count + info.mempool_stats.tx_count; return { saldo_satoshi: bevestigd, saldo_onbevestigd: onbevestigd, tx_aantal }; } async function scanChain(deriver, chain, onVoortgang) { const resultaten = []; let legeReeks = 0, index = 0; const chainLabel = chain === 0 ? 'ontvangst' : 'change'; while (legeReeks < SCAN.GAP_LIMIT) { const adres = deriver(chain, index); onVoortgang?.({ fase: 'scan', chain: chainLabel, index, adres }); const info = await haalAdresInfoOp(adres); await wacht(SCAN.DELAY_MS); if (info.tx_aantal > 0) { resultaten.push({ adres, chain, chainLabel, index, saldo_satoshi: info.saldo_satoshi, tx_aantal: info.tx_aantal, heeftSaldo: info.saldo_satoshi > 0, }); legeReeks = 0; } else { legeReeks++; } index++; } return resultaten; } export async function scanMovements(addresses, accountId, sourceId, onVoortgang, { ownAddresses = [] } = {}) { const own = new Set([...addresses, ...ownAddresses]); const gezien = new Set(); const rijen = []; const failed = []; let i = 0; for (const adres of addresses) { onVoortgang?.({ fase: 'tx', index: i++, total: addresses.length, adres }); let txs; try { txs = await addressTxs(adres); } catch (e) { console.warn(`[scanMovements] adres mislukt: ${adres}`, e); failed.push(adres); onVoortgang?.({ fase: 'tx-error', adres, error: e.message }); continue; } await wacht(SCAN.DELAY_MS); for (const tx of txs) { if (gezien.has(tx.txid)) continue; gezien.add(tx.txid); const inSum = (tx.vin || []).reduce((s, v) => s + (v.prevout && own.has(v.prevout.scriptpubkey_address) ? v.prevout.value : 0), 0); const outSum = (tx.vout || []).reduce((s, o) => s + (own.has(o.scriptpubkey_address) ? o.value : 0), 0); const net = outSum - inSum; if (net === 0) continue; const direction = net > 0 ? 'in' : 'out'; const fee = tx.fee ?? 0; const value_sats = direction === 'out' ? Math.max(0, Math.abs(net) - fee) : Math.abs(net); const fee_sats = direction === 'out' ? fee : 0; const pool = direction === 'in' ? (tx.vout || []) : (tx.vin || []).map(v => v.prevout).filter(Boolean); let repAddr = null, repVal = -1; for (const o of pool) { if (own.has(o.scriptpubkey_address) && o.value > repVal) { repVal = o.value; repAddr = o.scriptpubkey_address; } } const conf = tx.status && tx.status.confirmed && tx.status.block_height && tx.status.block_time; rijen.push({ accountId, sourceId, txid: tx.txid, direction, valueSats: value_sats, feeSats: fee_sats, blockHeight: conf ? tx.status.block_height : null, blockTime: conf ? new Date(tx.status.block_time * 1000).toISOString() : null, address: repAddr, }); } } return { rows: rijen, failed }; } export async function scanWallet(xpub, scriptType, onVoortgang) { onVoortgang?.({ fase: 'start' }); const deriver = maakDeriver(xpub, scriptType); const [extern, intern] = await Promise.all([ scanChain(deriver, 0, onVoortgang), scanChain(deriver, 1, onVoortgang), ]); const alleAdressen = [...extern, ...intern]; const totaalSaldo_satoshi = alleAdressen.reduce((s, a) => s + a.saldo_satoshi, 0); onVoortgang?.({ fase: 'klaar' }); return { xpub, totaalSaldo_satoshi, alleAdressen }; }