UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

504 lines 23.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MockServices = void 0; const sdk_1 = require("@bsv/sdk"); const WERR_errors_1 = require("../sdk/WERR_errors"); const utilityHelpers_1 = require("../utility/utilityHelpers"); const utilityHelpers_noBuffer_1 = require("../utility/utilityHelpers.noBuffer"); const Services_1 = require("../services/Services"); const MockChainStorage_1 = require("./MockChainStorage"); const MockChainTracker_1 = require("./MockChainTracker"); const MockMiner_1 = require("./MockMiner"); const merkleTree_1 = require("./merkleTree"); class MockServices { constructor(knex) { this.knex = knex; this.chain = 'mock'; this.storage = new MockChainStorage_1.MockChainStorage(knex); this.tracker = new MockChainTracker_1.MockChainTracker('mock', this.storage); this.miner = new MockMiner_1.MockMiner(); } async initialize() { await this.storage.migrate(); // Mine genesis block if chain is empty const tip = await this.storage.getChainTip(); if (!tip) { await this.miner.mineBlock(this.storage); } } async mineBlock() { return this.miner.mineBlock(this.storage); } async postBeef(beef, txids) { var _a; const results = []; for (const txid of txids) { try { // Find the transaction in the BEEF const beefTx = beef.findTxid(txid); if (!beefTx) { results.push({ name: 'MockServices', status: 'error', error: new WERR_errors_1.WERR_INVALID_PARAMETER('txid', `present in provided BEEF. txid: ${txid}`), txidResults: [{ txid, status: 'error' }] }); continue; } const rawTx = beefTx.rawTx; if (!rawTx) { results.push({ name: 'MockServices', status: 'error', error: new WERR_errors_1.WERR_INVALID_PARAMETER('rawTx', `present in BEEF for txid: ${txid}`), txidResults: [{ txid, status: 'error' }] }); continue; } const tx = sdk_1.Transaction.fromBinary(rawTx); // Validate inputs const currentHeight = await this.tracker.currentHeight(); for (let i = 0; i < tx.inputs.length; i++) { const input = tx.inputs[i]; const sourceTxid = input.sourceTXID || (input.sourceTransaction ? input.sourceTransaction.id('hex') : undefined); if (!sourceTxid) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('input.sourceTXID', `defined for input ${i}`); } const sourceVout = input.sourceOutputIndex; const utxo = await this.storage.getUtxo(sourceTxid, sourceVout); if (!utxo) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('input', `reference a known UTXO. Input ${i}: ${sourceTxid}.${sourceVout} not found`); } if (utxo.spentByTxid) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('input', `not be already spent. Input ${i}: ${sourceTxid}.${sourceVout} spent by ${utxo.spentByTxid}`); } // Coinbase maturity check if (utxo.isCoinbase && utxo.blockHeight !== null) { if (currentHeight - utxo.blockHeight < 100) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('input', `not spend immature coinbase. Input ${i}: coinbase at height ${utxo.blockHeight}, current height ${currentHeight}, need 100 confirmations`); } } // Ensure source transaction is set for script validation if (!input.sourceTransaction) { const sourceTxRow = await this.storage.getTransaction(sourceTxid); if (sourceTxRow) { const sourceRaw = sourceTxRow.rawTx instanceof Buffer ? Array.from(sourceTxRow.rawTx) : Array.isArray(sourceTxRow.rawTx) ? sourceTxRow.rawTx : Array.from(sourceTxRow.rawTx); input.sourceTransaction = sdk_1.Transaction.fromBinary(sourceRaw); } } } // Validate scripts using the SDK script interpreter // We set sourceTransaction on each input above, so verify should work. // Also set merklePath on source transactions to satisfy the SDK's proof requirement. for (const input of tx.inputs) { if (input.sourceTransaction && !input.sourceTransaction.merklePath) { const stxid = input.sourceTransaction.id('hex'); const stx = await this.storage.getTransaction(stxid); if (stx && stx.blockHeight !== null) { const txsInBlock = await this.storage.getTransactionsInBlock(stx.blockHeight); const stxids = txsInBlock.map(t => t.txid); const idx = stxids.indexOf(stxid); if (idx >= 0) { input.sourceTransaction.merklePath = (0, merkleTree_1.computeMerklePath)(stxids, idx, stx.blockHeight); } } } } const verified = await tx.verify('scripts only'); if (verified !== true) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('transaction', `pass script validation: ${verified}`); } // Store transaction await this.storage.insertTransaction(txid, Array.from(rawTx)); // Create UTXOs for each output for (let vout = 0; vout < tx.outputs.length; vout++) { const output = tx.outputs[vout]; const scriptBinary = output.lockingScript.toBinary(); const scriptHash = (0, utilityHelpers_noBuffer_1.asString)((0, utilityHelpers_1.sha256Hash)(Array.from(scriptBinary))); await this.storage.insertUtxo(txid, vout, Array.from(scriptBinary), (_a = output.satoshis) !== null && _a !== void 0 ? _a : 0, scriptHash); } // Spend inputs for (const input of tx.inputs) { const sourceTxid = input.sourceTXID || (input.sourceTransaction ? input.sourceTransaction.id('hex') : ''); await this.storage.markUtxoSpent(sourceTxid, input.sourceOutputIndex, txid); } results.push({ name: 'MockServices', status: 'success', txidResults: [{ txid, status: 'success' }] }); } catch (eu) { const error = eu instanceof Error ? new WERR_errors_1.WERR_INTERNAL(eu.message) : new WERR_errors_1.WERR_INTERNAL(String(eu)); results.push({ name: 'MockServices', status: 'error', error, txidResults: [{ txid, status: 'error' }] }); } } return results; } async reorg(startingHeight, numBlocks, txidMap) { const oldTip = await this.storage.getChainTip(); if (!oldTip) throw new WERR_errors_1.WERR_INTERNAL('Cannot reorg empty chain'); if (startingHeight > oldTip.height) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('startingHeight', `<= current tip height ${oldTip.height}`); } const deactivatedHeaders = []; // Collect all deactivated headers for (let h = startingHeight; h <= oldTip.height; h++) { const header = await this.storage.getBlockHeaderByHeight(h); if (header) deactivatedHeaders.push(header); } await this.knex.transaction(async (trx) => { const trxStorage = new MockChainStorage_1.MockChainStorage(trx); // Tear down old blocks for (let h = oldTip.height; h >= startingHeight; h--) { const txsInBlock = await trxStorage.getTransactionsInBlock(h); // Get coinbaseTxid from the raw row (not BlockHeader which lacks it) const headerRow = await trxStorage.knex('mockchain_block_headers').where({ height: h }).first(); const coinbaseTxid = headerRow === null || headerRow === void 0 ? void 0 : headerRow.coinbaseTxid; for (const tx of txsInBlock) { if (coinbaseTxid && tx.txid === coinbaseTxid) { // Delete coinbase UTXOs and transaction await trxStorage.deleteUtxosByTxid(tx.txid); await trxStorage.deleteTransaction(tx.txid); } else { // Return non-coinbase tx to mempool await trxStorage.setTransactionBlock(tx.txid, null, null); await trxStorage.setUtxoBlockHeight(tx.txid, null); } } if (headerRow) { await trxStorage.deleteBlockHeader(h); } } // Mine numBlocks new blocks for (let i = 0; i < numBlocks; i++) { const newHeight = startingHeight + i; // Determine which txids go in this block from txidMap const mappedTxids = []; if (txidMap) { for (const [tid, offset] of Object.entries(txidMap)) { if (offset === i) mappedTxids.push(tid); } } // Get previous hash let prevHash; if (newHeight === 0) { prevHash = '00'.repeat(32); } else { const prevHeader = await trxStorage.getBlockHeaderByHeight(newHeight - 1); if (!prevHeader) throw new WERR_errors_1.WERR_INTERNAL(`Missing block header at height ${newHeight - 1}`); prevHash = prevHeader.hash; } // Create coinbase const { createCoinbaseTransaction } = await Promise.resolve().then(() => __importStar(require('./MockMiner'))); const coinbaseTx = createCoinbaseTransaction(newHeight); const coinbaseTxid = coinbaseTx.id('hex'); const coinbaseRawTx = Array.from(coinbaseTx.toBinary()); const allTxids = [coinbaseTxid, ...mappedTxids]; const { computeMerkleRoot } = await Promise.resolve().then(() => __importStar(require('./merkleTree'))); const merkleRoot = computeMerkleRoot(allTxids); const time = Math.floor(Date.now() / 1000); const bits = 0x207fffff; const nonce = Math.floor(Math.random() * 0xffffffff); const headerObj = { version: 1, previousHash: prevHash, merkleRoot, time, bits, nonce }; const headerBinary = (0, Services_1.toBinaryBaseBlockHeader)(headerObj); const hash = (0, utilityHelpers_noBuffer_1.asString)((0, utilityHelpers_1.doubleSha256BE)(headerBinary)); // Insert coinbase tx await trxStorage.knex('mockchain_transactions').insert({ txid: coinbaseTxid, rawTx: Buffer.from(coinbaseRawTx), blockHeight: newHeight, blockIndex: 0 }); // Insert coinbase UTXO const coinbaseOutputScript = [0x51]; const coinbaseScriptHash = (0, utilityHelpers_noBuffer_1.asString)((0, utilityHelpers_1.sha256Hash)(coinbaseOutputScript)); await trxStorage.knex('mockchain_utxos').insert({ txid: coinbaseTxid, vout: 0, lockingScript: Buffer.from(coinbaseOutputScript), satoshis: 5000000000, scriptHash: coinbaseScriptHash, spentByTxid: null, isCoinbase: true, blockHeight: newHeight }); // Update mapped txs for (let j = 0; j < mappedTxids.length; j++) { await trxStorage.setTransactionBlock(mappedTxids[j], newHeight, j + 1); await trxStorage.setUtxoBlockHeight(mappedTxids[j], newHeight); } // Insert block header await trxStorage.knex('mockchain_block_headers').insert({ height: newHeight, hash, previousHash: prevHash, merkleRoot, version: 1, time, bits, nonce, coinbaseTxid }); } }); const newTip = await this.storage.getChainTip(); if (!newTip) throw new WERR_errors_1.WERR_INTERNAL('Chain tip missing after reorg'); return { oldTip, newTip, deactivatedHeaders }; } async getRawTx(txid) { const tx = await this.storage.getTransaction(txid); if (!tx) return { txid }; const rawTx = tx.rawTx instanceof Buffer ? Array.from(tx.rawTx) : Array.isArray(tx.rawTx) ? tx.rawTx : Array.from(tx.rawTx); return { txid, rawTx, name: 'MockServices' }; } async getMerklePath(txid) { const tx = await this.storage.getTransaction(txid); if (!tx || tx.blockHeight === null) return {}; const txsInBlock = await this.storage.getTransactionsInBlock(tx.blockHeight); const txids = txsInBlock.map(t => t.txid); const targetIndex = txids.indexOf(txid); if (targetIndex < 0) return {}; const header = await this.storage.getBlockHeaderByHeight(tx.blockHeight); const merklePath = (0, merkleTree_1.computeMerklePath)(txids, targetIndex, tx.blockHeight); return { merklePath, header: header || undefined, name: 'MockServices' }; } async getUtxoStatus(output, outputFormat, outpoint) { const hashBE = (0, Services_1.validateScriptHash)(output, outputFormat); // Convert hashBE to hashLE for our storage (which stores hashLE) const hashLE = (0, utilityHelpers_noBuffer_1.asString)((0, utilityHelpers_noBuffer_1.asArray)(hashBE).reverse()); const utxos = await this.storage.getUtxosByScriptHash(hashLE); const unspent = utxos.filter(u => !u.spentByTxid); let isUtxo = unspent.length > 0; const details = unspent.map(u => { var _a; return ({ txid: u.txid, index: u.vout, height: (_a = u.blockHeight) !== null && _a !== void 0 ? _a : undefined, satoshis: Number(u.satoshis) }); }); // If outpoint is provided, filter to match if (outpoint && isUtxo) { const [opTxid, opVoutStr] = outpoint.split('.'); const opVout = parseInt(opVoutStr, 10); const match = details.find(d => d.txid === opTxid && d.index === opVout); isUtxo = !!match; } return { name: 'MockServices', status: 'success', isUtxo, details }; } async getStatusForTxids(txids) { const currentHeight = await this.tracker.currentHeight(); const results = await Promise.all(txids.map(async (txid) => { const tx = await this.storage.getTransaction(txid); if (!tx) return { txid, status: 'unknown', depth: undefined }; if (tx.blockHeight !== null) { const depth = currentHeight - tx.blockHeight + 1; return { txid, status: 'mined', depth }; } return { txid, status: 'known', depth: 0 }; })); return { name: 'MockServices', status: 'success', results }; } async getScriptHashHistory(hash) { const utxos = await this.storage.getUtxosByScriptHash(hash); const history = utxos.map(u => { var _a; return ({ txid: u.txid, height: (_a = u.blockHeight) !== null && _a !== void 0 ? _a : undefined }); }); return { name: 'MockServices', status: 'success', history }; } async getChainTracker() { return this.tracker; } async getHeaderForHeight(height) { const header = await this.storage.getBlockHeaderByHeight(height); if (!header) throw new WERR_errors_1.WERR_INVALID_PARAMETER('height', `valid height '${height}' on mock chain`); return (0, Services_1.toBinaryBaseBlockHeader)(header); } async getHeight() { return this.tracker.currentHeight(); } async hashToHeader(hash) { const header = await this.storage.getBlockHeaderByHash(hash); if (!header) throw new WERR_errors_1.WERR_INVALID_PARAMETER('hash', `valid blockhash '${hash}' on mock chain`); return header; } hashOutputScript(script) { const hash = sdk_1.Utils.toHex((0, utilityHelpers_1.sha256Hash)(sdk_1.Utils.toArray(script, 'hex'))); return hash; } async isUtxo(output) { if (!output.lockingScript) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('output.lockingScript', 'validated by storage provider validateOutputScript.'); } const hash = this.hashOutputScript(sdk_1.Utils.toHex(output.lockingScript)); const or = await this.getUtxoStatus(hash, undefined, `${output.txid}.${output.vout}`); return or.isUtxo === true; } async getBsvExchangeRate() { return 50.0; } async getFiatExchangeRate(currency, base) { if (currency === (base || 'USD')) return 1; return 1.0; } async getFiatExchangeRates(targetCurrencies) { const rates = {}; for (const c of targetCurrencies) rates[c] = 1; return { timestamp: new Date(), base: 'USD', rates }; } async nLockTimeIsFinal(tx) { const MAXINT = 0xffffffff; const BLOCK_LIMIT = 500000000; let nLockTime; if (typeof tx === 'number') nLockTime = tx; else { if (typeof tx === 'string') { tx = sdk_1.Transaction.fromHex(tx); } else if (Array.isArray(tx)) { tx = sdk_1.Transaction.fromBinary(tx); } if (tx instanceof sdk_1.Transaction) { if (tx.inputs.every(i => i.sequence === MAXINT)) return true; nLockTime = tx.lockTime; } else { throw new WERR_errors_1.WERR_INTERNAL('Unsupported transaction format'); } } if (nLockTime >= BLOCK_LIMIT) { const limit = Math.floor(Date.now() / 1000); return nLockTime < limit; } const height = await this.getHeight(); return nLockTime < height; } async getBeefForTxid(txid) { const beef = new sdk_1.Beef(); const addTx = async (tid, alreadyAdded) => { if (alreadyAdded.has(tid)) return; alreadyAdded.add(tid); const txRow = await this.storage.getTransaction(tid); if (!txRow) return; const rawTx = txRow.rawTx instanceof Buffer ? Array.from(txRow.rawTx) : Array.isArray(txRow.rawTx) ? txRow.rawTx : Array.from(txRow.rawTx); if (txRow.blockHeight !== null) { // Mined: add with merkle path const pathResult = await this.getMerklePath(tid); if (pathResult.merklePath) { const bumpIndex = beef.mergeBump(pathResult.merklePath); beef.mergeRawTx(rawTx, bumpIndex); return; } } // Unmined or no path: recursively add source transactions const tx = sdk_1.Transaction.fromBinary(rawTx); for (const input of tx.inputs) { const sourceTxid = input.sourceTXID || (input.sourceTransaction ? input.sourceTransaction.id('hex') : undefined); if (sourceTxid && sourceTxid !== '00'.repeat(32)) { await addTx(sourceTxid, alreadyAdded); } } beef.mergeRawTx(rawTx); }; await addTx(txid, new Set()); return beef; } getServicesCallHistory() { return { version: 2, getMerklePath: { serviceName: 'getMerklePath', historyByProvider: {} }, getRawTx: { serviceName: 'getRawTx', historyByProvider: {} }, postBeef: { serviceName: 'postBeef', historyByProvider: {} }, getUtxoStatus: { serviceName: 'getUtxoStatus', historyByProvider: {} }, getStatusForTxids: { serviceName: 'getStatusForTxids', historyByProvider: {} }, getScriptHashHistory: { serviceName: 'getScriptHashHistory', historyByProvider: {} }, updateFiatExchangeRates: { serviceName: 'updateFiatExchangeRates', historyByProvider: {} } }; } } exports.MockServices = MockServices; //# sourceMappingURL=MockServices.js.map