"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setDisableDoubleSpendCheckForTest = setDisableDoubleSpendCheckForTest;
exports.createAction = createAction;
const sdk_1 = require("@bsv/sdk");
const generateChange_1 = require("./generateChange");
const StorageProvider_1 = require("../StorageProvider");
const WERR_errors_1 = require("../../sdk/WERR_errors");
const utilityHelpers_1 = require("../../utility/utilityHelpers");
const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer");
const EntityProvenTx_1 = require("../schema/entities/EntityProvenTx");
const Wallet_1 = require("../../Wallet");
const offsetKey_1 = require("./offsetKey");
const utils_1 = require("./utils");
let disableDoubleSpendCheckForTest = true;
function setDisableDoubleSpendCheckForTest(v) {
    disableDoubleSpendCheckForTest = v;
}
async function createAction(storage, auth, vargs, originator) {
    const logger = vargs.logger;
    logger === null || logger === void 0 ? void 0 : logger.group(`storage createAction`);
    //stampLog(vargs, `start storage createTransactionSdk`)
    if (vargs.isTestWerrReviewActions)
        (0, Wallet_1.throwDummyReviewActions)();
    if (!vargs.isNewTx)
        // The purpose of this function is to create the initial storage records associated
        // with a new transaction. It's an error if we have no new inputs or outputs...
        throw new WERR_errors_1.WERR_INTERNAL();
    /**
     * Steps to create a transaction:
     * - Verify that all inputs either have proof in vargs.inputBEEF or that options.trustSelf === 'known' and input txid.vout are known valid to storage.
     * - Create a new transaction record with status 'unsigned' as the anchor for construction work and to new outputs.
     * - Create all transaction labels.
     * - Add new commission output
     * - Attempt to fund the transaction by allocating change outputs:
     *    - As each change output is selected it is simultaneously locked.
     * - Create all new output, basket, tag records
     * - If requested, create result Beef with complete proofs for all inputs used
     * - Create result inputs with source locking scripts
     * - Create result outputs with new locking scripts.
     * - Create and return result.
     */
    const userId = auth.userId;
    const { storageBeef, beef, xinputs } = await validateRequiredInputs(storage, userId, vargs);
    logger === null || logger === void 0 ? void 0 : logger.log('validated required inputs');
    const xoutputs = validateRequiredOutputs(storage, userId, vargs);
    logger === null || logger === void 0 ? void 0 : logger.log('validated required outputs');
    const changeBasketName = 'default';
    const changeBasket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({
        partial: { userId, name: changeBasketName }
    }), `Invalid outputGeneration basket "${changeBasketName}"`);
    logger === null || logger === void 0 ? void 0 : logger.log('found change basket');
    const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
    logger === null || logger === void 0 ? void 0 : logger.log('validated noSendChange');
    const availableChangeCount = await storage.countChangeInputs(userId, changeBasket.basketId, !vargs.isDelayed);
    logger === null || logger === void 0 ? void 0 : logger.log(`counted change inputs ${availableChangeCount}`);
    const feeModel = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel);
    logger === null || logger === void 0 ? void 0 : logger.log(`validated fee model ${JSON.stringify(feeModel)}`);
    await preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel);
    logger === null || logger === void 0 ? void 0 : logger.log('passed insufficient-funds preflight');
    const newTx = await createNewTxRecord(storage, userId, vargs, storageBeef);
    logger === null || logger === void 0 ? void 0 : logger.log(`created new transaction record`);
    const ctx = {
        xinputs,
        xoutputs,
        changeBasket,
        noSendChangeIn,
        availableChangeCount,
        feeModel,
        transactionId: newTx.transactionId
    };
    const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx);
    logger === null || logger === void 0 ? void 0 : logger.log(`funded new transaction`);
    if (maxPossibleSatoshisAdjustment) {
        const a = maxPossibleSatoshisAdjustment;
        if (ctx.xoutputs[a.fixedOutputIndex].satoshis !== generateChange_1.maxPossibleSatoshis)
            throw new WERR_errors_1.WERR_INTERNAL();
        ctx.xoutputs[a.fixedOutputIndex].satoshis = a.satoshis;
        logger === null || logger === void 0 ? void 0 : logger.log(`adjusted change outputs to max possible`);
    }
    // The satoshis of the transaction is the satoshis we get back in change minus the satoshis we spend.
    const satoshis = changeOutputs.reduce((a, e) => a + e.satoshis, 0) - allocatedChange.reduce((a, e) => a + e.satoshis, 0);
    await storage.updateTransaction(newTx.transactionId, { satoshis });
    const { outputs, changeVouts } = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
    logger === null || logger === void 0 ? void 0 : logger.log(`created new output records`);
    const inputBeef = await mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef);
    logger === null || logger === void 0 ? void 0 : logger.log(`merged allocated change beefs`);
    const inputs = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
    logger === null || logger === void 0 ? void 0 : logger.log(`created new inputs`);
    const r = {
        reference: newTx.reference,
        version: newTx.version,
        lockTime: newTx.lockTime,
        inputs,
        outputs,
        derivationPrefix,
        inputBeef,
        noSendChangeOutputVouts: vargs.isNoSend ? changeVouts : undefined
    };
    logger === null || logger === void 0 ? void 0 : logger.groupEnd();
    return r;
}
function makeDefaultOutput(userId, transactionId, satoshis, vout) {
    const now = new Date();
    const output = {
        created_at: now,
        updated_at: now,
        outputId: 0,
        userId,
        transactionId,
        satoshis: satoshis,
        vout,
        basketId: undefined,
        change: false,
        customInstructions: undefined,
        derivationPrefix: undefined,
        derivationSuffix: undefined,
        outputDescription: '',
        lockingScript: undefined,
        providedBy: 'you',
        purpose: '',
        senderIdentityKey: undefined,
        spendable: true,
        spendingDescription: undefined,
        spentBy: undefined,
        txid: undefined,
        type: ''
    };
    return output;
}
async function createNewInputs(storage, userId, vargs, ctx, allocatedChange) {
    const r = [];
    const newInputs = [];
    for (const i of ctx.xinputs) {
        const o = i.output;
        newInputs.push({ i, o });
    }
    const knownInputRows = newInputs.filter((ni) => !!ni.i && !!ni.o);
    if (knownInputRows.length > 0) {
        let doubleSpendTxid;
        await storage.transaction(async (trx) => {
            const outputIds = knownInputRows.map(ni => (0, utilityHelpers_1.verifyId)(ni.o.outputId));
            const knownOutputsById = await storage.findOutputsByIds(outputIds, trx);
            for (const ni of knownInputRows) {
                const { i, o } = ni;
                const o2 = knownOutputsById[(0, utilityHelpers_1.verifyId)(o.outputId)];
                if (!o2)
                    throw new WERR_errors_1.WERR_INTERNAL(`missing outputId ${o.outputId}`);
                if (o2.spentBy !== undefined) {
                    const spendingTx = await storage.findTransactionById((0, utilityHelpers_1.verifyId)(o2.spentBy), trx);
                    if (spendingTx === null || spendingTx === void 0 ? void 0 : spendingTx.txid) {
                        doubleSpendTxid = spendingTx.txid;
                        return;
                    }
                }
                if (o2.spendable != true) {
                    throw new WERR_errors_1.WERR_INVALID_PARAMETER(`inputs[${i.vin}]`, `spendable output. output ${o.txid}:${o.vout} appears to have been spent (spendable=${o2.spendable}).`);
                }
                await storage.updateOutput((0, utilityHelpers_1.verifyId)(o.outputId), {
                    spendable: false,
                    spentBy: ctx.transactionId,
                    spendingDescription: i.inputDescription
                }, trx);
                o.spendable = false;
                o.spentBy = ctx.transactionId;
                o.spendingDescription = i.inputDescription;
            }
        });
        if (doubleSpendTxid) {
            const beef = await storage.getBeefForTransaction(doubleSpendTxid, {});
            const rar = {
                txid: '',
                status: 'doubleSpend',
                competingTxs: [doubleSpendTxid],
                competingBeef: beef.toBinary()
            };
            throw new WERR_errors_1.WERR_REVIEW_ACTIONS([rar], []);
        }
    }
    for (const o of allocatedChange) {
        newInputs.push({ o, unlockLen: 107 });
    }
    let vin = -1;
    for (const { i, o, unlockLen } of newInputs) {
        vin++;
        if (o) {
            if (!i && !unlockLen)
                throw new WERR_errors_1.WERR_INTERNAL(`vin ${vin} non-fixedInput without unlockLen`);
            const sourceTransaction = vargs.includeAllSourceTransactions && vargs.isSignAction
                ? await storage.getRawTxOfKnownValidTransaction(o.txid)
                : undefined;
            const ri = {
                vin,
                sourceTxid: o.txid,
                sourceVout: o.vout,
                sourceSatoshis: o.satoshis,
                sourceLockingScript: (0, utilityHelpers_noBuffer_1.asString)(o.lockingScript),
                sourceTransaction,
                unlockingScriptLength: unlockLen ? unlockLen : i.unlockingScriptLength,
                providedBy: i && o.providedBy === 'storage' ? 'you-and-storage' : o.providedBy,
                type: o.type,
                spendingDescription: o.spendingDescription || undefined,
                derivationPrefix: o.derivationPrefix || undefined,
                derivationSuffix: o.derivationSuffix || undefined,
                senderIdentityKey: o.senderIdentityKey || undefined
            };
            r.push(ri);
        }
        else {
            if (!i)
                throw new WERR_errors_1.WERR_INTERNAL(`vin ${vin} without output or xinput`);
            // user specified input with no corresponding output being spent.
            const ri = {
                vin,
                sourceTxid: i.outpoint.txid,
                sourceVout: i.outpoint.vout,
                sourceSatoshis: i.satoshis,
                sourceLockingScript: i.lockingScript.toHex(),
                unlockingScriptLength: i.unlockingScriptLength,
                providedBy: 'you',
                type: 'custom',
                spendingDescription: undefined,
                derivationPrefix: undefined,
                derivationSuffix: undefined,
                senderIdentityKey: undefined
            };
            r.push(ri);
        }
    }
    return r;
}
async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
    var _a;
    const outputs = [];
    // Lookup output baskets
    const txBaskets = {};
    const basketNames = [...new Set(ctx.xoutputs.map(x => x.basket).filter((v) => !!v))];
    Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
    // Lookup output tags
    const txTags = {};
    const tagNames = [...new Set(ctx.xoutputs.flatMap(x => x.tags))];
    Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
    const newOutputs = [];
    for (const xo of ctx.xoutputs) {
        const lockingScript = (0, utilityHelpers_noBuffer_1.asArray)(xo.lockingScript);
        if (xo.purpose === 'service-charge') {
            const now = new Date();
            await storage.insertCommission({
                userId,
                transactionId: ctx.transactionId,
                lockingScript,
                satoshis: xo.satoshis,
                isRedeemed: false,
                keyOffset: (0, utilityHelpers_1.verifyTruthy)(xo.keyOffset),
                created_at: now,
                updated_at: now,
                commissionId: 0
            });
            const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
            o.lockingScript = lockingScript;
            o.providedBy = 'storage';
            o.purpose = 'storage-commission';
            o.type = 'custom';
            o.spendable = false;
            newOutputs.push({ o, tags: [] });
        }
        else {
            // The user wants tracking if they put their output in a basket
            const basketId = !xo.basket ? undefined : txBaskets[xo.basket].basketId;
            const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
            o.lockingScript = lockingScript;
            o.basketId = basketId;
            o.customInstructions = xo.customInstructions;
            o.outputDescription = xo.outputDescription;
            o.providedBy = xo.providedBy;
            o.purpose = xo.purpose || '';
            o.type = 'custom';
            newOutputs.push({ o, tags: xo.tags });
        }
    }
    for (const o of changeOutputs) {
        o.spendable = true;
        newOutputs.push({ o, tags: [] });
    }
    if (vargs.options.randomizeOutputs) {
        const randomVals = [];
        const nextRandomVal = () => {
            let val = 0;
            if (!randomVals || randomVals.length === 0) {
                val = Math.random();
            }
            else {
                val = randomVals.shift() || 0;
                randomVals.push(val);
            }
            return val;
        };
        /** In-place array shuffle */
        const shuffleArray = (array) => {
            let currentIndex = array.length;
            let temporaryValue;
            let randomIndex;
            while (currentIndex !== 0) {
                randomIndex = Math.floor(nextRandomVal() * currentIndex);
                currentIndex -= 1;
                temporaryValue = array[currentIndex];
                array[currentIndex] = array[randomIndex];
                array[randomIndex] = temporaryValue;
            }
            return array;
        };
        let vout = -1;
        const newVouts = Array(newOutputs.length);
        for (let i = 0; i < newVouts.length; i++)
            newVouts[i] = i;
        shuffleArray(newVouts);
        for (const no of newOutputs) {
            vout++;
            if (no.o.vout !== vout)
                throw new WERR_errors_1.WERR_INTERNAL(`new output ${vout} has out of order vout ${no.o.vout}`);
            no.o.vout = newVouts[vout];
        }
    }
    const changeVouts = [];
    for (const { o, tags } of newOutputs) {
        o.outputId = await storage.insertOutput(o);
        if (o.change && o.purpose === 'change' && o.providedBy === 'storage')
            changeVouts.push(o.vout);
        // Add tags to the output
        for (const tagName of [...new Set(tags)]) {
            const tag = txTags[tagName];
            await storage.insertOutputTagMap({
                outputId: (0, utilityHelpers_1.verifyId)(o.outputId),
                outputTagId: (0, utilityHelpers_1.verifyId)(tag.outputTagId),
                created_at: new Date(),
                updated_at: new Date(),
                isDeleted: false
            });
        }
        const ro = {
            vout: (0, utilityHelpers_1.verifyInteger)(o.vout),
            satoshis: sdk_1.Validation.validateSatoshis(o.satoshis, 'o.satoshis'),
            lockingScript: !o.lockingScript ? '' : (0, utilityHelpers_noBuffer_1.asString)(o.lockingScript),
            providedBy: (0, utilityHelpers_1.verifyTruthy)(o.providedBy),
            purpose: o.purpose || undefined,
            basket: (_a = Object.values(txBaskets).find(b => b.basketId === o.basketId)) === null || _a === void 0 ? void 0 : _a.name,
            tags: tags,
            outputDescription: o.outputDescription,
            derivationSuffix: o.derivationSuffix,
            customInstructions: o.customInstructions
        };
        outputs.push(ro);
    }
    return { outputs, changeVouts };
}
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
    const now = new Date();
    const newTx = {
        created_at: now,
        updated_at: now,
        transactionId: 0,
        version: vargs.version,
        lockTime: vargs.lockTime,
        status: 'unsigned',
        reference: (0, utilityHelpers_1.randomBytesBase64)(12),
        satoshis: 0, // updated after fundingTransaction
        userId,
        isOutgoing: true,
        inputBEEF: storageBeef.toBinary(),
        description: vargs.description,
        txid: undefined,
        rawTx: undefined
    };
    newTx.transactionId = await storage.insertTransaction(newTx);
    for (const label of vargs.labels) {
        const txLabel = await storage.findOrInsertTxLabel(userId, label);
        await storage.findOrInsertTxLabelMap((0, utilityHelpers_1.verifyId)(newTx.transactionId), (0, utilityHelpers_1.verifyId)(txLabel.txLabelId));
    }
    return newTx;
}
/**
 * Convert vargs.outputs:
 *
 * lockingScript: HexString
 * satoshis: SatoshiValue
 * outputDescription: DescriptionString5to50Bytes
 * basket?: BasketStringUnder300Bytes
 * customInstructions?: string
 * tags: BasketStringUnderBytes[]
 *
 * to XValidCreateActionOutput (which aims for StorageCreateTransactionSdkOutput)
 *
 * adds:
 *   vout: number
 *   providedBy: StorageProvidedBy
 *   purpose?: string
 *   derivationSuffix?: string
 *   keyOffset?: string
 *
 * @param vargs
 * @returns xoutputs
 */
