UNPKG

wallet-storage-client

Version:
443 lines (442 loc) 17.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProvenTxReq = void 0; const index_client_1 = require("../../../index.client"); const _1 = require("."); class ProvenTxReq extends _1.EntityBase { static async fromStorageTxid(storage, txid, trx) { const reqApi = (0, index_client_1.verifyOneOrNone)(await storage.findProvenTxReqs({ partial: { txid }, trx })); if (!reqApi) return undefined; return new ProvenTxReq(reqApi); } static async fromStorageId(storage, id, trx) { const reqApi = (0, index_client_1.verifyOneOrNone)(await storage.findProvenTxReqs({ partial: { provenTxReqId: id }, trx })); if (!reqApi) throw new index_client_1.sdk.WERR_INTERNAL(`proven_tx_reqs with id ${id} is missing.`); return new ProvenTxReq(reqApi); } static fromTxid(txid, rawTx, inputBEEF) { const now = new Date(); return new ProvenTxReq({ 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) { const newApi = (0, index_client_1.verifyOne)(await storage.findProvenTxReqs({ partial: { provenTxReqId: this.id } })); 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 key of Object.keys(notes)) if (key > filter) fh.notes[key] = notes[key]; } return fh; } historyPretty(since, indent = 0) { const h = since ? this.historySince(since) : { ...this.history }; if (!h.notes) return ''; const keyLimit = since ? since.toISOString() : undefined; let log = ''; for (const key of Object.keys(h.notes)) { if (keyLimit && key < keyLimit) continue; h.notes[key] = this.parseHistoryNote(h.notes[key]); log += `${key}: ${h.notes[key]}\n`; } if (log.slice(-1) !== '\n') log += '\n'; 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 key of Object.keys(h.notes)) { this.parseHistoryNote(h.notes[key], summary); } } return summary; } parseHistoryNote(note, summary) { const c = summary || { setToCompleted: false, setToUnmined: false, setToCallback: false, setToDoubleSpend: false, setToSending: false, setToUnconfirmed: false, }; try { const v = JSON.parse(note); switch (v.what) { case "postReqsToNetwork result": { const r = v["result"]; return `posted by ${v["name"]} status=${r.status} txid=${r.txid}`; } break; case "getMerkleProof invalid": { return `getMerkleProof failing after ${v["attempts"]} attempts over ${v["ageInMinutes"]} minutes`; } break; case "ProvenTxReq.set status": { const status = v.new; 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; } return `set status ${v.old} to ${v.new}`; } break; case "notified": return `notified`; default: break; } } catch (_a) { /** */ } return note; } addNotifyTransactionId(id) { if (!Number.isInteger(id)) throw new index_client_1.sdk.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; } addHistoryNote(note, when, noDupes) { if (!this.history.notes) this.history.notes = {}; if (typeof note === 'string') note = JSON.stringify({ what: "string", note }); else note = JSON.stringify(note); when || (when = new Date()); let msecs = when.getTime(); let key = when.toISOString(); if (!noDupes) { while (this.history.notes[key]) { // Make sure new key (timestamp) will not overwrite existing. // Fudge the time by 1 msec forward until unique msecs += 1; key = new Date(msecs).toISOString(); } } if (!this.history.notes[key]) this.history.notes[key] = note; } /** * Updates database record with current state of this entity. * @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 index_client_1.entity.ProvenTxReq(reqApi1); } else { const req = new ProvenTxReq(reqApi1); req.mergeNotifyTransactionIds(reqApi0); req.mergeHistory(reqApi0); 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: "ProvenTxReq.set status", old: this.api.status, new: 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, index_client_1.arraysEqual)(eo.rawTx, ei.rawTx) || !eo.inputBEEF && ei.inputBEEF || eo.inputBEEF && !ei.inputBEEF || eo.inputBEEF && ei.inputBEEF && !(0, index_client_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, index_client_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, index_client_1.verifyOneOrNone)(await storage.findProvenTxReqs({ partial: { txid: ei.txid }, trx })); return { found: !!ef, eo: new ProvenTxReq(ef || { ...ei }), eiId: (0, index_client_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 ProvenTxReq(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 ProvenTxReq(ei); if (eie.history.notes) { for (const [k, v] of Object.entries(eie.history.notes)) { this.addHistoryNote(v, new Date(k), noDupes); } } } static isTerminalStatus(status) { return index_client_1.sdk.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 index_client_1.sdk.WERR_INTERNAL('ProvenTxReq merge batch not equal.'); this.mergeHistory(ei, syncMap); this.mergeNotifyTransactionIds(ei, syncMap); this.updated_at = new Date(); await storage.updateProvenTxReq(this.id, this.toApi(), trx); return false; } } exports.ProvenTxReq = ProvenTxReq; //# sourceMappingURL=ProvenTxReq.js.map