Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
// 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 };
}