UNPKG

wallet-storage-client

Version:
635 lines 27.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAction = createAction; exports.offsetPubKey = offsetPubKey; exports.lockScriptWithKeyOffsetFromPubKey = lockScriptWithKeyOffsetFromPubKey; exports.createStorageServiceChargeScript = createStorageServiceChargeScript; const sdk_1 = require("@bsv/sdk"); const index_client_1 = require("../../index.client"); const generateChange_1 = require("./generateChange"); async function createAction(storage, auth, vargs, originator) { //stampLog(vargs, `start storage createTransactionSdk`) 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 index_client_1.sdk.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); const xoutputs = validateRequiredOutputs(storage, userId, vargs); const changeBasketName = 'default'; const changeBasket = (0, index_client_1.verifyOne)(await storage.findOutputBaskets({ partial: { userId, name: changeBasketName } }), `Invalid outputGeneration basket "${changeBasketName}"`); const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket); const availableChangeCount = await storage.countChangeInputs(userId, changeBasket.basketId, !vargs.isDelayed); const feeModel = (0, index_client_1.validateStorageFeeModel)(storage.feeModel); const newTx = await createNewTxRecord(storage, userId, vargs, storageBeef); const ctx = { xinputs, xoutputs, changeBasket, noSendChangeIn, availableChangeCount, feeModel, transactionId: newTx.transactionId }; const { allocatedChange, changeOutputs, derivationPrefix } = await fundNewTransactionSdk(storage, userId, vargs, ctx); // 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); const inputBeef = await mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef); const inputs = await createNewInputs(storage, userId, vargs, ctx, allocatedChange); const r = { reference: newTx.reference, version: newTx.version, lockTime: newTx.lockTime, inputs, outputs, derivationPrefix, inputBeef, noSendChangeOutputVouts: vargs.isNoSend ? changeVouts : undefined }; //stampLog(vargs, `end storage createTransactionSdk`) 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 }); if (o) { await storage.updateOutput(o.outputId, { spendable: false, spentBy: ctx.transactionId, spendingDescription: i.inputDescription }); } } 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 index_client_1.sdk.WERR_INTERNAL(`vin ${vin} non-fixedInput without unlockLen`); const ri = { vin, sourceTxid: o.txid, sourceVout: o.vout, sourceSatoshis: o.satoshis, sourceLockingScript: (0, index_client_1.asString)(o.lockingScript), 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 index_client_1.sdk.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 = {}; for (const xo of ctx.xoutputs) { if (xo.basket !== undefined && !txBaskets[xo.basket]) txBaskets[xo.basket] = await storage.findOrInsertOutputBasket(userId, xo.basket); } // Lookup output tags const txTags = {}; for (const xo of ctx.xoutputs) { for (const tag of xo.tags) { txTags[tag] = await storage.findOrInsertOutputTag(userId, tag); } } const newOutputs = []; for (const xo of ctx.xoutputs) { const lockingScript = (0, index_client_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, index_client_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 index_client_1.sdk.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 tags) { const tag = txTags[tagName]; await storage.findOrInsertOutputTagMap((0, index_client_1.verifyId)(o.outputId), (0, index_client_1.verifyId)(tag.outputTagId)); } const ro = { vout: (0, index_client_1.verifyTruthy)(o.vout), satoshis: (0, index_client_1.verifyTruthy)(o.satoshis), lockingScript: !o.lockingScript ? '' : (0, index_client_1.asString)(o.lockingScript), providedBy: (0, index_client_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, index_client_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, index_client_1.verifyId)(newTx.transactionId), (0, index_client_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 sdk.StorageCreateTransactionSdkOutput) * * adds: * vout: number * providedBy: sdk.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 } = 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() })); 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 index_client_1.sdk.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 index_client_1.sdk.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 index_client_1.sdk.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for possibly known ${txid}`); } 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 index_client_1.sdk.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(); for (const input of xinputs) { const { txid, vout } = input.outpoint; const output = (0, index_client_1.verifyOneOrNone)(await storage.findOutputs({ partial: { userId, txid, vout } })); if (output) { input.output = output; if (!Array.isArray(output.lockingScript) || !Number.isInteger(output.satoshis)) throw new index_client_1.sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'output with valid lockingScript and satoshis'); if (!output.spendable && !vargs.isNoSend) throw new index_client_1.sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'spendable output unless noSend is true'); // input is spending an existing user output which has an lockingScript input.satoshis = (0, index_client_1.verifyNumber)(output.satoshis); input.lockingScript = sdk_1.Script.fromBinary((0, index_client_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 index_client_1.sdk.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for ${txid}`); btx = beef.mergeRawTx((0, index_client_1.asArray)(rawTx)); if (proven) beef.mergeBump(new index_client_1.entity.ProvenTx(proven).getMerklePath()); } // btx is valid has parsed transaction data. if (vout >= btx.tx.outputs.length) throw new index_client_1.sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'valid outpoint'); const so = btx.tx.outputs[vout]; input.satoshis = (0, index_client_1.verifyTruthy)(so.satoshis); input.lockingScript = so.lockingScript; } } return { beef, storageBeef, xinputs }; } async function validateNoSendChange(dojo, 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, index_client_1.verifyOneOrNone)(await dojo.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, index_client_1.verifyNumber)(output.satoshis) || output.basketId !== changeBasket.basketId) throw new index_client_1.sdk.WERR_INVALID_PARAMETER('noSendChange outpoint', 'valid'); if (-1 < r.findIndex(o => o.outputId === output.outputId)) // noSendChange duplicate OutPoints are not allowed. throw new index_client_1.sdk.WERR_INVALID_PARAMETER('noSendChange outpoint', 'unique. Duplicates are not allowed.'); r.push(output); } } return r; } async function fundNewTransactionSdk(dojo, 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, ctx.changeBasket.minimumDesiredUTXOValue / 4), changeLockingScriptLength: 25, changeUnlockingScriptLength: 107, targetNetCount: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount }; 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 dojo.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 dojo.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 dojo.updateOutput(outputId, { spendable: true, spentBy: undefined }); }; const gcr = await (0, generateChange_1.generateChangeSdk)(params, allocateChangeInput, releaseChangeInput); // Generate a derivation prefix for the payment const derivationPrefix = (0, index_client_1.randomBytesBase64)(16); const r = { 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: (0, index_client_1.randomBytesBase64)(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(dojo, 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 dojo.getBeefForTransaction(o.txid, options); } } return trimInputBeef(beef, vargs); } function keyOffsetToHashedSecret(pub, keyOffset) { let offset; if (keyOffset !== undefined && typeof keyOffset === 'string') { if (keyOffset.length === 64) offset = sdk_1.PrivateKey.fromString(keyOffset, 'hex'); else offset = sdk_1.PrivateKey.fromWif(keyOffset); } else { offset = sdk_1.PrivateKey.fromRandom(); keyOffset = offset.toWif(); } const sharedSecret = pub.mul(offset).encode(true, undefined); const hashedSecret = (0, index_client_1.sha256Hash)(sharedSecret); return { hashedSecret: new sdk_1.BigNumber(hashedSecret), keyOffset }; } function offsetPubKey(pubKey, keyOffset) { const pub = sdk_1.PublicKey.fromString(pubKey); const r = keyOffsetToHashedSecret(pub, keyOffset); // The hashed secret is multiplied by the generator point. const point = new sdk_1.Curve().g.mul(r.hashedSecret); // The resulting point is added to the recipient public key. const offsetPubKey = new sdk_1.PublicKey(pub.add(point)); return { offsetPubKey: offsetPubKey.toString(), keyOffset: r.keyOffset }; } function lockScriptWithKeyOffsetFromPubKey(pubKey, keyOffset) { const r = offsetPubKey(pubKey, keyOffset); const offsetPub = sdk_1.PublicKey.fromString(r.offsetPubKey); const hash = offsetPub.toHash(); const script = new sdk_1.P2PKH().lock(hash).toHex(); return { script, keyOffset: r.keyOffset }; } function createStorageServiceChargeScript(pubKeyHex) { return lockScriptWithKeyOffsetFromPubKey(pubKeyHex); } //# sourceMappingURL=createAction.js.map