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