function validateRequiredOutputs(storage, userId, vargs) {
    const xoutputs = [];
    let vout = -1;
    for (const output of vargs.outputs) {
        vout++;
        const xo = {
            ...output,
            vout,
            providedBy: 'you',
            purpose: undefined,
            derivationSuffix: undefined,
            keyOffset: undefined
        };
        xoutputs.push(xo);
    }
    if (storage.commissionSatoshis > 0 && storage.commissionPubKeyHex) {
        vout++;
        const { script, keyOffset } = (0, offsetKey_1.createStorageServiceChargeScript)(storage.commissionPubKeyHex);
        xoutputs.push({
            lockingScript: script,
            satoshis: storage.commissionSatoshis,
            outputDescription: 'Storage Service Charge',
            basket: undefined,
            tags: [],
            vout,
            providedBy: 'storage',
            purpose: 'service-charge',
            keyOffset
        });
    }
    return xoutputs;
}
/**
 * Verify that we are in posession of validity proof data for any inputs being proposed for a new transaction.
 *
 * `vargs.inputs` is the source of inputs.
 * `vargs.inputBEEF` may include new user supplied validity data.
 * 'vargs.options.trustSelf === 'known'` indicates whether we can rely on the storage database records.
 *
 * If there are no inputs, returns an empty `Beef`.
 *
 * Always pulls rawTx data into first level of validity chains so that parsed transaction data is available
 * and checks input sourceSatoshis as well as filling in input sourceLockingScript.
 *
 * This data may be pruned again before being returned to the user based on `vargs.options.knownTxids`.
 *
 * @param storage
 * @param userId
 * @param vargs
 * @returns {storageBeef} containing only validity proof data for only unknown required inputs.
 * @returns {beef} containing verified validity proof data for all required inputs.
 * @returns {xinputs} extended validated required inputs.
 */
