@bsv/wallet-toolbox-client
Version:
Client only Wallet Storage
320 lines • 12.8 kB
JavaScript
;
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