UNPKG

@bsv/wallet-toolbox-client

Version:
524 lines (523 loc) 18 kB
"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