@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
504 lines • 23.7 kB
JavaScript
;
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