async function validateRequiredInputs(storage, userId, vargs) {
    //stampLog(vargs, `start storage verifyInputBeef`)
    const beef = new sdk_1.Beef();
    if (vargs.inputs.length === 0)
        return { storageBeef: beef, beef, xinputs: [] };
    if (vargs.inputBEEF)
        beef.mergeBeef(vargs.inputBEEF);
    const xinputs = vargs.inputs.map((input, vin) => ({
        ...input,
        vin,
        satoshis: -1,
        lockingScript: new sdk_1.Script(),
        output: undefined
    }));
    const trustSelf = vargs.options.trustSelf === 'known';
    const inputTxids = {};
    for (const input of xinputs)
        inputTxids[input.outpoint.txid] = true;
    // Check beef from user that either there are no txidOnly entries,
    // or that we can trust storage data and it does indeed vouch
    // for any txidOnly entries
    for (const btx of beef.txs) {
        if (btx.isTxidOnly) {
            if (!trustSelf)
                throw new WERR_errors_1.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain complete proof data for ${btx.txid}`);
            if (!inputTxids[btx.txid]) {
                // inputTxids are checked next
                const isKnown = await storage.verifyKnownValidTransaction(btx.txid);
                if (!isKnown)
                    throw new WERR_errors_1.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain complete proof data for unknown ${btx.txid}`);
            }
        }
    }
    // Make sure that there's an entry for all inputs txid values:
    for (const txid of Object.keys(inputTxids)) {
        let btx = beef.findTxid(txid);
        if (!btx && trustSelf) {
            if (await storage.verifyKnownValidTransaction(txid))
                btx = beef.mergeTxidOnly(txid);
        }
        if (!btx) {
            throw new WERR_errors_1.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for possibly known ${txid}, beef ${beef.toLogString()}`);
        }
    }
    if (!(await beef.verify(await storage.getServices().getChainTracker(), true))) {
        console.log(`verifyInputBeef failed, inputBEEF failed to verify.\n${beef.toLogString()}\n`);
        //console.log(`verifyInputBeef failed, inputBEEF failed to verify.\n${stampLogFormat(vargs.log)}\n${beef.toLogString()}\n`)
        throw new WERR_errors_1.WERR_INVALID_PARAMETER('inputBEEF', 'valid Beef when factoring options.trustSelf');
    }
    // beef may now be trusted and has a BeefTx for every input txid.
    const storageBeef = beef.clone();
    const preloadedOutputsByOutpoint = await storage.findOutputsByOutpoints(userId, xinputs.map(i => ({ txid: i.outpoint.txid, vout: i.outpoint.vout })));
    for (const input of xinputs) {
        const { txid, vout } = input.outpoint;
        let output = preloadedOutputsByOutpoint[`${txid}.${vout}`];
        if (!output)
            output = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findOutputs({ partial: { userId, txid, vout } }));
        if (output) {
            if (output.change) {
                throw new WERR_errors_1.WERR_INVALID_PARAMETER(`inputs[${input.vin}]`, 'an unmanaged input. Change outputs are managed by your wallet.');
            }
            input.output = output;
            if (!Array.isArray(output.lockingScript) || !Number.isInteger(output.satoshis))
                throw new WERR_errors_1.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'output with valid lockingScript and satoshis');
            if (!disableDoubleSpendCheckForTest && !output.spendable && !vargs.isNoSend)
                throw new WERR_errors_1.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'spendable output unless noSend is true');
            // input is spending an existing user output which has an lockingScript
            input.satoshis = sdk_1.Validation.validateSatoshis(output.satoshis, 'output.satoshis');
            input.lockingScript = sdk_1.Script.fromBinary((0, utilityHelpers_noBuffer_1.asArray)(output.lockingScript));
        }
        else {
            let btx = beef.findTxid(txid);
            if (btx.isTxidOnly) {
                const { rawTx, proven } = await storage.getProvenOrRawTx(txid);
                //stampLog(vargs, `... storage verifyInputBeef getProvenOrRawTx ${txid} ${proven ? 'proven' : rawTx ? 'rawTx' : 'unknown'}`)
                if (!rawTx)
                    throw new WERR_errors_1.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for ${txid}`);
                btx = beef.mergeRawTx((0, utilityHelpers_noBuffer_1.asArray)(rawTx));
                if (proven)
                    beef.mergeBump(new EntityProvenTx_1.EntityProvenTx(proven).getMerklePath());
            }
            // btx is valid has parsed transaction data.
            if (vout >= btx.tx.outputs.length)
                throw new WERR_errors_1.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'valid outpoint');
            const so = btx.tx.outputs[vout];
            input.satoshis = sdk_1.Validation.validateSatoshis(so.satoshis, 'so.satoshis');
            input.lockingScript = so.lockingScript;
        }
    }
    return { beef, storageBeef, xinputs };
}
async function verifyBeefFixOrhpans(beef, storage) {
    const r = beef.verifyValid();
    if (!r.valid) {
        // Beef is structurally invalid.
        return false;
    }
    const heights = Object.keys(r.roots);
    const services = storage.getServices();
    const chainTracker = await services.getChainTracker();
    let rootsAreValid = true;
    for (const height of heights) {
        const isValid = await chainTracker.isValidRootForHeight(r.roots[height], Number(height));
        if (isValid)
            continue;
        // The original block may have been orphaned, check for a new proof.
        const mp = beef.bumps.find(b => b.blockHeight === Number(height));
        //const p = await services.getMerklePath()
    }
    return false;
}
async function validateNoSendChange(storage, userId, vargs, changeBasket) {
    const r = [];
    if (!vargs.isNoSend)
        return [];
    const noSendChange = vargs.options.noSendChange;
    if (noSendChange && noSendChange.length > 0) {
        for (const op of noSendChange) {
            const output = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findOutputs({
                partial: { userId, txid: op.txid, vout: op.vout }
            }));
            // noSendChange is not marked spendable until sent, may not already be spent, and must have a valid greater than zero satoshis
            if (!output ||
                output.providedBy !== 'storage' ||
                output.purpose !== 'change' ||
                output.spendable === false ||
                Number.isInteger(output.spentBy) ||
                !(0, utilityHelpers_1.verifyNumber)(output.satoshis) ||
                output.basketId !== changeBasket.basketId)
                throw new WERR_errors_1.WERR_INVALID_PARAMETER('noSendChange outpoint', 'valid');
            if (-1 < r.findIndex(o => o.outputId === output.outputId))
                // noSendChange duplicate OutPoints are not allowed.
                throw new WERR_errors_1.WERR_INVALID_PARAMETER('noSendChange outpoint', 'unique. Duplicates are not allowed.');
            r.push(output);
        }
    }
    return r;
}
async function preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel) {
    if (feeModel.model !== 'sat/kb' || !feeModel.value)
        return;
    const fixedInputSatoshis = xinputs.reduce((a, e) => a + e.satoshis, 0);
    const noSendSatoshis = noSendChangeIn.reduce((a, e) => a + Number(e.satoshis || 0), 0);
    const spending = xoutputs.reduce((a, e) => a + e.satoshis, 0);
    const minSize = (0, utils_1.transactionSize)(xinputs.map(i => i.unlockingScriptLength || 0), xoutputs.map(o => Math.floor(o.lockingScript.length / 2)));
    const minFee = Math.ceil((minSize / 1000) * feeModel.value);
    const minRequired = spending + minFee;
    const fixedAvailable = fixedInputSatoshis + noSendSatoshis;
    if (fixedAvailable >= minRequired)
        return;
    // Keep common successful path cheap:
    // - If there are zero change candidates, failure is certain.
    // Otherwise, defer to the main funding allocator.
    const deficit = minRequired - fixedAvailable;
    if (availableChangeCount <= 0) {
        throw new WERR_errors_1.WERR_INSUFFICIENT_FUNDS(minRequired, deficit);
    }
}
async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
    const params = {
        fixedInputs: ctx.xinputs.map(xi => ({
            satoshis: xi.satoshis,
            unlockingScriptLength: xi.unlockingScriptLength
        })),
        fixedOutputs: ctx.xoutputs.map(xo => ({
            satoshis: xo.satoshis,
            lockingScriptLength: xo.lockingScript.length / 2
        })),
        feeModel: ctx.feeModel,
        changeInitialSatoshis: ctx.changeBasket.minimumDesiredUTXOValue,
        changeFirstSatoshis: Math.max(1, Math.round(ctx.changeBasket.minimumDesiredUTXOValue / 4)),
        changeLockingScriptLength: 25,
        changeUnlockingScriptLength: 107,
        targetNetCount: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount,
        randomVals: vargs.randomVals
    };
    const noSendChange = [...ctx.noSendChangeIn];
    const outputs = {};
    const allocateChangeInput = async (targetSatoshis, exactSatoshis) => {
        // noSendChange gets allocated first...typically only one input...just allocate in order...
        if (noSendChange.length > 0) {
            const o = noSendChange.pop();
            outputs[o.outputId] = o;
            // allocate the output in storage, noSendChange is by definition spendable false and part of noSpend transaction batch.
            await storage.updateOutput(o.outputId, {
                spendable: false,
                spentBy: ctx.transactionId
            });
            o.spendable = false;
            o.spentBy = ctx.transactionId;
            const r = {
                outputId: o.outputId,
                satoshis: o.satoshis
            };
            return r;
        }
        const basketId = ctx.changeBasket.basketId;
        const o = await storage.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
        if (!o)
            return undefined;
        outputs[o.outputId] = o;
        const r = {
            outputId: o.outputId,
            satoshis: o.satoshis
        };
        return r;
    };
    const releaseChangeInput = async (outputId) => {
        const nsco = ctx.noSendChangeIn.find(o => o.outputId === outputId);
        if (nsco) {
            noSendChange.push(nsco);
            return;
        }
        await storage.updateOutput(outputId, {
            spendable: true,
            spentBy: undefined
        });
    };
    const gcr = await (0, generateChange_1.generateChangeSdk)(params, allocateChangeInput, releaseChangeInput, vargs.logger);
    const nextRandomVal = () => {
        let val = 0;
        if (!vargs.randomVals || vargs.randomVals.length === 0) {
            val = Math.random();
        }
        else {
            val = vargs.randomVals.shift() || 0;
            vargs.randomVals.push(val);
        }
        return val;
    };
    /**
     * @returns a random integer betweenn min and max, inclussive.
     */
    const rand = (min, max) => {
        if (max < min)
            throw new WERR_errors_1.WERR_INVALID_PARAMETER('max', `less than min (${min}). max is (${max})`);
        return Math.floor(nextRandomVal() * (max - min + 1) + min);
    };
    const randomDerivation = (count) => {
        let val = [];
        if (!vargs.randomVals || vargs.randomVals.length === 0) {
            val = (0, sdk_1.Random)(count);
        }
        else {
            for (let i = 0; i < count; i++)
                val.push(rand(0, 255));
        }
        return sdk_1.Utils.toBase64(val);
    };
    // Generate a derivation prefix for the payment
    const derivationPrefix = randomDerivation(16);
    const r = {
        maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
        allocatedChange: gcr.allocatedChangeInputs.map(i => outputs[i.outputId]),
        changeOutputs: gcr.changeOutputs.map((o, i) => ({
            // what we knnow now and can insert into the database for this new transaction's change output
            created_at: new Date(),
            updated_at: new Date(),
            outputId: 0,
            userId,
            transactionId: ctx.transactionId,
            vout: params.fixedOutputs.length + i,
            satoshis: o.satoshis,
            basketId: ctx.changeBasket.basketId,
            spendable: false,
            change: true,
            type: 'P2PKH',
            derivationPrefix,
            derivationSuffix: randomDerivation(16),
            providedBy: 'storage',
            purpose: 'change',
            customInstructions: undefined,
            senderIdentityKey: undefined,
            outputDescription: '',
            // what will be known when transaction is signed
            txid: undefined,
            lockingScript: undefined,
            // when this output gets spent
            spentBy: undefined,
            spendingDescription: undefined
        })),
        derivationPrefix
    };
    return r;
}
/**
 * Avoid returning any known raw transaction data by converting any known transaction
 * in the `beef` to txidOnly.
 * @returns undefined if `vargs.options.returnTXIDOnly` or trimmed `Beef`
 */
function trimInputBeef(beef, vargs) {
    if (vargs.options.returnTXIDOnly)
        return undefined;
    const knownTxids = {};
    for (const txid of vargs.options.knownTxids)
        knownTxids[txid] = true;
    for (const txid of beef.txs.map(btx => btx.txid))
        if (knownTxids[txid])
            beef.makeTxidOnly(txid);
    return beef.toBinary();
}
async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef) {
    const options = {
        trustSelf: undefined,
        knownTxids: vargs.options.knownTxids,
        mergeToBeef: beef,
        ignoreStorage: false,
        ignoreServices: true,
        ignoreNewProven: false,
        minProofLevel: undefined
    };
    if (vargs.options.returnTXIDOnly)
        return undefined;
    for (const o of allocatedChange) {
        if (!beef.findTxid(o.txid) && !vargs.options.knownTxids.find(txid => txid === o.txid)) {
            await storage.getBeefForTransaction(o.txid, options);
        }
    }
    return trimInputBeef(beef, vargs);
}
//# sourceMappingURL=createAction.js.map