"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