UNPKG

@bsv/wallet-toolbox-client

Version:
1,124 lines 53.9 kB
"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']); const fields = e.fields; if (e.fields) delete e.fields; if (e.certificateId === 0) delete e.certificateId; 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.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) { const config = { migrationSource: new KnexMigrations_1.KnexMigrations(this.chain, storageName, storageIdentityKey, 1024) }; await this.knex.migrate.latest(config); const version = await this.knex.migrate.currentVersion(config); return version; } async dropAllData() { // Only using migrations to migrate down, don't need valid properties for settings table. const config = { migrationSource: new KnexMigrations_1.KnexMigrations('test', '', '', 1024) }; const count = Object.keys(config.migrationSource.migrations).length; 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; } } } 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(); } if (!this._verifiedReadyForDatabaseAccess) { // Make sure foreign key constraint checking is turned on in SQLite. 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 statusText = status.map(s => `'${s}'`).join(','); const txStatusCondition = `(SELECT status FROM transactions WHERE outputs.transactionId = transactions.transactionId) in (${statusText})`; let q = this.knex('outputs').where({ userId, spendable: true, basketId }).whereRaw(txStatusCondition); const count = await this.getCount(q); return count; } /** * 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 statusText = status.map(s => `'${s}'`).join(','); const r = await this.knex.transaction(async (trx) => { const txStatusCondition = `AND (SELECT status FROM transactions WHERE outputs.transactionId = transactions.transactionId) in (${statusText})`; let outputId; const setOutputId = async (rawQuery) => { let oidr = await trx.raw(rawQuery); outputId = undefined; if (!oidr['outputId'] && oidr.length > 0) oidr = oidr[0]; if (!oidr['outputId'] && oidr.length > 0) oidr = oidr[0]; if (oidr['outputId']) outputId = Number(oidr['outputId']); }; if (exactSatoshis !== undefined) { // Find outputId of output that with exactSatoshis await setOutputId(` SELECT outputId FROM outputs WHERE userId = ${userId} AND spendable = 1 AND basketId = ${basketId} ${txStatusCondition} AND satoshis = ${exactSatoshis} LIMIT 1; `); } if (outputId === undefined) { // Find outputId of output that would at least fund targetSatoshis await setOutputId(` SELECT outputId FROM outputs WHERE userId = ${userId} AND spendable = 1 AND basketId = ${basketId} ${txStatusCondition} AND satoshis - ${targetSatoshis} = ( SELECT MIN(satoshis - ${targetSatoshis}) FROM outputs WHERE userId = ${userId} AND spendable = 1 AND basketId = ${basketId} ${txStatusCondition} AND satoshis - ${targetSatoshis} >= 0 ) LIMIT 1; `); } if (outputId === undefined) { // Find outputId of output that would add the most fund targetSatoshis await setOutputId(` SELECT outputId FROM outputs WHERE userId = ${userId} AND spendable = 1 AND basketId = ${basketId} ${txStatusCondition} AND satoshis - ${targetSatoshis} = ( SELECT MAX(satoshis - ${targetSatoshis}) FROM outputs WHERE userId = ${userId} AND spendable = 1 AND basketId = ${basketId} ${txStatusCondition} AND satoshis - ${targetSatoshis} < 0 ) LIMIT 1; `); } if (outputId === undefined) return undefined; await this.updateOutput(outputId, { spendable: false, spentBy: transactionId }, trx); const r = (0, utilityHelpers_1.verifyTruthy)(await this.findOutputById(outputId, trx)); return r; }); 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(*) from transactions where status = 'completed' and created_at > '${one_week_ago}') as txCompletedWeek, (select count(*) from transactions where status = 'completed' and created_at > '${one_month_ago}') as txCompletedMonth, (select count(*) from transactions where status = 'completed') as txCompletedTotal, (select count(*) from transactions where status = 'failed' and created_at > '${one_day_ago}') as txFailedDay, (select count(*) from transactions where status = 'failed' and created_at > '${one_week_ago}') as txFailedWeek, (select count(*) from transactions where status = 'failed' and created_at > '${one_month_ago}') as txFailedMonth, (select count(*) from transactions where status = 'failed') as txFailedTotal, (select count(*) from transactions where status = 'unprocessed' and created_at > '${one_day_ago}') as txUnprocessedDay, (select count(*) from transactions where status = 'unprocessed' and created_at > '${one_week_ago}') as txUnprocessedWeek, (select count(*) from transactions where status = 'unprocessed' and created_at > '${one_month_ago}') as txUnprocessedMonth, (select count(*) from transactions where status = 'unprocessed') as txUnprocessedTotal, (select count(*) from transactions where status = 'sending' and created_at > '${one_day_ago}') as txSendingDay, (select count(*) from transactions where status = 'sending' and created_at > '${one_week_ago}') as txSendingWeek, (select count(*) from transactions where status = 'sending' and created_at > '${one_month_ago}') as txSendingMonth, (select count(*) from transactions where status = 'sending') as txSendingTotal, (select count(*) from transactions where status = 'unproven' and created_at > '${one_day_ago}') as txUnprovenDay, (select count(*) from transactions where status = 'unproven' and created_at > '${one_week_ago}') as txUnprovenWeek, (select count(*) from transactions where status = 'unproven' and created_at > '${one_month_ago}') as txUnprovenMonth, (select count(*) from transactions where status = 'unproven') as txUnprovenTotal, (select count(*) from transactions where status = 'unsigned' and created_at > '${one_day_ago}') as txUnsignedDay, (select count(*) from transactions where status = 'unsigned' and created_at > '${one_week_ago}') as txUnsignedWeek, (select count(*) from transactions where status = 'unsigned' and created_at > '${one_month_ago}') as txUnsignedMonth, (select count(*) from transactions where status = 'unsigned') as txUnsignedTotal, (select count(*) from transactions where status = 'nosend' and created_at > '${one_day_ago}') as txNosendDay, (select count(*) from transactions where status = 'nosend' and created_at > '${one_week_ago}') as txNosendWeek, (select count(*) from transactions where status = 'nosend' and created_at > '${one_month_ago}') as txNosendMonth, (select count(*) from transactions where status = 'nosend') as txNosendTotal, (select count(*) from transactions where status = 'nonfinal' and created_at > '${one_day_ago}') as txNonfinalDay, (select count(*) from transactions where status = 'nonfinal' and created_at > '${one_week_ago}') as txNonfinalWeek, (select count(*) from transactions where status = 'nonfinal' and created_at > '${one_month_ago}') as txNonfinalMonth, (select count(*) from transactions where status = 'nonfinal') as txNonfinalTotal, (select count(*) from transactions where status = 'unfail' and created_at > '${one_day_ago}') as txUnfailDay, (select count(*) from transactions where status = 'unfail' and created_at > '${one_week_ago}') as txUnfailWeek, (select count(*) from transactions where status = 'unfail' and created_at > '${one_month_ago}') as txUnfailMonth, (select count(*) from transactions where status = 'unfail') as txUnfailTotal, (select sum(satoshis) from outputs where spendable = 1 and \`change\` = 1 and created_at > '${one_day_ago}') as satoshisDefaultDay, (select sum(satoshis) from outputs where spendable = 1 and \`change\` = 1 and created_at > '${one_week_ago}') as satoshisDefaultWeek, (select sum(satoshis) from outputs where spendable = 1 and \`change\` = 1 and created_at > '${one_month_ago}') as satoshisDefaultMonth, (select sum(satoshis) from outputs where spendable = 1 and \`change\` = 1) as satoshisDefaultTotal, (sel