UNPKG

@bsv/wallet-toolbox-client

Version:
320 lines 12.8 kB
"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.attemptToPostReqsToNetwork = attemptToPostReqsToNetwork; const sdk_1 = require("@bsv/sdk"); const sdk = __importStar(require("../../sdk")); const utilityHelpers_1 = require("../../utility/utilityHelpers"); /** * Attempt to post one or more `ProvenTxReq` with status 'unsent' * to the bitcoin network. * * @param reqs */ async function attemptToPostReqsToNetwork(storage, reqs, trx) { // initialize results, validate reqs ready to post, txids are of the transactions in the beef that we care about. const { r, vreqs, txids } = await validateReqsAndMergeBeefs(storage, reqs, trx); const services = storage.getServices(); const pbrs = await services.postBeef(r.beef, txids); // post beef results (pbrs) is an array by service provider // for each service provider, there's an aggregate result and individual results by txid. await transferNotesToReqHistories(txids, vreqs, pbrs, storage, trx); const apbrs = aggregatePostBeefResultsByTxid(txids, vreqs, pbrs); await updateReqsFromAggregateResults(txids, r, apbrs, storage, services, trx); return r; } async function validateReqsAndMergeBeefs(storage, reqs, trx) { const r = { status: 'success', beef: new sdk_1.Beef(), details: [], log: '' }; const vreqs = []; for (const req of reqs) { try { const noRawTx = !req.rawTx; const noTxIds = !req.notify.transactionIds || req.notify.transactionIds.length < 1; const noInputBEEF = !req.inputBEEF; if (noRawTx || noTxIds || noInputBEEF) { // This should have happened earlier... req.addHistoryNote({ when: new Date().toISOString(), what: 'validateReqFailed', noRawTx, noTxIds, noInputBEEF }); req.status = 'invalid'; await req.updateStorageDynamicProperties(storage, trx); r.details.push({ txid: req.txid, req, status: 'invalid' }); } else { const vreq = { txid: req.txid, req, status: 'unknown' }; await storage.mergeReqToBeefToShareExternally(req.api, r.beef, [], trx); vreqs.push(vreq); r.details.push(vreq); } } catch (eu) { const { code, message } = sdk.WalletError.fromUnknown(eu); req.addHistoryNote({ when: new Date().toISOString(), what: 'validateReqError', txid: req.txid, code, message }); req.attempts++; if (req.attempts > 6) { req.status = 'invalid'; r.details.push({ txid: req.txid, req, status: 'invalid' }); } await req.updateStorageDynamicProperties(storage, trx); } } return { r, vreqs, txids: vreqs.map(r => r.txid) }; } async function transferNotesToReqHistories(txids, vreqs, pbrs, storage, trx) { for (const txid of txids) { const vreq = vreqs.find(r => r.txid === txid); if (!vreq) throw new sdk.WERR_INTERNAL(); const notes = []; for (const pbr of pbrs) { notes.push(...(pbr.notes || [])); const r = pbr.txidResults.find(tr => tr.txid === txid); if (r) notes.push(...(r.notes || [])); } for (const n of notes) { vreq.req.addHistoryNote(n); } await vreq.req.updateStorageDynamicProperties(storage, trx); } } /** * For each txid, decide on the aggregate success or failure of attempting to broadcast it to the bitcoin processing network. * * Possible results: * 1. Success: At least one success, no double spends. * 2. DoubleSpend: One or more double spends. * 3. InvalidTransaction: No success, no double spend, one or more non-exception errors. * 4. Service Failure: No results or all results are exception errors. * * @param txids * @param reqs * @param pbrs * @param storage * @returns */ function aggregatePostBeefResultsByTxid(txids, vreqs, pbrs) { const r = {}; for (const txid of txids) { const vreq = vreqs.find(r => r.txid === txid); const ar = { txid, vreq, txidResults: [], status: 'success', successCount: 0, doubleSpendCount: 0, statusErrorCount: 0, serviceErrorCount: 0, competingTxs: [] }; r[txid] = ar; for (const pbr of pbrs) { const tr = pbr.txidResults.find(tr => tr.txid === txid); if (tr) { ar.txidResults.push(tr); if (tr.status === 'success') ar.successCount++; else if (tr.doubleSpend) { ar.doubleSpendCount++; if (tr.competingTxs) { ar.competingTxs = [...tr.competingTxs]; } } else if (tr.serviceError) ar.serviceErrorCount++; else ar.statusErrorCount++; } if (ar.competingTxs.length > 1) ar.competingTxs = [...new Set(ar.competingTxs)]; // Remove duplicates } if (ar.successCount > 0 && ar.doubleSpendCount === 0) ar.status = 'success'; else if (ar.doubleSpendCount > 0) ar.status = 'doubleSpend'; else if (ar.statusErrorCount > 0) ar.status = 'invalidTx'; else ar.status = 'serviceError'; } return r; } /** * For each txid in submitted `txids`: * * Based on its aggregate status, and whether broadcast happening in background (isDelayed) or immediately (!isDelayed), * and iff current req.status is not 'unproven' or 'completed': * * 'success': * req.status => 'unmined', tx.status => 'unproven' * 'doubleSpend': * req.status => 'doubleSpend', tx.status => 'failed' * 'invalidTx': * req.status => 'invalid', tx.status => 'failed' * 'serviceError': * increment req.attempts * * @param txids * @param apbrs * @param storage * @param services if valid, doubleSpend results will be verified (but only if not within a trx. e.g. trx must be undefined) * @param trx */ async function updateReqsFromAggregateResults(txids, r, apbrs, storage, services, trx) { for (const txid of txids) { const ar = apbrs[txid]; const req = ar.vreq.req; await req.refreshFromStorage(storage, trx); const { successCount, doubleSpendCount, statusErrorCount, serviceErrorCount } = ar; const note = { when: new Date().toISOString(), what: 'aggregateResults', reqStatus: req.status, aggStatus: ar.status, attempts: req.attempts, successCount, doubleSpendCount, statusErrorCount, serviceErrorCount }; if (['completed', 'unmined'].indexOf(req.status) >= 0) // However it happened, don't degrade status if it is somehow already beyond broadcast stage continue; if (ar.status === 'doubleSpend' && services && !trx) await confirmDoubleSpend(ar, r.beef, storage, services); let newReqStatus = undefined; let newTxStatus = undefined; switch (ar.status) { case 'success': newReqStatus = 'unmined'; newTxStatus = 'unproven'; break; case 'doubleSpend': newReqStatus = 'doubleSpend'; newTxStatus = 'failed'; break; case 'invalidTx': newReqStatus = 'invalid'; newTxStatus = 'failed'; break; case 'serviceError': newReqStatus = 'sending'; newTxStatus = 'sending'; req.attempts++; break; default: throw new sdk.WERR_INTERNAL(`unimplemented AggregateStatus ${ar.status}`); } note.newReqStatus = newReqStatus; note.newTxStatus = newTxStatus; note.newAttempts = req.attempts; if (newReqStatus) req.status = newReqStatus; req.addHistoryNote(note); await req.updateStorageDynamicProperties(storage, trx); if (newTxStatus) { const ids = req.notify.transactionIds; if (ids) { // Also set generated outputs to spendable false and consumed input outputs to spendable true (and clears their spentBy). await storage.updateTransactionsStatus(ids, newTxStatus, trx); } } // Transfer critical results to details going back to the user const details = r.details.find(d => d.txid === txid); details.status = ar.status; details.competingTxs = ar.competingTxs; } } /** * Requires ar.status === 'doubleSpend' * * Parse the rawTx and review each input as a possible double spend. * * If all inputs appear to be unspent, update aggregate status to 'success' if successCount > 0, otherwise 'serviceError'. * * @param ar * @param storage * @param services */ async function confirmDoubleSpend(ar, beef, storage, services) { var _a, _b; const req = ar.vreq.req; const note = { when: new Date().toISOString(), what: 'confirmDoubleSpend' }; let known = false; for (let retry = 0; retry < 3; retry++) { const gsr = await services.getStatusForTxids([req.txid]); note[`getStatus${retry}`] = `${gsr.status}${gsr.error ? `${gsr.error.code}` : ''},${(_a = gsr.results[0]) === null || _a === void 0 ? void 0 : _a.status}`; if (gsr.status === 'success' && gsr.results[0].status !== 'unknown') { known = true; break; } else { await (0, utilityHelpers_1.wait)(1000); } } if (known) { // doubleSpend -> success ar.status = 'success'; note.newStatus = ar.status; } else { // Confirmed double spend, get txids of possible competing transactions. const tx = sdk_1.Transaction.fromBinary(req.rawTx); const competingTxids = new Set(ar.competingTxs); for (const input of tx.inputs) { const sourceTx = (_b = beef.findTxid(input.sourceTXID)) === null || _b === void 0 ? void 0 : _b.tx; if (!sourceTx) throw new sdk.WERR_INTERNAL(`beef lacks tx for ${input.sourceTXID}`); const lockingScript = sourceTx.outputs[input.sourceOutputIndex].lockingScript.toHex(); const hash = services.hashOutputScript(lockingScript); const shhrs = await services.getScriptHashHistory(hash); if (shhrs.status === 'success') { for (const h of shhrs.history) { // Neither the source of the input nor the current transaction are competition. if (h.txid !== input.sourceTXID && h.txid !== ar.txid) competingTxids.add(h.txid); } } } ar.competingTxs = [...competingTxids].slice(-24); // keep at most 24, if they were sorted by time, keep newest note.competingTxs = ar.competingTxs.join(','); } req.addHistoryNote(note); } //# sourceMappingURL=attemptToPostReqsToNetwork.js.map