UNPKG

@bsv/wallet-toolbox-client

Version:
567 lines 25.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StorageProvider = void 0; exports.validateStorageFeeModel = validateStorageFeeModel; const sdk_1 = require("@bsv/sdk"); const getBeefForTransaction_1 = require("./methods/getBeefForTransaction"); const processAction_1 = require("./methods/processAction"); const attemptToPostReqsToNetwork_1 = require("./methods/attemptToPostReqsToNetwork"); const listCertificates_1 = require("./methods/listCertificates"); const createAction_1 = require("./methods/createAction"); const internalizeAction_1 = require("./methods/internalizeAction"); const StorageReaderWriter_1 = require("./StorageReaderWriter"); const entities_1 = require("./schema/entities"); const WERR_errors_1 = require("../sdk/WERR_errors"); const utilityHelpers_1 = require("../utility/utilityHelpers"); const WalletError_1 = require("../sdk/WalletError"); const utilityHelpers_noBuffer_1 = require("../utility/utilityHelpers.noBuffer"); class StorageProvider extends StorageReaderWriter_1.StorageReaderWriter { static defaultOptions() { return { feeModel: { model: 'sat/kb', value: 1 }, commissionSatoshis: 0, commissionPubKeyHex: undefined }; } static createStorageBaseOptions(chain) { const options = { ...StorageProvider.defaultOptions(), chain }; return options; } constructor(options) { super(options); this.isDirty = false; this.feeModel = options.feeModel; this.commissionPubKeyHex = options.commissionPubKeyHex; this.commissionSatoshis = options.commissionSatoshis; this.maxRecursionDepth = 12; } isStorageProvider() { return true; } setServices(v) { this._services = v; } getServices() { if (!this._services) throw new WERR_errors_1.WERR_INVALID_OPERATION('Must setServices first.'); return this._services; } async abortAction(auth, args) { if (!auth.userId) throw new WERR_errors_1.WERR_INVALID_PARAMETER('auth.userId', 'valid'); const userId = auth.userId; let reference = args.reference; let txid = undefined; const r = await this.transaction(async (trx) => { let tx = (0, utilityHelpers_1.verifyOneOrNone)(await this.findTransactions({ partial: { reference, userId }, noRawTx: true, trx })); if (!tx && args.reference.length === 64) { // reference may also be a txid txid = reference; reference = undefined; tx = (0, utilityHelpers_1.verifyOneOrNone)(await this.findTransactions({ partial: { txid, userId }, noRawTx: true, trx })); } const unAbortableStatus = ['completed', 'failed', 'sending', 'unproven']; if (!tx || !tx.isOutgoing || -1 < unAbortableStatus.findIndex(s => s === tx.status)) throw new WERR_errors_1.WERR_INVALID_PARAMETER('reference', 'an inprocess, outgoing action that has not been signed and shared to the network.'); await this.updateTransactionStatus('failed', tx.transactionId, userId, reference, trx); if (tx.txid) { const req = await entities_1.EntityProvenTxReq.fromStorageTxid(this, tx.txid, trx); if (req) { req.addHistoryNote({ what: 'abortAction', reference: args.reference }); req.status = 'invalid'; await req.updateStorageDynamicProperties(this, trx); } } const r = { aborted: true }; return r; }); return r; } async internalizeAction(auth, args) { return await (0, internalizeAction_1.internalizeAction)(this, auth, args); } /** * Given an array of transaction txids with current ProvenTxReq ready-to-share status, * lookup their ProvenTxReqApi req records. * For the txids with reqs and status still ready to send construct a single merged beef. * * @param txids * @param knownTxids * @param trx */ async getReqsAndBeefToShareWithWorld(txids, knownTxids, trx) { const r = { beef: new sdk_1.Beef(), details: [] }; for (const txid of txids) { const d = { txid, // status: 'readyToSend' | 'alreadySent' | 'error' | 'unknown' status: 'unknown' // req?: TableProvenTxReq // proven?: TableProvenTx // error?: string }; r.details.push(d); try { d.proven = (0, utilityHelpers_1.verifyOneOrNone)(await this.findProvenTxs({ partial: { txid }, trx })); if (d.proven) d.status = 'alreadySent'; else { const alreadySentStatus = ['unmined', 'callback', 'unconfirmed', 'completed']; const readyToSendStatus = ['sending', 'unsent', 'nosend', 'unprocessed']; const errorStatus = ['unknown', 'nonfinal', 'invalid', 'doubleSpend']; d.req = (0, utilityHelpers_1.verifyOneOrNone)(await this.findProvenTxReqs({ partial: { txid }, trx })); if (!d.req) { d.status = 'error'; d.error = `ERR_UNKNOWN_TXID: ${txid} was not found.`; } else if (errorStatus.indexOf(d.req.status) > -1) { d.status = 'error'; d.error = `ERR_INVALID_PARAMETER: ${txid} is not ready to send.`; } else if (alreadySentStatus.indexOf(d.req.status) > -1) { d.status = 'alreadySent'; } else if (readyToSendStatus.indexOf(d.req.status) > -1) { if (!d.req.rawTx || !d.req.inputBEEF) { d.status = 'error'; d.error = `ERR_INTERNAL: ${txid} req is missing rawTx or beef.`; } else d.status = 'readyToSend'; } else { d.status = 'error'; d.error = `ERR_INTERNAL: ${txid} has unexpected req status ${d.req.status}`; } if (d.status === 'readyToSend') { await this.mergeReqToBeefToShareExternally(d.req, r.beef, knownTxids, trx); } } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); d.error = `${e.name}: ${e.message}`; } } return r; } async mergeReqToBeefToShareExternally(req, mergeToBeef, knownTxids, trx) { const { rawTx, inputBEEF: beef } = req; if (!rawTx || !beef) throw new WERR_errors_1.WERR_INTERNAL(`req rawTx and beef must be valid.`); mergeToBeef.mergeRawTx((0, utilityHelpers_noBuffer_1.asArray)(rawTx)); mergeToBeef.mergeBeef((0, utilityHelpers_noBuffer_1.asArray)(beef)); const tx = sdk_1.Transaction.fromBinary((0, utilityHelpers_noBuffer_1.asArray)(rawTx)); for (const input of tx.inputs) { if (!input.sourceTXID) throw new WERR_errors_1.WERR_INTERNAL(`req all transaction inputs must have valid sourceTXID`); const txid = input.sourceTXID; const btx = mergeToBeef.findTxid(txid); if (!btx) { if (knownTxids && knownTxids.indexOf(txid) > -1) mergeToBeef.mergeTxidOnly(txid); else await this.getValidBeefForKnownTxid(txid, mergeToBeef, undefined, knownTxids, trx); } } } /** * Checks if txid is a known valid ProvenTx and returns it if found. * Next checks if txid is a current ProvenTxReq and returns that if found. * If `newReq` is provided and an existing ProvenTxReq isn't found, * use `newReq` to create a new ProvenTxReq. * * This is safe "findOrInsert" operation using retry if unique index constraint * is violated by a race condition insert. * * @param txid * @param newReq * @param trx * @returns */ async getProvenOrReq(txid, newReq, trx) { if (newReq && txid !== newReq.txid) throw new WERR_errors_1.WERR_INVALID_PARAMETER('newReq', `same txid`); const r = { proven: undefined, req: undefined }; r.proven = (0, utilityHelpers_1.verifyOneOrNone)(await this.findProvenTxs({ partial: { txid }, trx })); if (r.proven) return r; for (let retry = 0;; retry++) { try { r.req = (0, utilityHelpers_1.verifyOneOrNone)(await this.findProvenTxReqs({ partial: { txid }, trx })); if (!r.req && !newReq) break; if (!r.req && newReq) { await this.insertProvenTxReq(newReq, trx); } if (r.req && newReq) { // Merge history and notify into existing const req1 = new entities_1.EntityProvenTxReq(r.req); req1.mergeHistory(newReq, undefined, true); req1.mergeNotifyTransactionIds(newReq); await req1.updateStorageDynamicProperties(this, trx); } break; } catch (eu) { if (retry > 0) throw eu; } } return r; } async updateTransactionsStatus(transactionIds, status, trx) { await this.transaction(async (trx) => { for (const id of transactionIds) { await this.updateTransactionStatus(status, id, undefined, undefined, trx); } }, trx); } /** * For all `status` values besides 'failed', just updates the transaction records status property. * * For 'status' of 'failed', attempts to make outputs previously allocated as inputs to this transaction usable again. * * @param status * @param transactionId * @param userId * @param reference * @param trx */ async updateTransactionStatus(status, transactionId, userId, reference, trx) { if (!transactionId && !(userId && reference)) throw new WERR_errors_1.WERR_MISSING_PARAMETER('either transactionId or userId and reference'); await this.transaction(async (trx) => { const where = {}; if (transactionId) where.transactionId = transactionId; if (userId) where.userId = userId; if (reference) where.reference = reference; const tx = (0, utilityHelpers_1.verifyOne)(await this.findTransactions({ partial: where, noRawTx: true, trx })); //if (tx.status === status) // no change required. Assume inputs and outputs spendable and spentBy are valid for status. //return // Once completed, this method cannot be used to "uncomplete" transaction. if ((status !== 'completed' && tx.status === 'completed') || tx.provenTxId) throw new WERR_errors_1.WERR_INVALID_OPERATION('The status of a "completed" transaction cannot be changed.'); // It is not possible to un-fail a transaction. Information is lost and not recoverable. if (status !== 'failed' && tx.status === 'failed') throw new WERR_errors_1.WERR_INVALID_OPERATION(`A "failed" transaction may not be un-failed by this method.`); switch (status) { case 'failed': { // Attempt to make outputs previously allocated as inputs to this transaction usable again. // Only clear input's spentBy and reset spendable = true if it references this transaction const t = new entities_1.EntityTransaction(tx); const inputs = await t.getInputs(this, trx); for (const input of inputs) { // input is a prior output belonging to userId that reference this transaction either by `spentBy` // or by txid and vout. await this.updateOutput((0, utilityHelpers_1.verifyId)(input.outputId), { spendable: true, spentBy: undefined }, trx); } } break; case 'nosend': case 'unsigned': case 'unprocessed': case 'sending': case 'unproven': case 'completed': break; default: throw new WERR_errors_1.WERR_INVALID_PARAMETER('status', `not be ${status}`); } await this.updateTransaction(tx.transactionId, { status }, trx); }, trx); } async createAction(auth, args) { if (!auth.userId) throw new WERR_errors_1.WERR_UNAUTHORIZED(); return await (0, createAction_1.createAction)(this, auth, args); } async processAction(auth, args) { if (!auth.userId) throw new WERR_errors_1.WERR_UNAUTHORIZED(); return await (0, processAction_1.processAction)(this, auth, args); } async attemptToPostReqsToNetwork(reqs, trx, logger) { return await (0, attemptToPostReqsToNetwork_1.attemptToPostReqsToNetwork)(this, reqs, trx, logger); } async listCertificates(auth, args) { return await (0, listCertificates_1.listCertificates)(this, auth, args); } async verifyKnownValidTransaction(txid, trx) { const { proven, rawTx } = await this.getProvenOrRawTx(txid, trx); return proven != undefined || rawTx != undefined; } async getValidBeefForKnownTxid(txid, mergeToBeef, trustSelf, knownTxids, trx, requiredLevels) { const beef = await this.getValidBeefForTxid(txid, mergeToBeef, trustSelf, knownTxids, trx, requiredLevels); if (!beef) throw new WERR_errors_1.WERR_INVALID_PARAMETER('txid', `known to storage. ${txid} is not known.`); return beef; } async getValidBeefForTxid(txid, mergeToBeef, trustSelf, knownTxids, trx, requiredLevels) { const beef = mergeToBeef || new sdk_1.Beef(); const r = await this.getProvenOrRawTx(txid, trx); if (r.proven) { if (requiredLevels) { r.rawTx = r.proven.rawTx; } else { if (trustSelf === 'known') beef.mergeTxidOnly(txid); else { beef.mergeRawTx(r.proven.rawTx); const mp = new entities_1.EntityProvenTx(r.proven).getMerklePath(); beef.mergeBump(mp); return beef; } } } if (!r.rawTx) return undefined; if (trustSelf === 'known') { beef.mergeTxidOnly(txid); } else { beef.mergeRawTx(r.rawTx); if (r.inputBEEF) beef.mergeBeef(r.inputBEEF); const tx = sdk_1.Transaction.fromBinary(r.rawTx); if (requiredLevels) requiredLevels--; for (const input of tx.inputs) { const btx = beef.findTxid(input.sourceTXID); if (!btx) { if (!requiredLevels && knownTxids && knownTxids.indexOf(input.sourceTXID) > -1) beef.mergeTxidOnly(input.sourceTXID); else await this.getValidBeefForKnownTxid(input.sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels); } } } return beef; } async getBeefForTransaction(txid, options) { const beef = await (0, getBeefForTransaction_1.getBeefForTransaction)(this, txid, options); return beef; } async findMonitorEventById(id, trx) { return (0, utilityHelpers_1.verifyOneOrNone)(await this.findMonitorEvents({ partial: { id }, trx })); } async relinquishCertificate(auth, args) { const vargs = sdk_1.Validation.validateRelinquishCertificateArgs(args); const cert = (0, utilityHelpers_1.verifyOne)(await this.findCertificates({ partial: { certifier: vargs.certifier, serialNumber: vargs.serialNumber, type: vargs.type } })); return await this.updateCertificate(cert.certificateId, { isDeleted: true }); } async relinquishOutput(auth, args) { const vargs = sdk_1.Validation.validateRelinquishOutputArgs(args); const { txid, vout } = sdk_1.Validation.parseWalletOutpoint(vargs.output); const output = (0, utilityHelpers_1.verifyOne)(await this.findOutputs({ partial: { txid, vout } })); return await this.updateOutput(output.outputId, { basketId: undefined }); } async processSyncChunk(args, chunk) { const user = (0, utilityHelpers_1.verifyTruthy)(await this.findUserByIdentityKey(args.identityKey)); const ss = new entities_1.EntitySyncState((0, utilityHelpers_1.verifyOne)(await this.findSyncStates({ partial: { storageIdentityKey: args.fromStorageIdentityKey, userId: user.userId } }))); const r = await ss.processSyncChunk(this, args, chunk); return r; } /** * Handles storage changes when a valid MerklePath and mined block header are found for a ProvenTxReq txid. * * Performs the following storage updates (typically): * 1. Lookup the exising `ProvenTxReq` record for its rawTx * 2. Insert a new ProvenTx record using properties from `args` and rawTx, yielding a new provenTxId * 3. Update ProvenTxReq record with status 'completed' and new provenTxId value (and history of status changed) * 4. Unpack notify transactionIds from req and update each transaction's status to 'completed', provenTxId value. * 5. Update ProvenTxReq history again to record that transactions have been notified. * 6. Return results... * * Alterations of "typically" to handle: */ async updateProvenTxReqWithNewProvenTx(args) { const req = await entities_1.EntityProvenTxReq.fromStorageId(this, args.provenTxReqId); let proven; if (req.provenTxId) { // Someone beat us to it, grab what we need for results... proven = new entities_1.EntityProvenTx((0, utilityHelpers_1.verifyOne)(await this.findProvenTxs({ partial: { txid: args.txid } }))); } else { let isNew; ({ proven, isNew } = await this.transaction(async (trx) => { const { proven: api, isNew } = await this.findOrInsertProvenTx({ created_at: new Date(), updated_at: new Date(), provenTxId: 0, txid: args.txid, height: args.height, index: args.index, merklePath: args.merklePath, rawTx: req.rawTx, blockHash: args.blockHash, merkleRoot: args.merkleRoot }, trx); proven = new entities_1.EntityProvenTx(api); if (isNew) { req.status = 'completed'; req.provenTxId = proven.provenTxId; await req.updateStorageDynamicProperties(this, trx); // upate the transaction notifications outside of storage transaction.... } return { proven, isNew }; })); if (isNew) { const ids = req.notify.transactionIds || []; if (ids.length > 0) { for (const id of ids) { try { await this.updateTransaction(id, { provenTxId: proven.provenTxId, status: 'completed' }); req.addHistoryNote({ what: 'notifyTxOfProof', transactionId: id }); } catch (eu) { const { code, description } = WalletError_1.WalletError.fromUnknown(eu); const { provenTxId } = proven; req.addHistoryNote({ what: 'notifyTxOfProofError', id, provenTxId, code, description }); } } await req.updateStorageDynamicProperties(this); } } } const r = { status: req.status, history: req.apiHistory, provenTxId: proven.provenTxId }; return r; } /** * For each spendable output in the 'default' basket of the authenticated user, * verify that the output script, satoshis, vout and txid match that of an output * still in the mempool of at least one service provider. * * @returns object with invalidSpendableOutputs array. A good result is an empty array. */ async confirmSpendableOutputs() { const invalidSpendableOutputs = []; const users = await this.findUsers({ partial: {} }); for (const { userId } of users) { const defaultBasket = (0, utilityHelpers_1.verifyOne)(await this.findOutputBaskets({ partial: { userId, name: 'default' } })); const where = { userId, basketId: defaultBasket.basketId, spendable: true }; const outputs = await this.findOutputs({ partial: where }); const services = this.getServices(); for (let i = outputs.length - 1; i >= 0; i--) { const o = outputs[i]; const oid = (0, utilityHelpers_1.verifyId)(o.outputId); if (o.spendable) { let ok = false; if (o.lockingScript && o.lockingScript.length > 0) { const hash = services.hashOutputScript((0, utilityHelpers_noBuffer_1.asString)(o.lockingScript)); const r = await services.getUtxoStatus(hash, undefined, `${o.txid}.${o.vout}`); if (r.isUtxo === true) ok = true; } if (!ok) invalidSpendableOutputs.push(o); } } } return { invalidSpendableOutputs }; } async updateProvenTxReqDynamics(id, update, trx) { const partial = {}; if (update['updated_at']) partial['updated_at'] = update['updated_at']; if (update['provenTxId']) partial['provenTxId'] = update['provenTxId']; if (update['status']) partial['status'] = update['status']; if (Number.isInteger(update['attempts'])) partial['attempts'] = update['attempts']; if (update['notified'] !== undefined) partial['notified'] = update['notified']; if (update['batch']) partial['batch'] = update['batch']; if (update['history']) partial['history'] = update['history']; if (update['notify']) partial['notify'] = update['notify']; return await this.updateProvenTxReq(id, partial, trx); } async extendOutput(o, includeBasket = false, includeTags = false, trx) { const ox = o; if (includeBasket && ox.basketId) ox.basket = await this.findOutputBasketById(o.basketId, trx); if (includeTags) { ox.tags = await this.getTagsForOutputId(o.outputId); } return o; } async validateOutputScript(o, trx) { // without offset and length values return what we have (make no changes) if (!o.scriptLength || !o.scriptOffset || !o.txid) return; // if there is an outputScript and its length is the expected length return what we have. if (o.lockingScript && o.lockingScript.length === o.scriptLength) return; // outputScript is missing or has incorrect length... const script = await this.getRawTxOfKnownValidTransaction(o.txid, o.scriptOffset, o.scriptLength, trx); if (!script) return; o.lockingScript = script; } } exports.StorageProvider = StorageProvider; function validateStorageFeeModel(v) { const r = { model: 'sat/kb', value: 1 }; if (typeof v === 'object') { if (v.model !== 'sat/kb') throw new WERR_errors_1.WERR_INVALID_PARAMETER('StorageFeeModel.model', `"sat/kb"`); if (typeof v.value === 'number') { r.value = v.value; } } return r; } //# sourceMappingURL=StorageProvider.js.map