wallet-storage-client
Version:
Client only Wallet Storage
443 lines (442 loc) • 17.1 kB
JavaScript
"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