@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
1,178 lines • 58.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageKnex = void 0;
const tables_1 = require("./schema/tables");
const KnexMigrations_1 = require("./schema/KnexMigrations");
const StorageProvider_1 = require("./StorageProvider");
const purgeData_1 = require("./methods/purgeData");
const listActionsKnex_1 = require("./methods/listActionsKnex");
const listOutputsKnex_1 = require("./methods/listOutputsKnex");
const reviewStatus_1 = require("./methods/reviewStatus");
const WERR_errors_1 = require("../sdk/WERR_errors");
const utilityHelpers_1 = require("../utility/utilityHelpers");
class StorageKnex extends StorageProvider_1.StorageProvider {
constructor(options) {
super(options);
this._verifiedReadyForDatabaseAccess = false;
if (!options.knex)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('options.knex', `valid`);
this.knex = options.knex;
}
async readSettings() {
return this.validateEntity((0, utilityHelpers_1.verifyOne)(await this.toDb(undefined)('settings')));
}
async getProvenOrRawTx(txid, trx) {
const k = this.toDb(trx);
const r = {
proven: undefined,
rawTx: undefined,
inputBEEF: undefined
};
r.proven = (0, utilityHelpers_1.verifyOneOrNone)(await this.findProvenTxs({ partial: { txid: txid } }));
if (!r.proven) {
const reqRawTx = (0, utilityHelpers_1.verifyOneOrNone)(await k('proven_tx_reqs')
.where('txid', txid)
.whereIn('status', ['unsent', 'unmined', 'unconfirmed', 'sending', 'nosend', 'completed'])
.select('rawTx', 'inputBEEF'));
if (reqRawTx) {
r.rawTx = Array.from(reqRawTx.rawTx);
r.inputBEEF = Array.from(reqRawTx.inputBEEF);
}
}
return r;
}
dbTypeSubstring(source, fromOffset, forLength) {
if (this.dbtype === 'MySQL')
return `substring(${source} from ${fromOffset} for ${forLength})`;
return `substr(${source}, ${fromOffset}, ${forLength})`;
}
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
if (!txid)
return undefined;
if (!this.isAvailable())
await this.makeAvailable();
let rawTx = undefined;
if (Number.isInteger(offset) && Number.isInteger(length)) {
let rs = await this.toDb(trx).raw(`select ${this.dbTypeSubstring('rawTx', offset + 1, length)} as rawTx from proven_txs where txid = '${txid}'`);
if (this.dbtype === 'MySQL')
rs = rs[0];
const r = (0, utilityHelpers_1.verifyOneOrNone)(rs);
if (r && r.rawTx) {
rawTx = Array.from(r.rawTx);
}
else {
let rs = await this.toDb(trx).raw(`select ${this.dbTypeSubstring('rawTx', offset + 1, length)} as rawTx from proven_tx_reqs where txid = '${txid}' and status in ('unsent', 'nosend', 'sending', 'unmined', 'completed', 'unfail')`);
if (this.dbtype === 'MySQL')
rs = rs[0];
const r = (0, utilityHelpers_1.verifyOneOrNone)(rs);
if (r && r.rawTx) {
rawTx = Array.from(r.rawTx);
}
}
}
else {
const r = await this.getProvenOrRawTx(txid, trx);
if (r.proven)
rawTx = r.proven.rawTx;
else
rawTx = r.rawTx;
}
return rawTx;
}
getProvenTxsForUserQuery(args) {
const k = this.toDb(args.trx);
let q = k('proven_txs').where(function () {
this.whereExists(k
.select('*')
.from('transactions')
.whereRaw(`proven_txs.provenTxId = transactions.provenTxId and transactions.userId = ${args.userId}`));
});
if (args.paged) {
q = q.limit(args.paged.limit);
q = q.offset(args.paged.offset || 0);
}
if (args.since)
q = q.where('updated_at', '>=', this.validateDateForWhere(args.since));
return q;
}
async getProvenTxsForUser(args) {
const q = this.getProvenTxsForUserQuery(args);
const rs = await q;
return this.validateEntities(rs);
}
getProvenTxReqsForUserQuery(args) {
const k = this.toDb(args.trx);
let q = k('proven_tx_reqs').where(function () {
this.whereExists(k
.select('*')
.from('transactions')
.whereRaw(`proven_tx_reqs.txid = transactions.txid and transactions.userId = ${args.userId}`));
});
if (args.paged) {
q = q.limit(args.paged.limit);
q = q.offset(args.paged.offset || 0);
}
if (args.since)
q = q.where('updated_at', '>=', this.validateDateForWhere(args.since));
return q;
}
async getProvenTxReqsForUser(args) {
const q = this.getProvenTxReqsForUserQuery(args);
const rs = await q;
return this.validateEntities(rs, undefined, ['notified']);
}
getTxLabelMapsForUserQuery(args) {
const k = this.toDb(args.trx);
let q = k('tx_labels_map').whereExists(k
.select('*')
.from('tx_labels')
.whereRaw(`tx_labels.txLabelId = tx_labels_map.txLabelId and tx_labels.userId = ${args.userId}`));
if (args.since)
q = q.where('updated_at', '>=', this.validateDateForWhere(args.since));
if (args.paged) {
q = q.limit(args.paged.limit);
q = q.offset(args.paged.offset || 0);
}
return q;
}
async getTxLabelMapsForUser(args) {
const q = this.getTxLabelMapsForUserQuery(args);
const rs = await q;
return this.validateEntities(rs, undefined, ['isDeleted']);
}
getOutputTagMapsForUserQuery(args) {
const k = this.toDb(args.trx);
let q = k('output_tags_map').whereExists(k
.select('*')
.from('output_tags')
.whereRaw(`output_tags.outputTagId = output_tags_map.outputTagId and output_tags.userId = ${args.userId}`));
if (args.since)
q = q.where('updated_at', '>=', this.validateDateForWhere(args.since));
if (args.paged) {
q = q.limit(args.paged.limit);
q = q.offset(args.paged.offset || 0);
}
return q;
}
async getOutputTagMapsForUser(args) {
const q = this.getOutputTagMapsForUserQuery(args);
const rs = await q;
return this.validateEntities(rs, undefined, ['isDeleted']);
}
async listActions(auth, vargs) {
if (!auth.userId)
throw new WERR_errors_1.WERR_UNAUTHORIZED();
return await (0, listActionsKnex_1.listActions)(this, auth, vargs);
}
async listOutputs(auth, vargs) {
if (!auth.userId)
throw new WERR_errors_1.WERR_UNAUTHORIZED();
return await (0, listOutputsKnex_1.listOutputs)(this, auth, vargs);
}
async insertProvenTx(tx, trx) {
const e = await this.validateEntityForInsert(tx, trx);
if (e.provenTxId === 0)
delete e.provenTxId;
const [id] = await this.toDb(trx)('proven_txs').insert(e);
tx.provenTxId = id;
return tx.provenTxId;
}
async insertProvenTxReq(tx, trx) {
const e = await this.validateEntityForInsert(tx, trx);
if (e.provenTxReqId === 0)
delete e.provenTxReqId;
const [id] = await this.toDb(trx)('proven_tx_reqs').insert(e);
tx.provenTxReqId = id;
return tx.provenTxReqId;
}
async insertUser(user, trx) {
const e = await this.validateEntityForInsert(user, trx);
if (e.userId === 0)
delete e.userId;
const [id] = await this.toDb(trx)('users').insert(e);
user.userId = id;
return user.userId;
}
async insertCertificateAuth(auth, certificate) {
if (!auth.userId || (certificate.userId && certificate.userId !== auth.userId))
throw new WERR_errors_1.WERR_UNAUTHORIZED();
certificate.userId = auth.userId;
return await this.insertCertificate(certificate);
}
async insertCertificate(certificate, trx) {
const e = await this.validateEntityForInsert(certificate, trx, undefined, ['isDeleted']);
if (e.certificateId === 0)
delete e.certificateId;
const logger = e.logger;
if (e.logger)
delete e.logger;
const fields = e.fields;
if (e.fields)
delete e.fields;
const [id] = await this.toDb(trx)('certificates').insert(e);
certificate.certificateId = id;
if (fields) {
for (const field of fields) {
field.certificateId = id;
field.userId = certificate.userId;
await this.insertCertificateField(field, trx);
}
}
return certificate.certificateId;
}
async insertCertificateField(certificateField, trx) {
const e = await this.validateEntityForInsert(certificateField, trx);
await this.toDb(trx)('certificate_fields').insert(e);
}
async insertOutputBasket(basket, trx) {
const e = await this.validateEntityForInsert(basket, trx, undefined, ['isDeleted']);
if (e.basketId === 0)
delete e.basketId;
const [id] = await this.toDb(trx)('output_baskets').insert(e);
basket.basketId = id;
return basket.basketId;
}
async insertTransaction(tx, trx) {
const e = await this.validateEntityForInsert(tx, trx);
if (e.transactionId === 0)
delete e.transactionId;
const [id] = await this.toDb(trx)('transactions').insert(e);
tx.transactionId = id;
return tx.transactionId;
}
async insertCommission(commission, trx) {
const e = await this.validateEntityForInsert(commission, trx);
if (e.commissionId === 0)
delete e.commissionId;
const [id] = await this.toDb(trx)('commissions').insert(e);
commission.commissionId = id;
return commission.commissionId;
}
async insertOutput(output, trx) {
try {
const e = await this.validateEntityForInsert(output, trx);
if (e.outputId === 0)
delete e.outputId;
const [id] = await this.toDb(trx)('outputs').insert(e);
output.outputId = id;
return output.outputId;
}
catch (e) {
throw e;
}
}
async insertOutputTag(tag, trx) {
const e = await this.validateEntityForInsert(tag, trx, undefined, ['isDeleted']);
if (e.outputTagId === 0)
delete e.outputTagId;
const [id] = await this.toDb(trx)('output_tags').insert(e);
tag.outputTagId = id;
return tag.outputTagId;
}
async insertOutputTagMap(tagMap, trx) {
const e = await this.validateEntityForInsert(tagMap, trx, undefined, ['isDeleted']);
const [id] = await this.toDb(trx)('output_tags_map').insert(e);
}
async insertTxLabel(label, trx) {
const e = await this.validateEntityForInsert(label, trx, undefined, ['isDeleted']);
if (e.txLabelId === 0)
delete e.txLabelId;
const [id] = await this.toDb(trx)('tx_labels').insert(e);
label.txLabelId = id;
return label.txLabelId;
}
async insertTxLabelMap(labelMap, trx) {
const e = await this.validateEntityForInsert(labelMap, trx, undefined, ['isDeleted']);
const [id] = await this.toDb(trx)('tx_labels_map').insert(e);
}
async insertMonitorEvent(event, trx) {
const e = await this.validateEntityForInsert(event, trx);
if (e.id === 0)
delete e.id;
const [id] = await this.toDb(trx)('monitor_events').insert(e);
event.id = id;
return event.id;
}
async insertSyncState(syncState, trx) {
const e = await this.validateEntityForInsert(syncState, trx, ['when'], ['init']);
if (e.syncStateId === 0)
delete e.syncStateId;
const [id] = await this.toDb(trx)('sync_states').insert(e);
syncState.syncStateId = id;
return syncState.syncStateId;
}
async updateCertificateField(certificateId, fieldName, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('certificate_fields')
.where({ certificateId, fieldName })
.update(this.validatePartialForUpdate(update));
}
async updateCertificate(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('certificates')
.where({ certificateId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']));
}
async updateCommission(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('commissions')
.where({ commissionId: id })
.update(this.validatePartialForUpdate(update));
}
async updateOutputBasket(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('output_baskets')
.where({ basketId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']));
}
async updateOutput(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('outputs')
.where({ outputId: id })
.update(this.validatePartialForUpdate(update));
}
async updateOutputTagMap(outputId, tagId, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('output_tags_map')
.where({ outputId, outputTagId: tagId })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']));
}
async updateOutputTag(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('output_tags')
.where({ outputTagId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']));
}
async updateProvenTxReq(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
let r;
if (Array.isArray(id)) {
r = await this.toDb(trx)('proven_tx_reqs')
.whereIn('provenTxReqId', id)
.update(this.validatePartialForUpdate(update));
}
else if (Number.isInteger(id)) {
r = await this.toDb(trx)('proven_tx_reqs')
.where({ provenTxReqId: id })
.update(this.validatePartialForUpdate(update));
}
else {
throw new WERR_errors_1.WERR_INVALID_PARAMETER('id', 'transactionId or array of transactionId');
}
return r;
}
async updateProvenTx(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('proven_txs')
.where({ provenTxId: id })
.update(this.validatePartialForUpdate(update));
}
async updateSyncState(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('sync_states')
.where({ syncStateId: id })
.update(this.validatePartialForUpdate(update, ['when'], ['init']));
}
async updateTransaction(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
let r;
if (Array.isArray(id)) {
r = await this.toDb(trx)('transactions')
.whereIn('transactionId', id)
.update(await this.validatePartialForUpdate(update));
}
else if (Number.isInteger(id)) {
r = await this.toDb(trx)('transactions')
.where({ transactionId: id })
.update(await this.validatePartialForUpdate(update));
}
else {
throw new WERR_errors_1.WERR_INVALID_PARAMETER('id', 'transactionId or array of transactionId');
}
return r;
}
async updateTxLabelMap(transactionId, txLabelId, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('tx_labels_map')
.where({ transactionId, txLabelId })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']));
}
async updateTxLabel(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('tx_labels')
.where({ txLabelId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']));
}
async updateUser(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('users').where({ userId: id }).update(this.validatePartialForUpdate(update));
}
async updateMonitorEvent(id, update, trx) {
await this.verifyReadyForDatabaseAccess(trx);
return await this.toDb(trx)('monitor_events')
.where({ id })
.update(this.validatePartialForUpdate(update));
}
setupQuery(table, args) {
let q = this.toDb(args.trx)(table);
if (args.partial && Object.keys(args.partial).length > 0)
q.where(args.partial);
if (args.since)
q.where('updated_at', '>=', this.validateDateForWhere(args.since));
if (args.orderDescending) {
let sortColumn = '';
switch (table) {
case 'certificates':
sortColumn = 'certificateId';
break;
case 'commissions':
sortColumn = 'commissionId';
break;
case 'output_baskets':
sortColumn = 'basketId';
break;
case 'outputs':
sortColumn = 'outputId';
break;
case 'output_tags':
sortColumn = 'outputTagId';
break;
case 'proven_tx_reqs':
sortColumn = 'provenTxReqId';
break;
case 'proven_txs':
sortColumn = 'provenTxId';
break;
case 'sync_states':
sortColumn = 'syncStateId';
break;
case 'transactions':
sortColumn = 'transactionId';
break;
case 'tx_labels':
sortColumn = 'txLabelId';
break;
case 'users':
sortColumn = 'userId';
break;
case 'monitor_events':
sortColumn = 'id';
break;
default:
break;
}
if (sortColumn !== '') {
q.orderBy(sortColumn, 'desc');
}
}
if (args.paged) {
q.limit(args.paged.limit);
q.offset(args.paged.offset || 0);
}
return q;
}
findCertificateFieldsQuery(args) {
return this.setupQuery('certificate_fields', args);
}
findCertificatesQuery(args) {
const q = this.setupQuery('certificates', args);
if (args.certifiers && args.certifiers.length > 0)
q.whereIn('certifier', args.certifiers);
if (args.types && args.types.length > 0)
q.whereIn('type', args.types);
return q;
}
findCommissionsQuery(args) {
if (args.partial.lockingScript)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('partial.lockingScript', `undefined. Commissions may not be found by lockingScript value.`);
return this.setupQuery('commissions', args);
}
findOutputBasketsQuery(args) {
return this.setupQuery('output_baskets', args);
}
findOutputsQuery(args, count) {
if (args.partial.lockingScript)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.lockingScript', `undefined. Outputs may not be found by lockingScript value.`);
const q = this.setupQuery('outputs', args);
if (args.txStatus && args.txStatus.length > 0) {
q.whereRaw(`(select status from transactions where transactions.transactionId = outputs.transactionId) in (${args.txStatus.map(s => `'${s}'`).join(',')})`);
}
if (args.noScript && !count) {
const columns = tables_1.outputColumnsWithoutLockingScript.map(c => `outputs.${c}`);
q.select(columns);
}
return q;
}
findOutputTagMapsQuery(args) {
const q = this.setupQuery('output_tags_map', args);
if (args.tagIds && args.tagIds.length > 0)
q.whereIn('outputTagId', args.tagIds);
return q;
}
findOutputTagsQuery(args) {
return this.setupQuery('output_tags', args);
}
findProvenTxReqsQuery(args) {
if (args.partial.rawTx)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.rawTx', `undefined. ProvenTxReqs may not be found by rawTx value.`);
if (args.partial.inputBEEF)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.inputBEEF', `undefined. ProvenTxReqs may not be found by inputBEEF value.`);
const q = this.setupQuery('proven_tx_reqs', args);
if (args.status && args.status.length > 0)
q.whereIn('status', args.status);
if (args.txids) {
const txids = args.txids.filter(txid => txid !== undefined);
if (txids.length > 0)
q.whereIn('txid', txids);
}
return q;
}
findProvenTxsQuery(args) {
if (args.partial.rawTx)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.rawTx', `undefined. ProvenTxs may not be found by rawTx value.`);
if (args.partial.merklePath)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.merklePath', `undefined. ProvenTxs may not be found by merklePath value.`);
return this.setupQuery('proven_txs', args);
}
findSyncStatesQuery(args) {
return this.setupQuery('sync_states', args);
}
findTransactionsQuery(args, count) {
if (args.partial.rawTx)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.rawTx', `undefined. Transactions may not be found by rawTx value.`);
if (args.partial.inputBEEF)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('args.partial.inputBEEF', `undefined. Transactions may not be found by inputBEEF value.`);
const q = this.setupQuery('transactions', args);
if (args.status && args.status.length > 0)
q.whereIn('status', args.status);
if (args.from)
q.where('created_at', '>=', this.validateDateForWhere(args.from));
if (args.to)
q.where('created_at', '<', this.validateDateForWhere(args.to));
if (args.noRawTx && !count) {
const columns = tables_1.transactionColumnsWithoutRawTx.map(c => `transactions.${c}`);
q.select(columns);
}
return q;
}
findTxLabelMapsQuery(args) {
const q = this.setupQuery('tx_labels_map', args);
if (args.labelIds && args.labelIds.length > 0)
q.whereIn('txLabelId', args.labelIds);
return q;
}
findTxLabelsQuery(args) {
return this.setupQuery('tx_labels', args);
}
findUsersQuery(args) {
return this.setupQuery('users', args);
}
findMonitorEventsQuery(args) {
return this.setupQuery('monitor_events', args);
}
async findCertificatesAuth(auth, args) {
if (!auth.userId || (args.partial.userId && args.partial.userId !== auth.userId))
throw new WERR_errors_1.WERR_UNAUTHORIZED();
args.partial.userId = auth.userId;
return await this.findCertificates(args);
}
async findOutputBasketsAuth(auth, args) {
if (!auth.userId || (args.partial.userId && args.partial.userId !== auth.userId))
throw new WERR_errors_1.WERR_UNAUTHORIZED();
args.partial.userId = auth.userId;
return await this.findOutputBaskets(args);
}
async findOutputsAuth(auth, args) {
if (!auth.userId || (args.partial.userId && args.partial.userId !== auth.userId))
throw new WERR_errors_1.WERR_UNAUTHORIZED();
args.partial.userId = auth.userId;
return await this.findOutputs(args);
}
async findCertificateFields(args) {
return this.validateEntities(await this.findCertificateFieldsQuery(args));
}
async findCertificates(args) {
const q = this.findCertificatesQuery(args);
let r = await q;
r = this.validateEntities(r, undefined, ['isDeleted']);
if (args.includeFields) {
for (const c of r) {
c.fields = this.validateEntities(await this.findCertificateFields({
partial: { certificateId: c.certificateId, userId: c.userId },
trx: args.trx
}));
}
}
return r;
}
async findCommissions(args) {
const q = this.findCommissionsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['isRedeemed']);
}
async findOutputBaskets(args) {
const q = this.findOutputBasketsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['isDeleted']);
}
async findOutputs(args) {
const q = this.findOutputsQuery(args);
const r = await q;
if (!args.noScript) {
for (const o of r) {
await this.validateOutputScript(o, args.trx);
}
}
return this.validateEntities(r, undefined, ['spendable', 'change']);
}
async findOutputTagMaps(args) {
const q = this.findOutputTagMapsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['isDeleted']);
}
async findOutputTags(args) {
const q = this.findOutputTagsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['isDeleted']);
}
async findProvenTxReqs(args) {
const q = this.findProvenTxReqsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['notified']);
}
async findProvenTxs(args) {
const q = this.findProvenTxsQuery(args);
const r = await q;
return this.validateEntities(r);
}
async findSyncStates(args) {
const q = this.findSyncStatesQuery(args);
const r = await q;
return this.validateEntities(r, ['when'], ['init']);
}
async findTransactions(args) {
const q = this.findTransactionsQuery(args);
const r = await q;
if (!args.noRawTx) {
for (const t of r) {
await this.validateRawTransaction(t, args.trx);
}
}
return this.validateEntities(r, undefined, ['isOutgoing']);
}
async findTxLabelMaps(args) {
const q = this.findTxLabelMapsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['isDeleted']);
}
async findTxLabels(args) {
const q = this.findTxLabelsQuery(args);
const r = await q;
return this.validateEntities(r, undefined, ['isDeleted']);
}
async findUsers(args) {
const q = this.findUsersQuery(args);
const r = await q;
return this.validateEntities(r);
}
async findMonitorEvents(args) {
const q = this.findMonitorEventsQuery(args);
const r = await q;
return this.validateEntities(r, ['when'], undefined);
}
async getCount(q) {
q.count();
const r = await q;
return r[0]['count(*)'];
}
async countCertificateFields(args) {
return await this.getCount(this.findCertificateFieldsQuery(args));
}
async countCertificates(args) {
return await this.getCount(this.findCertificatesQuery(args));
}
async countCommissions(args) {
return await this.getCount(this.findCommissionsQuery(args));
}
async countOutputBaskets(args) {
return await this.getCount(this.findOutputBasketsQuery(args));
}
async countOutputs(args) {
return await this.getCount(this.findOutputsQuery(args, true));
}
async countOutputTagMaps(args) {
return await this.getCount(this.findOutputTagMapsQuery(args));
}
async countOutputTags(args) {
return await this.getCount(this.findOutputTagsQuery(args));
}
async countProvenTxReqs(args) {
return await this.getCount(this.findProvenTxReqsQuery(args));
}
async countProvenTxs(args) {
return await this.getCount(this.findProvenTxsQuery(args));
}
async countSyncStates(args) {
return await this.getCount(this.findSyncStatesQuery(args));
}
async countTransactions(args) {
return await this.getCount(this.findTransactionsQuery(args, true));
}
async countTxLabelMaps(args) {
return await this.getCount(this.findTxLabelMapsQuery(args));
}
async countTxLabels(args) {
return await this.getCount(this.findTxLabelsQuery(args));
}
async countUsers(args) {
return await this.getCount(this.findUsersQuery(args));
}
async countMonitorEvents(args) {
return await this.getCount(this.findMonitorEventsQuery(args));
}
async destroy() {
var _a;
await ((_a = this.knex) === null || _a === void 0 ? void 0 : _a.destroy());
}
async migrate(storageName, storageIdentityKey) {
var _a;
// Check if this is a SQLite database by looking at the Knex client config
const clientName = ((_a = this.knex.client.config) === null || _a === void 0 ? void 0 : _a.client) || '';
const isSQLite = clientName.includes('sqlite');
// For SQLite, disable transactions during migrations and turn off foreign keys.
// PRAGMA foreign_keys is silently ignored inside transactions, so we must
// disable transactions for the migration to allow the PRAGMA to take effect.
// See: https://github.com/knex/knex/issues/4155
if (isSQLite) {
await this.knex.raw('PRAGMA foreign_keys = OFF;');
}
const config = {
migrationSource: new KnexMigrations_1.KnexMigrations(this.chain, storageName, storageIdentityKey, 1024),
disableTransactions: isSQLite
};
await this.knex.migrate.latest(config);
const version = await this.knex.migrate.currentVersion(config);
// Re-enable foreign key checks for SQLite
if (isSQLite) {
await this.knex.raw('PRAGMA foreign_keys = ON;');
}
return version;
}
async dropAllData() {
var _a;
// Only using migrations to migrate down, don't need valid properties for settings table.
const migrationSource = new KnexMigrations_1.KnexMigrations('test', '', '', 1024);
// Check if this is a SQLite database by looking at the Knex client config
const clientName = ((_a = this.knex.client.config) === null || _a === void 0 ? void 0 : _a.client) || '';
const isSQLite = clientName.includes('sqlite');
// For SQLite, disable transactions during migrations and turn off foreign keys.
// PRAGMA foreign_keys is silently ignored inside transactions, so we must
// disable transactions for the migration to allow the PRAGMA to take effect.
// See: https://github.com/knex/knex/issues/4155
const config = {
migrationSource,
disableTransactions: isSQLite
};
const count = Object.keys(migrationSource.migrations).length;
// Disable foreign key checks for SQLite before dropping tables
// This is necessary for better-sqlite3 which enforces FK constraints by default
if (isSQLite) {
await this.knex.raw('PRAGMA foreign_keys = OFF;');
}
for (let i = 0; i < count; i++) {
try {
const r = await this.knex.migrate.down(config);
if (!r) {
console.error(`Migration returned falsy result await this.knex.migrate.down(config)`);
break;
}
}
catch (eu) {
break;
}
}
// Re-enable foreign key checks for SQLite
if (isSQLite) {
await this.knex.raw('PRAGMA foreign_keys = ON;');
}
}
async transaction(scope, trx) {
if (trx)
return await scope(trx);
return await this.knex.transaction(async (knextrx) => {
const trx = knextrx;
return await scope(trx);
});
}
/**
* Convert the standard optional `TrxToken` parameter into either a direct knex database instance,
* or a Knex.Transaction as appropriate.
*/
toDb(trx) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const db = !trx ? this.knex : trx;
this.whenLastAccess = new Date();
return db;
}
async validateRawTransaction(t, trx) {
// if there is no txid or there is a rawTransaction return what we have.
if (t.rawTx || !t.txid)
return;
// rawTransaction is missing, see if we moved it ...
const rawTx = await this.getRawTxOfKnownValidTransaction(t.txid, undefined, undefined, trx);
if (!rawTx)
return;
t.rawTx = rawTx;
}
/**
* Make sure database is ready for access:
*
* - dateScheme is known
* - foreign key constraints are enabled
*
* @param trx
*/
async verifyReadyForDatabaseAccess(trx) {
if (!this._settings) {
this._settings = await this.readSettings();
}
// Always run the PRAGMA for SQLite to ensure foreign key constraints are enabled.
// This is necessary because PRAGMA foreign_keys is a per-connection setting,
// and connection pools may create new connections that don't have it set.
// The performance impact is minimal as SQLite handles this efficiently.
if (this._settings.dbtype === 'SQLite') {
await this.toDb(trx).raw('PRAGMA foreign_keys = ON;');
}
this._verifiedReadyForDatabaseAccess = true;
return this._settings.dbtype;
}
/**
* Helper to force uniform behavior across database engines.
* Use to process the update template for entities being updated.
*/
validatePartialForUpdate(update, dateFields, booleanFields) {
if (!this.dbtype)
throw new WERR_errors_1.WERR_INTERNAL('must call verifyReadyForDatabaseAccess first');
const v = update;
if (v.created_at)
v.created_at = this.validateEntityDate(v.created_at);
if (v.updated_at)
v.updated_at = this.validateEntityDate(v.updated_at);
if (!v.created_at)
delete v.created_at;
if (!v.updated_at)
v.updated_at = this.validateEntityDate(new Date());
if (dateFields) {
for (const df of dateFields) {
if (v[df])
v[df] = this.validateOptionalEntityDate(v[df]);
}
}
if (booleanFields) {
for (const df of booleanFields) {
if (update[df] !== undefined)
update[df] = !!update[df] ? 1 : 0;
}
}
for (const key of Object.keys(v)) {
const val = v[key];
if (Array.isArray(val) && (val.length === 0 || typeof val[0] === 'number')) {
v[key] = Buffer.from(val);
}
else if (val === undefined) {
v[key] = null;
}
}
this.isDirty = true;
return v;
}
/**
* Helper to force uniform behavior across database engines.
* Use to process new entities being inserted into the database.
*/
async validateEntityForInsert(entity, trx, dateFields, booleanFields) {
await this.verifyReadyForDatabaseAccess(trx);
const v = { ...entity };
v.created_at = this.validateOptionalEntityDate(v.created_at, true);
v.updated_at = this.validateOptionalEntityDate(v.updated_at, true);
if (!v.created_at)
delete v.created_at;
if (!v.updated_at)
delete v.updated_at;
if (dateFields) {
for (const df of dateFields) {
if (v[df])
v[df] = this.validateOptionalEntityDate(v[df]);
}
}
if (booleanFields) {
for (const df of booleanFields) {
if (entity[df] !== undefined)
entity[df] = !!entity[df] ? 1 : 0;
}
}
for (const key of Object.keys(v)) {
const val = v[key];
if (Array.isArray(val) && (val.length === 0 || typeof val[0] === 'number')) {
v[key] = Buffer.from(val);
}
else if (val === undefined) {
v[key] = null;
}
}
this.isDirty = true;
return v;
}
async getLabelsForTransactionId(transactionId, trx) {
if (transactionId === undefined)
return [];
const labels = await this.toDb(trx)('tx_labels')
.join('tx_labels_map', 'tx_labels_map.txLabelId', 'tx_labels.txLabelId')
.where('tx_labels_map.transactionId', transactionId)
.whereNot('tx_labels_map.isDeleted', true)
.whereNot('tx_labels.isDeleted', true);
return this.validateEntities(labels, undefined, ['isDeleted']);
}
async getTagsForOutputId(outputId, trx) {
const tags = await this.toDb(trx)('output_tags')
.join('output_tags_map', 'output_tags_map.outputTagId', 'output_tags.outputTagId')
.where('output_tags_map.outputId', outputId)
.whereNot('output_tags_map.isDeleted', true)
.whereNot('output_tags.isDeleted', true);
return this.validateEntities(tags, undefined, ['isDeleted']);
}
async purgeData(params, trx) {
return await (0, purgeData_1.purgeData)(this, params, trx);
}
async reviewStatus(args) {
return await (0, reviewStatus_1.reviewStatus)(this, args);
}
/**
* Counts the outputs for userId in basketId that are spendable: true
* AND whose transaction status is one of:
* - completed
* - unproven
* - sending (if excludeSending is false)
*/
async countChangeInputs(userId, basketId, excludeSending) {
const status = ['completed', 'unproven'];
if (!excludeSending)
status.push('sending');
const q = this.knex('outputs as o')
.join('transactions as t', 'o.transactionId', 't.transactionId')
.where({ 'o.userId': userId, 'o.spendable': true, 'o.basketId': basketId })
.whereIn('t.status', status);
const count = await this.getCount(q);
return count;
}
async findOutputsByIds(outputIds, trx) {
const byId = {};
if (outputIds.length < 1)
return byId;
const rows = await this.toDb(trx)('outputs').whereIn('outputId', outputIds).select('*');
for (const o of rows) {
await this.validateOutputScript(o, trx);
}
const vrows = this.validateEntities(rows, undefined, ['spendable', 'change']);
for (const row of vrows) {
if (row.outputId !== undefined)
byId[row.outputId] = row;
}
return byId;
}
async findOutputsByOutpoints(userId, outpoints, trx) {
const byOutpoint = {};
if (outpoints.length < 1)
return byOutpoint;
const outpointSet = new Set(outpoints.map(o => `${o.txid}.${o.vout}`));
const txids = [...new Set(outpoints.map(o => o.txid))];
const vouts = [...new Set(outpoints.map(o => o.vout))];
const rows = await this.toDb(trx)('outputs')
.where('userId', userId)
.whereIn('txid', txids)
.whereIn('vout', vouts)
.select('*');
// Only return requested outpoints, vouts of one txid may end up matching another txid that was not requested.
const filteredRows = rows.filter(r => outpointSet.has(`${r.txid}.${r.vout}`));
const vrows = this.validateEntities(filteredRows, undefined, ['spendable', 'change']);
for (const row of vrows) {
await this.validateOutputScript(row, trx);
byOutpoint[`${row.txid}.${row.vout}`] = row;
}
return byOutpoint;
}
async findOrInsertOutputBasketsBulk(userId, names, trx) {
const byName = {};
if (names.length < 1)
return byName;
const uniqueNames = [...new Set(names)];
const existing = await this.toDb(trx)('output_baskets')
.where('userId', userId)
.whereIn('name', uniqueNames)
.select('*');
for (const basket of existing) {
if (basket.isDeleted)
await this.updateOutputBasket((0, utilityHelpers_1.verifyId)(basket.basketId), { isDeleted: false }, trx);
byName[basket.name] = basket;
}
for (const name of uniqueNames) {
if (!byName[name])
byName[name] = await this.findOrInsertOutputBasket(userId, name, trx);
}
return byName;
}
async findOrInsertOutputTagsBulk(userId, tags, trx) {
const byTag = {};
if (tags.length < 1)
return byTag;
const uniqueTags = [...new Set(tags)];
const existing = await this.toDb(trx)('output_tags')
.where('userId', userId)
.whereIn('tag', uniqueTags)
.select('*');
for (const outputTag of existing) {
if (outputTag.isDeleted)
await this.updateOutputTag((0, utilityHelpers_1.verifyId)(outputTag.outputTagId), { isDeleted: false }, trx);
byTag[outputTag.tag] = outputTag;
}
for (const tag of uniqueTags) {
if (!byTag[tag])
byTag[tag] = await this.findOrInsertOutputTag(userId, tag, trx);
}
return byTag;
}
async sumSpendableSatoshisInBasket(userId, basketId, excludeSending, trx) {
const status = ['completed', 'unproven'];
if (!excludeSending)
status.push('sending');
const row = await this.toDb(trx)('outputs as o')
.join('transactions as t', 'o.transactionId', 't.transactionId')
.where({ 'o.userId': userId, 'o.spendable': true, 'o.basketId': basketId })
.whereIn('t.status', status)
.sum({ totalSatoshis: 'o.satoshis' })
.first();
return Number((row && row['totalSatoshis']) || 0);
}
/**
* Finds closest matching available change output to use as input for new transaction.
*
* Transactionally allocate the output such that
*/
async allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, excludeSending, transactionId) {
const status = ['completed', 'unproven'];
if (!excludeSending)
status.push('sending');
const r = await this.knex.transaction(async (trx) => {
const baseQuery = () => trx('outputs as o')
.join('transactions as t', 'o.transactionId', 't.transactionId')
.where('o.userId', userId)
.where('o.spendable', true)
.where('o.basketId', basketId)
.whereIn('t.status', status)
.select('o.*');
let output;
if (exactSatoshis !== undefined) {
output = await baseQuery().where('o.satoshis', exactSatoshis).orderBy('o.outputId', 'asc').first();
}
if (!output) {
output = await baseQuery()
.where('o.satoshis', '>=', targetSatoshis)
.orderBy('o.satoshis', 'asc')
.orderBy('o.outputId', 'asc')
.first();
}
if (!output) {
output = await baseQuery()
.where('o.satoshis', '<', targetSatoshis)
.orderBy('o.satoshis', 'desc')
.orderBy('o.outputId', 'desc')
.first();
}
if (!output)
return undefined;
await this.updateOutput(output.outputId, {
spendable: false,
spentBy: transactionId
}, trx);
// Keep behavior identical to the pre-optimization path: ensure lockingScript
// is present even when it was offloaded from outputs into rawTx storage.
await this.validateOutputScript(output, trx);
output.spendable = false;
output.spentBy = transactionId;
return output;
});
return r;
}
/**
* Helper to force uniform behavior across database engines.
* Use to process all individual records with time stamps retreived from database.
*/
validateEntity(entity, dateFields, booleanFields) {
entity.created_at = this.validateDate(entity.created_at);
entity.updated_at = this.validateDate(entity.updated_at);
if (dateFields) {
for (const df of dateFields) {
if (entity[df])
entity[df] = this.validateDate(entity[df]);
}
}
if (booleanFields) {
for (const df of booleanFields) {
if (entity[df] !== undefined)
entity[df] = !!entity[df];
}
}
for (const key of Object.keys(entity)) {
const val = entity[key];
if (val === null) {
entity[key] = undefined;
}
else if (Buffer.isBuffer(val)) {
entity[key] = Array.from(val);
}
}
return entity;
}
/**
* Helper to force uniform behavior across database engines.
* Use to process all arrays of records with time stamps retreived from database.
* @returns input `entities` array with contained values validated.
*/
validateEntities(entities, dateFields, booleanFields) {
for (let i = 0; i < entities.length; i++) {
entities[i] = this.validateEntity(entities[i], dateFields, booleanFields);
}
return entities;
}
async adminStats(adminIdentityKey) {
if (this.dbtype !== 'MySQL')
throw new WERR_errors_1.WERR_NOT_IMPLEMENTED('adminStats, only MySQL is supported');
const monitorEvent = (0, utilityHelpers_1.verifyOneOrNone)(await this.findMonitorEvents({
partial: { event: 'MonitorCallHistory' },
orderDescending: true,
paged: { limit: 1 }
}));
const monitorStats = monitorEvent ? JSON.parse(monitorEvent.details) : undefined;
const servicesStats = this.getServices().getServicesCallHistory(true);
await this.insertMonitorEvent({
event: 'ServicesCallHistory',
details: JSON.stringify(servicesStats),
created_at: new Date(),
updated_at: new Date(),
id: 0
});
const one_day_ago = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const one_week_ago = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const one_month_ago = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const [[{ usersDay, usersMonth, usersWeek, usersTotal, transactionsDay, transactionsMonth, transactionsWeek, transactionsTotal, txCompletedDay, txCompletedMonth, txCompletedWeek, txCompletedTotal, txFailedDay, txFailedMonth, txFailedWeek, txFailedTotal, txUnprocessedDay, txUnprocessedMonth, txUnprocessedWeek, txUnprocessedTotal, txSendingDay, txSendingMonth, txSendingWeek, txSendingTotal, txUnprovenDay, txUnprovenMonth, txUnprovenWeek, txUnprovenTotal, txUnsignedDay, txUnsignedMonth, txUnsignedWeek, txUnsignedTotal, txNosendDay, txNosendMonth, txNosendWeek, txNosendTotal, txNonfinalDay, txNonfinalMonth, txNonfinalWeek, txNonfinalTotal, txUnfailDay, txUnfailMonth, txUnfailWeek, txUnfailTotal, satoshisDefaultDay, satoshisDefaultMonth, satoshisDefaultWeek, satoshisDefaultTotal, satoshisOtherDay, satoshisOtherMonth, satoshisOtherWeek, satoshisOtherTotal, basketsDay, basketsMonth, basketsWeek, basketsTotal, labelsDay, labelsMonth, labelsWeek, labelsTotal, tagsDay, tagsMonth, tagsWeek, tagsTotal }]] = await this.knex.raw(`
select
(select count(*) from users where created_at > '${one_day_ago}') as usersDay,
(select count(*) from users where created_at > '${one_week_ago}') as usersWeek,
(select count(*) from users where created_at > '${one_month_ago}') as usersMonth,
(select count(*) from users) as usersTotal,
(select count(*) from transactions where created_at > '${one_day_ago}') as transactionsDay,
(select count(*) from transactions where created_at > '${one_week_ago}') as transactionsWeek,
(select count(*) from transactions where created_at > '${one_month_ago}') as transactionsMonth,
(select count(*) from transactions) as transactionsTotal,
(select count(*) from transactions where status = 'completed' and created_at > '${one_day_ago}') as txCompletedDay,
(select count(*