Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user