@bsv/wallet-toolbox-client
Version:
Client only Wallet Storage
524 lines (523 loc) • 18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EntityProvenTxReq = void 0;
const types_1 = require("../../../sdk/types");
const WERR_errors_1 = require("../../../sdk/WERR_errors");
const utilityHelpers_1 = require("../../../utility/utilityHelpers");
const EntityBase_1 = require("./EntityBase");
class EntityProvenTxReq extends EntityBase_1.EntityBase {
static async fromStorageTxid(storage, txid, trx) {
const reqApi = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findProvenTxReqs({ partial: { txid }, trx }));
if (!reqApi)
return undefined;
return new EntityProvenTxReq(reqApi);
}
static async fromStorageId(storage, id, trx) {
const reqApi = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findProvenTxReqs({ partial: { provenTxReqId: id }, trx }));
if (!reqApi)
throw new WERR_errors_1.WERR_INTERNAL(`proven_tx_reqs with id ${id} is missing.`);
return new EntityProvenTxReq(reqApi);
}
static fromTxid(txid, rawTx, inputBEEF) {
const now = new Date();
return new EntityProvenTxReq({
provenTxReqId: 0,
created_at: now,
updated_at: now,
txid,
inputBEEF,
rawTx,
status: 'unknown',
history: '{}',
notify: '{}',
attempts: 0,
notified: false
});
}
packApiHistory() {
this.api.history = JSON.stringify(this.history);
}
packApiNotify() {
this.api.notify = JSON.stringify(this.notify);
}
unpackApiHistory() {
this.history = JSON.parse(this.api.history);
}
unpackApiNotify() {
this.notify = JSON.parse(this.api.notify);
}
get apiHistory() {
this.packApiHistory();
return this.api.history;
}
get apiNotify() {
this.packApiNotify();
return this.api.notify;
}
set apiHistory(v) {
this.api.history = v;
this.unpackApiHistory();
}
set apiNotify(v) {
this.api.notify = v;
this.unpackApiNotify();
}
updateApi() {
this.packApiHistory();
this.packApiNotify();
}
unpackApi() {
this.unpackApiHistory();
this.unpackApiNotify();
if (this.notify.transactionIds) {
// Cleanup null values and duplicates.
const transactionIds = [];
for (const id of this.notify.transactionIds) {
if (Number.isInteger(id) && !transactionIds.some(txid => txid === id))
transactionIds.push(id);
}
this.notify.transactionIds = transactionIds;
}
}
async refreshFromStorage(storage, trx) {
const newApi = (0, utilityHelpers_1.verifyOne)(await storage.findProvenTxReqs({ partial: { provenTxReqId: this.id }, trx }));
this.api = newApi;
this.unpackApi();
}
constructor(api) {
const now = new Date();
super(api || {
provenTxReqId: 0,
created_at: now,
updated_at: now,
txid: '',
rawTx: [],
history: '',
notify: '',
attempts: 0,
status: 'unknown',
notified: false
});
this.history = {};
this.notify = {};
this.unpackApi();
}
/**
* Returns history to only what followed since date.
*/
historySince(since) {
const fh = { notes: [] };
const filter = since.toISOString();
const notes = this.history.notes;
if (notes && fh.notes) {
for (const note of notes)
if (note.when && note.when > filter)
fh.notes.push(note);
}
return fh;
}
historyPretty(since, indent = 0) {
const h = since ? this.historySince(since) : { ...this.history };
if (!h.notes)
return '';
const whenLimit = since ? since.toISOString() : undefined;
let log = '';
for (const note of h.notes) {
if (whenLimit && note.when && note.when < whenLimit)
continue;
log += this.prettyNote(note) + '\n';
}
return log;
}
prettyNote(note) {
let log = `${note.when}: ${note.what}`;
for (const [key, val] of Object.entries(note)) {
if (key !== 'when' && key !== 'what') {
if (typeof val === 'string')
log += ' ' + key + ':`' + val + '`';
else
log += ' ' + key + ':' + val;
}
}
return log;
}
getHistorySummary() {
const summary = {
setToCompleted: false,
setToUnmined: false,
setToCallback: false,
setToDoubleSpend: false,
setToSending: false,
setToUnconfirmed: false
};
const h = this.history;
if (h.notes) {
for (const note of h.notes) {
this.parseHistoryNote(note, summary);
}
}
return summary;
}
parseHistoryNote(note, summary) {
const c = summary || {
setToCompleted: false,
setToUnmined: false,
setToCallback: false,
setToDoubleSpend: false,
setToSending: false,
setToUnconfirmed: false
};
let n = this.prettyNote(note);
try {
switch (note.what) {
case 'status':
{
const status = note.status_now;
switch (status) {
case 'completed':
c.setToCompleted = true;
break;
case 'unmined':
c.setToUnmined = true;
break;
case 'callback':
c.setToCallback = true;
break;
case 'doubleSpend':
c.setToDoubleSpend = true;
break;
case 'sending':
c.setToSending = true;
break;
case 'unconfirmed':
c.setToUnconfirmed = true;
break;
default:
break;
}
}
break;
default:
break;
}
}
catch (_a) {
/** */
}
return n;
}
addNotifyTransactionId(id) {
if (!Number.isInteger(id))
throw new WERR_errors_1.WERR_INVALID_PARAMETER('id', 'integer');
const s = new Set(this.notify.transactionIds || []);
s.add(id);
this.notify.transactionIds = [...s].sort((a, b) => (a > b ? 1 : a < b ? -1 : 0));
this.notified = false;
}
/**
* Adds a note to history.
* Notes with identical property values to an existing note are ignored.
* @param note Note to add
* @param noDupes if true, only newest note with same `what` value is retained.
*/
addHistoryNote(note, noDupes) {
if (!this.history.notes)
this.history.notes = [];
if (!note.when)
note.when = new Date().toISOString();
if (noDupes) {
// Remove any existing notes with same 'what' value and either no 'when' or an earlier 'when'
this.history.notes = this.history.notes.filter(n => n.what !== note.what || (n.when && n.when > note.when));
}
let addNote = true;
for (const n of this.history.notes) {
let isEqual = true;
for (const [k, v] of Object.entries(n)) {
if (v !== note[k]) {
isEqual = false;
break;
}
}
if (isEqual)
addNote = false;
if (!addNote)
break;
}
if (addNote) {
this.history.notes.push(note);
const k = (n) => {
return `${n.when} ${n.what}`;
};
this.history.notes.sort((a, b) => (k(a) < k(b) ? -1 : k(a) > k(b) ? 1 : 0));
}
}
/**
* Updates database record with current state of this EntityUser
* @param storage
* @param trx
*/
async updateStorage(storage, trx) {
this.updated_at = new Date();
this.updateApi();
if (this.id === 0) {
await storage.insertProvenTxReq(this.api);
}
const update = { ...this.api };
await storage.updateProvenTxReq(this.id, update, trx);
}
/**
* Update storage with changes to non-static properties:
* updated_at
* provenTxId
* status
* history
* notify
* notified
* attempts
* batch
*
* @param storage
* @param trx
*/
async updateStorageDynamicProperties(storage, trx) {
this.updated_at = new Date();
this.updateApi();
const update = {
updated_at: this.api.updated_at,
provenTxId: this.api.provenTxId,
status: this.api.status,
history: this.api.history,
notify: this.api.notify,
notified: this.api.notified,
attempts: this.api.attempts,
batch: this.api.batch
};
if (storage.isStorageProvider()) {
const sp = storage;
await sp.updateProvenTxReqDynamics(this.id, update, trx);
}
else {
const wsm = storage;
await wsm.runAsStorageProvider(async (sp) => {
await sp.updateProvenTxReqDynamics(this.id, update, trx);
});
}
}
async insertOrMerge(storage, trx) {
const req = await storage.transaction(async (trx) => {
let reqApi0 = this.toApi();
const { req: reqApi1, isNew } = await storage.findOrInsertProvenTxReq(reqApi0, trx);
if (isNew) {
return new EntityProvenTxReq(reqApi1);
}
else {
const req = new EntityProvenTxReq(reqApi1);
req.mergeNotifyTransactionIds(reqApi0);
req.mergeHistory(reqApi0, undefined, true);
await req.updateStorage(storage, trx);
return req;
}
}, trx);
return req;
}
/**
* See `ProvenTxReqStatusApi`
*/
get status() {
return this.api.status;
}
set status(v) {
if (v !== this.api.status) {
this.addHistoryNote({ what: 'status', status_was: this.api.status, status_now: v });
this.api.status = v;
}
}
get provenTxReqId() {
return this.api.provenTxReqId;
}
set provenTxReqId(v) {
this.api.provenTxReqId = v;
}
get created_at() {
return this.api.created_at;
}
set created_at(v) {
this.api.created_at = v;
}
get updated_at() {
return this.api.updated_at;
}
set updated_at(v) {
this.api.updated_at = v;
}
get txid() {
return this.api.txid;
}
set txid(v) {
this.api.txid = v;
}
get inputBEEF() {
return this.api.inputBEEF;
}
set inputBEEF(v) {
this.api.inputBEEF = v;
}
get rawTx() {
return this.api.rawTx;
}
set rawTx(v) {
this.api.rawTx = v;
}
get attempts() {
return this.api.attempts;
}
set attempts(v) {
this.api.attempts = v;
}
get provenTxId() {
return this.api.provenTxId;
}
set provenTxId(v) {
this.api.provenTxId = v;
}
get notified() {
return this.api.notified;
}
set notified(v) {
this.api.notified = v;
}
get batch() {
return this.api.batch;
}
set batch(v) {
this.api.batch = v;
}
get id() {
return this.api.provenTxReqId;
}
set id(v) {
this.api.provenTxReqId = v;
}
get entityName() {
return 'provenTxReq';
}
get entityTable() {
return 'proven_tx_reqs';
}
/**
* 'convergent' equality must satisfy (A sync B) equals (B sync A)
*/
equals(ei, syncMap) {
const eo = this.toApi();
if (eo.txid != ei.txid ||
!(0, utilityHelpers_1.arraysEqual)(eo.rawTx, ei.rawTx) ||
(!eo.inputBEEF && ei.inputBEEF) ||
(eo.inputBEEF && !ei.inputBEEF) ||
(eo.inputBEEF && ei.inputBEEF && !(0, utilityHelpers_1.arraysEqual)(eo.inputBEEF, ei.inputBEEF)) ||
eo.batch != ei.batch)
return false;
if (syncMap) {
if (
// attempts doesn't matter for convergent equality
// history doesn't matter for convergent equality
// only local transactionIds matter, that cared about this txid in sorted order
eo.provenTxReqId !== syncMap.provenTxReq.idMap[(0, utilityHelpers_1.verifyId)(ei.provenTxReqId)] ||
(!eo.provenTxId && ei.provenTxId) ||
(eo.provenTxId && !ei.provenTxId) ||
(ei.provenTxId && eo.provenTxId !== syncMap.provenTx.idMap[ei.provenTxId])
// || eo.created_at !== minDate(ei.created_at, eo.created_at)
// || eo.updated_at !== maxDate(ei.updated_at, eo.updated_at)
)
return false;
}
else {
if (eo.attempts != ei.attempts ||
eo.history != ei.history ||
eo.notify != ei.notify ||
eo.provenTxReqId !== ei.provenTxReqId ||
eo.provenTxId !== ei.provenTxId
// || eo.created_at !== ei.created_at
// || eo.updated_at !== ei.updated_at
)
return false;
}
return true;
}
static async mergeFind(storage, userId, ei, syncMap, trx) {
const ef = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findProvenTxReqs({ partial: { txid: ei.txid }, trx }));
return {
found: !!ef,
eo: new EntityProvenTxReq(ef || { ...ei }),
eiId: (0, utilityHelpers_1.verifyId)(ei.provenTxReqId)
};
}
mapNotifyTransactionIds(syncMap) {
// Map external notification transaction ids to local ids
const externalIds = this.notify.transactionIds || [];
this.notify.transactionIds = [];
for (const transactionId of externalIds) {
const localTxId = syncMap.transaction.idMap[transactionId];
if (localTxId) {
this.addNotifyTransactionId(localTxId);
}
}
}
mergeNotifyTransactionIds(ei, syncMap) {
var _a;
// Map external notification transaction ids to local ids and merge them if they exist.
const eie = new EntityProvenTxReq(ei);
if (eie.notify.transactionIds) {
(_a = this.notify).transactionIds || (_a.transactionIds = []);
for (const transactionId of eie.notify.transactionIds) {
const localTxId = syncMap ? syncMap.transaction.idMap[transactionId] : transactionId;
if (localTxId) {
this.addNotifyTransactionId(localTxId);
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
mergeHistory(ei, syncMap, noDupes) {
const eie = new EntityProvenTxReq(ei);
if (eie.history.notes) {
for (const note of eie.history.notes) {
this.addHistoryNote(note);
}
}
}
static isTerminalStatus(status) {
return types_1.ProvenTxReqTerminalStatus.some(s => s === status);
}
async mergeNew(storage, userId, syncMap, trx) {
if (this.provenTxId)
this.provenTxId = syncMap.provenTx.idMap[this.provenTxId];
this.mapNotifyTransactionIds(syncMap);
this.provenTxReqId = 0;
this.provenTxReqId = await storage.insertProvenTxReq(this.toApi(), trx);
}
/**
* When merging `ProvenTxReq`, care is taken to avoid short-cirtuiting notification: `status` must not transition to `completed` without
* passing through `notifying`. Thus a full convergent merge passes through these sequence steps:
* 1. Remote storage completes before local storage.
* 2. The remotely completed req and ProvenTx sync to local storage.
* 3. The local storage transitions to `notifying`, after merging the remote attempts and history.
* 4. The local storage notifies, transitioning to `completed`.
* 5. Having been updated, the local req, but not ProvenTx sync to remote storage, but do not merge because the earlier `completed` wins.
* 6. Convergent equality is achieved (completing work - history and attempts are equal)
*
* On terminal failure: `doubleSpend` trumps `invalid` as it contains more data.
*/
async mergeExisting(storage, since, ei, syncMap, trx) {
if (!this.batch && ei.batch)
this.batch = ei.batch;
else if (this.batch && ei.batch && this.batch !== ei.batch)
throw new WERR_errors_1.WERR_INTERNAL('ProvenTxReq merge batch not equal.');
this.mergeHistory(ei, syncMap, true);
this.mergeNotifyTransactionIds(ei, syncMap);
this.updated_at = new Date(Math.max(ei.updated_at.getTime(), this.updated_at.getTime()));
await storage.updateProvenTxReq(this.id, this.toApi(), trx);
return false;
}
}
exports.EntityProvenTxReq = EntityProvenTxReq;
//# sourceMappingURL=EntityProvenTxReq.js.map