UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

1,290 lines (1,213 loc) 57.1 kB
import { ListActionsResult, ListOutputsResult } from '@bsv/sdk' import { sdk, verifyOne, verifyOneOrNone, verifyTruthy } from '../index.all' import { outputColumnsWithoutLockingScript, TableCertificate, TableCertificateField, TableCertificateX, TableCommission, TableMonitorEvent, TableOutput, TableOutputBasket, TableOutputTag, TableOutputTagMap, TableProvenTx, TableProvenTxReq, TableSettings, TableSyncState, TableTransaction, TableTxLabel, TableTxLabelMap, TableUser, transactionColumnsWithoutRawTx } from './schema/tables' import { KnexMigrations } from './schema/KnexMigrations' import { Knex } from 'knex' import { AdminStatsResult, StorageProvider, StorageProviderOptions } from './StorageProvider' import { purgeData } from './methods/purgeData' import { listActions } from './methods/listActionsKnex' import { listOutputs } from './methods/listOutputsKnex' import { DBType } from './StorageReader' import { reviewStatus } from './methods/reviewStatus' import { ServicesCallHistory } from '../sdk/WalletServices.interfaces' export interface StorageKnexOptions extends StorageProviderOptions { /** * Knex database interface initialized with valid connection configuration. */ knex: Knex } export class StorageKnex extends StorageProvider implements sdk.WalletStorageProvider { knex: Knex constructor(options: StorageKnexOptions) { super(options) if (!options.knex) throw new sdk.WERR_INVALID_PARAMETER('options.knex', `valid`) this.knex = options.knex } async readSettings(): Promise<TableSettings> { return this.validateEntity(verifyOne(await this.toDb(undefined)<TableSettings>('settings'))) } override async getProvenOrRawTx(txid: string, trx?: sdk.TrxToken): Promise<sdk.ProvenOrRawTx> { const k = this.toDb(trx) const r: sdk.ProvenOrRawTx = { proven: undefined, rawTx: undefined, inputBEEF: undefined } r.proven = verifyOneOrNone(await this.findProvenTxs({ partial: { txid: txid } })) if (!r.proven) { const reqRawTx = 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: string, fromOffset: number, forLength?: number) { if (this.dbtype === 'MySQL') return `substring(${source} from ${fromOffset} for ${forLength!})` return `substr(${source}, ${fromOffset}, ${forLength})` } override async getRawTxOfKnownValidTransaction( txid?: string, offset?: number, length?: number, trx?: sdk.TrxToken ): Promise<number[] | undefined> { if (!txid) return undefined if (!this.isAvailable()) await this.makeAvailable() let rawTx: number[] | undefined = undefined if (Number.isInteger(offset) && Number.isInteger(length)) { let rs: { rawTx: Buffer | null }[] = 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 as unknown as { rawTx: Buffer | null }[][])[0] const r = verifyOneOrNone(rs) if (r && r.rawTx) { rawTx = Array.from(r.rawTx) } else { let rs: { rawTx: Buffer | null }[] = 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 as unknown as { rawTx: Buffer | null }[][])[0] const r = 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: sdk.FindForUserSincePagedArgs): Knex.QueryBuilder { 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 } override async getProvenTxsForUser(args: sdk.FindForUserSincePagedArgs): Promise<TableProvenTx[]> { const q = this.getProvenTxsForUserQuery(args) const rs = await q return this.validateEntities(rs) } getProvenTxReqsForUserQuery(args: sdk.FindForUserSincePagedArgs): Knex.QueryBuilder { 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 } override async getProvenTxReqsForUser(args: sdk.FindForUserSincePagedArgs): Promise<TableProvenTxReq[]> { const q = this.getProvenTxReqsForUserQuery(args) const rs = await q return this.validateEntities(rs, undefined, ['notified']) } getTxLabelMapsForUserQuery(args: sdk.FindForUserSincePagedArgs): Knex.QueryBuilder { 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 } override async getTxLabelMapsForUser(args: sdk.FindForUserSincePagedArgs): Promise<TableTxLabelMap[]> { const q = this.getTxLabelMapsForUserQuery(args) const rs = await q return this.validateEntities(rs, undefined, ['isDeleted']) } getOutputTagMapsForUserQuery(args: sdk.FindForUserSincePagedArgs): Knex.QueryBuilder { 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 } override async getOutputTagMapsForUser(args: sdk.FindForUserSincePagedArgs): Promise<TableOutputTagMap[]> { const q = this.getOutputTagMapsForUserQuery(args) const rs = await q return this.validateEntities(rs, undefined, ['isDeleted']) } override async listActions(auth: sdk.AuthId, vargs: sdk.ValidListActionsArgs): Promise<ListActionsResult> { if (!auth.userId) throw new sdk.WERR_UNAUTHORIZED() return await listActions(this, auth, vargs) } override async listOutputs(auth: sdk.AuthId, vargs: sdk.ValidListOutputsArgs): Promise<ListOutputsResult> { if (!auth.userId) throw new sdk.WERR_UNAUTHORIZED() return await listOutputs(this, auth, vargs) } override async insertProvenTx(tx: TableProvenTx, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(tx, trx) if (e.provenTxId === 0) delete e.provenTxId const [id] = await this.toDb(trx)<TableProvenTx>('proven_txs').insert(e) tx.provenTxId = id return tx.provenTxId } override async insertProvenTxReq(tx: TableProvenTxReq, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(tx, trx) if (e.provenTxReqId === 0) delete e.provenTxReqId const [id] = await this.toDb(trx)<TableProvenTxReq>('proven_tx_reqs').insert(e) tx.provenTxReqId = id return tx.provenTxReqId } override async insertUser(user: TableUser, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(user, trx) if (e.userId === 0) delete e.userId const [id] = await this.toDb(trx)<TableUser>('users').insert(e) user.userId = id return user.userId } override async insertCertificateAuth(auth: sdk.AuthId, certificate: TableCertificateX): Promise<number> { if (!auth.userId || (certificate.userId && certificate.userId !== auth.userId)) throw new sdk.WERR_UNAUTHORIZED() certificate.userId = auth.userId return await this.insertCertificate(certificate) } override async insertCertificate(certificate: TableCertificateX, trx?: sdk.TrxToken): Promise<number> { 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)<TableCertificate>('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 } override async insertCertificateField(certificateField: TableCertificateField, trx?: sdk.TrxToken): Promise<void> { const e = await this.validateEntityForInsert(certificateField, trx) await this.toDb(trx)<TableCertificate>('certificate_fields').insert(e) } override async insertOutputBasket(basket: TableOutputBasket, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(basket, trx, undefined, ['isDeleted']) if (e.basketId === 0) delete e.basketId const [id] = await this.toDb(trx)<TableOutputBasket>('output_baskets').insert(e) basket.basketId = id return basket.basketId } override async insertTransaction(tx: TableTransaction, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(tx, trx) if (e.transactionId === 0) delete e.transactionId const [id] = await this.toDb(trx)<TableTransaction>('transactions').insert(e) tx.transactionId = id return tx.transactionId } override async insertCommission(commission: TableCommission, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(commission, trx) if (e.commissionId === 0) delete e.commissionId const [id] = await this.toDb(trx)<TableCommission>('commissions').insert(e) commission.commissionId = id return commission.commissionId } override async insertOutput(output: TableOutput, trx?: sdk.TrxToken): Promise<number> { try { const e = await this.validateEntityForInsert(output, trx) if (e.outputId === 0) delete e.outputId const [id] = await this.toDb(trx)<TableOutput>('outputs').insert(e) output.outputId = id return output.outputId } catch (e) { throw e } } override async insertOutputTag(tag: TableOutputTag, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(tag, trx, undefined, ['isDeleted']) if (e.outputTagId === 0) delete e.outputTagId const [id] = await this.toDb(trx)<TableOutputTag>('output_tags').insert(e) tag.outputTagId = id return tag.outputTagId } override async insertOutputTagMap(tagMap: TableOutputTagMap, trx?: sdk.TrxToken): Promise<void> { const e = await this.validateEntityForInsert(tagMap, trx, undefined, ['isDeleted']) const [id] = await this.toDb(trx)<TableOutputTagMap>('output_tags_map').insert(e) } override async insertTxLabel(label: TableTxLabel, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(label, trx, undefined, ['isDeleted']) if (e.txLabelId === 0) delete e.txLabelId const [id] = await this.toDb(trx)<TableTxLabel>('tx_labels').insert(e) label.txLabelId = id return label.txLabelId } override async insertTxLabelMap(labelMap: TableTxLabelMap, trx?: sdk.TrxToken): Promise<void> { const e = await this.validateEntityForInsert(labelMap, trx, undefined, ['isDeleted']) const [id] = await this.toDb(trx)<TableTxLabelMap>('tx_labels_map').insert(e) } override async insertMonitorEvent(event: TableMonitorEvent, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(event, trx) if (e.id === 0) delete e.id const [id] = await this.toDb(trx)<TableMonitorEvent>('monitor_events').insert(e) event.id = id return event.id } override async insertSyncState(syncState: TableSyncState, trx?: sdk.TrxToken): Promise<number> { const e = await this.validateEntityForInsert(syncState, trx, ['when'], ['init']) if (e.syncStateId === 0) delete e.syncStateId const [id] = await this.toDb(trx)<TableSyncState>('sync_states').insert(e) syncState.syncStateId = id return syncState.syncStateId } override async updateCertificateField( certificateId: number, fieldName: string, update: Partial<TableCertificateField>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableCertificateField>('certificate_fields') .where({ certificateId, fieldName }) .update(this.validatePartialForUpdate(update)) } override async updateCertificate(id: number, update: Partial<TableCertificate>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableCertificate>('certificates') .where({ certificateId: id }) .update(this.validatePartialForUpdate(update, undefined, ['isDeleted'])) } override async updateCommission(id: number, update: Partial<TableCommission>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableCommission>('commissions') .where({ commissionId: id }) .update(this.validatePartialForUpdate(update)) } override async updateOutputBasket( id: number, update: Partial<TableOutputBasket>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableOutputBasket>('output_baskets') .where({ basketId: id }) .update(this.validatePartialForUpdate(update, undefined, ['isDeleted'])) } override async updateOutput(id: number, update: Partial<TableOutput>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableOutput>('outputs') .where({ outputId: id }) .update(this.validatePartialForUpdate(update)) } override async updateOutputTagMap( outputId: number, tagId: number, update: Partial<TableOutputTagMap>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableOutputTagMap>('output_tags_map') .where({ outputId, outputTagId: tagId }) .update(this.validatePartialForUpdate(update, undefined, ['isDeleted'])) } override async updateOutputTag(id: number, update: Partial<TableOutputTag>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableOutputTag>('output_tags') .where({ outputTagId: id }) .update(this.validatePartialForUpdate(update, undefined, ['isDeleted'])) } override async updateProvenTxReq( id: number | number[], update: Partial<TableProvenTxReq>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) let r: number if (Array.isArray(id)) { r = await this.toDb(trx)<TableProvenTxReq>('proven_tx_reqs') .whereIn('provenTxReqId', id) .update(this.validatePartialForUpdate(update)) } else if (Number.isInteger(id)) { r = await this.toDb(trx)<TableProvenTxReq>('proven_tx_reqs') .where({ provenTxReqId: id }) .update(this.validatePartialForUpdate(update)) } else { throw new sdk.WERR_INVALID_PARAMETER('id', 'transactionId or array of transactionId') } return r } override async updateProvenTx(id: number, update: Partial<TableProvenTx>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableProvenTx>('proven_txs') .where({ provenTxId: id }) .update(this.validatePartialForUpdate(update)) } override async updateSyncState(id: number, update: Partial<TableSyncState>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableSyncState>('sync_states') .where({ syncStateId: id }) .update(this.validatePartialForUpdate(update, ['when'], ['init'])) } override async updateTransaction( id: number | number[], update: Partial<TableTransaction>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) let r: number if (Array.isArray(id)) { r = await this.toDb(trx)<TableTransaction>('transactions') .whereIn('transactionId', id) .update(await this.validatePartialForUpdate(update)) } else if (Number.isInteger(id)) { r = await this.toDb(trx)<TableTransaction>('transactions') .where({ transactionId: id }) .update(await this.validatePartialForUpdate(update)) } else { throw new sdk.WERR_INVALID_PARAMETER('id', 'transactionId or array of transactionId') } return r } override async updateTxLabelMap( transactionId: number, txLabelId: number, update: Partial<TableTxLabelMap>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableTxLabelMap>('tx_labels_map') .where({ transactionId, txLabelId }) .update(this.validatePartialForUpdate(update, undefined, ['isDeleted'])) } override async updateTxLabel(id: number, update: Partial<TableTxLabel>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableTxLabel>('tx_labels') .where({ txLabelId: id }) .update(this.validatePartialForUpdate(update, undefined, ['isDeleted'])) } override async updateUser(id: number, update: Partial<TableUser>, trx?: sdk.TrxToken): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableUser>('users').where({ userId: id }).update(this.validatePartialForUpdate(update)) } override async updateMonitorEvent( id: number, update: Partial<TableMonitorEvent>, trx?: sdk.TrxToken ): Promise<number> { await this.verifyReadyForDatabaseAccess(trx) return await this.toDb(trx)<TableMonitorEvent>('monitor_events') .where({ id }) .update(this.validatePartialForUpdate(update)) } setupQuery<T extends object>(table: string, args: sdk.FindPartialSincePagedArgs<T>): Knex.QueryBuilder { let q = this.toDb(args.trx)<T>(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: sdk.FindCertificateFieldsArgs): Knex.QueryBuilder { return this.setupQuery('certificate_fields', args) } findCertificatesQuery(args: sdk.FindCertificatesArgs): Knex.QueryBuilder { 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: sdk.FindCommissionsArgs): Knex.QueryBuilder { if (args.partial.lockingScript) throw new sdk.WERR_INVALID_PARAMETER( 'partial.lockingScript', `undefined. Commissions may not be found by lockingScript value.` ) return this.setupQuery('commissions', args) } findOutputBasketsQuery(args: sdk.FindOutputBasketsArgs): Knex.QueryBuilder { return this.setupQuery('output_baskets', args) } findOutputsQuery(args: sdk.FindOutputsArgs, count?: boolean): Knex.QueryBuilder { if (args.partial.lockingScript) throw new sdk.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 = outputColumnsWithoutLockingScript.map(c => `outputs.${c}`) q.select(columns) } return q } findOutputTagMapsQuery(args: sdk.FindOutputTagMapsArgs): Knex.QueryBuilder { const q = this.setupQuery('output_tags_map', args) if (args.tagIds && args.tagIds.length > 0) q.whereIn('outputTagId', args.tagIds) return q } findOutputTagsQuery(args: sdk.FindOutputTagsArgs): Knex.QueryBuilder { return this.setupQuery('output_tags', args) } findProvenTxReqsQuery(args: sdk.FindProvenTxReqsArgs): Knex.QueryBuilder { if (args.partial.rawTx) throw new sdk.WERR_INVALID_PARAMETER( 'args.partial.rawTx', `undefined. ProvenTxReqs may not be found by rawTx value.` ) if (args.partial.inputBEEF) throw new sdk.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: sdk.FindProvenTxsArgs): Knex.QueryBuilder { if (args.partial.rawTx) throw new sdk.WERR_INVALID_PARAMETER( 'args.partial.rawTx', `undefined. ProvenTxs may not be found by rawTx value.` ) if (args.partial.merklePath) throw new sdk.WERR_INVALID_PARAMETER( 'args.partial.merklePath', `undefined. ProvenTxs may not be found by merklePath value.` ) return this.setupQuery('proven_txs', args) } findSyncStatesQuery(args: sdk.FindSyncStatesArgs): Knex.QueryBuilder { return this.setupQuery('sync_states', args) } findTransactionsQuery(args: sdk.FindTransactionsArgs, count?: boolean): Knex.QueryBuilder { if (args.partial.rawTx) throw new sdk.WERR_INVALID_PARAMETER( 'args.partial.rawTx', `undefined. Transactions may not be found by rawTx value.` ) if (args.partial.inputBEEF) throw new sdk.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 = transactionColumnsWithoutRawTx.map(c => `transactions.${c}`) q.select(columns) } return q } findTxLabelMapsQuery(args: sdk.FindTxLabelMapsArgs): Knex.QueryBuilder { const q = this.setupQuery('tx_labels_map', args) if (args.labelIds && args.labelIds.length > 0) q.whereIn('txLabelId', args.labelIds) return q } findTxLabelsQuery(args: sdk.FindTxLabelsArgs): Knex.QueryBuilder { return this.setupQuery('tx_labels', args) } findUsersQuery(args: sdk.FindUsersArgs): Knex.QueryBuilder { return this.setupQuery('users', args) } findMonitorEventsQuery(args: sdk.FindMonitorEventsArgs): Knex.QueryBuilder { return this.setupQuery('monitor_events', args) } override async findCertificatesAuth(auth: sdk.AuthId, args: sdk.FindCertificatesArgs): Promise<TableCertificateX[]> { if (!auth.userId || (args.partial.userId && args.partial.userId !== auth.userId)) throw new sdk.WERR_UNAUTHORIZED() args.partial.userId = auth.userId return await this.findCertificates(args) } override async findOutputBasketsAuth( auth: sdk.AuthId, args: sdk.FindOutputBasketsArgs ): Promise<TableOutputBasket[]> { if (!auth.userId || (args.partial.userId && args.partial.userId !== auth.userId)) throw new sdk.WERR_UNAUTHORIZED() args.partial.userId = auth.userId return await this.findOutputBaskets(args) } override async findOutputsAuth(auth: sdk.AuthId, args: sdk.FindOutputsArgs): Promise<TableOutput[]> { if (!auth.userId || (args.partial.userId && args.partial.userId !== auth.userId)) throw new sdk.WERR_UNAUTHORIZED() args.partial.userId = auth.userId return await this.findOutputs(args) } override async findCertificateFields(args: sdk.FindCertificateFieldsArgs): Promise<TableCertificateField[]> { return this.validateEntities(await this.findCertificateFieldsQuery(args)) } override async findCertificates(args: sdk.FindCertificatesArgs): Promise<TableCertificateX[]> { const q = this.findCertificatesQuery(args) let r: TableCertificateX[] = 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 } override async findCommissions(args: sdk.FindCommissionsArgs): Promise<TableCommission[]> { const q = this.findCommissionsQuery(args) const r = await q return this.validateEntities(r, undefined, ['isRedeemed']) } override async findOutputBaskets(args: sdk.FindOutputBasketsArgs): Promise<TableOutputBasket[]> { const q = this.findOutputBasketsQuery(args) const r = await q return this.validateEntities(r, undefined, ['isDeleted']) } override async findOutputs(args: sdk.FindOutputsArgs): Promise<TableOutput[]> { 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']) } override async findOutputTagMaps(args: sdk.FindOutputTagMapsArgs): Promise<TableOutputTagMap[]> { const q = this.findOutputTagMapsQuery(args) const r = await q return this.validateEntities(r, undefined, ['isDeleted']) } override async findOutputTags(args: sdk.FindOutputTagsArgs): Promise<TableOutputTag[]> { const q = this.findOutputTagsQuery(args) const r = await q return this.validateEntities(r, undefined, ['isDeleted']) } override async findProvenTxReqs(args: sdk.FindProvenTxReqsArgs): Promise<TableProvenTxReq[]> { const q = this.findProvenTxReqsQuery(args) const r = await q return this.validateEntities(r, undefined, ['notified']) } override async findProvenTxs(args: sdk.FindProvenTxsArgs): Promise<TableProvenTx[]> { const q = this.findProvenTxsQuery(args) const r = await q return this.validateEntities(r) } override async findSyncStates(args: sdk.FindSyncStatesArgs): Promise<TableSyncState[]> { const q = this.findSyncStatesQuery(args) const r = await q return this.validateEntities(r, ['when'], ['init']) } override async findTransactions(args: sdk.FindTransactionsArgs): Promise<TableTransaction[]> { 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']) } override async findTxLabelMaps(args: sdk.FindTxLabelMapsArgs): Promise<TableTxLabelMap[]> { const q = this.findTxLabelMapsQuery(args) const r = await q return this.validateEntities(r, undefined, ['isDeleted']) } override async findTxLabels(args: sdk.FindTxLabelsArgs): Promise<TableTxLabel[]> { const q = this.findTxLabelsQuery(args) const r = await q return this.validateEntities(r, undefined, ['isDeleted']) } override async findUsers(args: sdk.FindUsersArgs): Promise<TableUser[]> { const q = this.findUsersQuery(args) const r = await q return this.validateEntities(r) } override async findMonitorEvents(args: sdk.FindMonitorEventsArgs): Promise<TableMonitorEvent[]> { const q = this.findMonitorEventsQuery(args) const r = await q return this.validateEntities(r, ['when'], undefined) } async getCount<T extends object>(q: Knex.QueryBuilder<T, T[]>): Promise<number> { q.count() const r = await q return r[0]['count(*)'] } override async countCertificateFields(args: sdk.FindCertificateFieldsArgs): Promise<number> { return await this.getCount(this.findCertificateFieldsQuery(args)) } override async countCertificates(args: sdk.FindCertificatesArgs): Promise<number> { return await this.getCount(this.findCertificatesQuery(args)) } override async countCommissions(args: sdk.FindCommissionsArgs): Promise<number> { return await this.getCount(this.findCommissionsQuery(args)) } override async countOutputBaskets(args: sdk.FindOutputBasketsArgs): Promise<number> { return await this.getCount(this.findOutputBasketsQuery(args)) } override async countOutputs(args: sdk.FindOutputsArgs): Promise<number> { return await this.getCount(this.findOutputsQuery(args, true)) } override async countOutputTagMaps(args: sdk.FindOutputTagMapsArgs): Promise<number> { return await this.getCount(this.findOutputTagMapsQuery(args)) } override async countOutputTags(args: sdk.FindOutputTagsArgs): Promise<number> { return await this.getCount(this.findOutputTagsQuery(args)) } override async countProvenTxReqs(args: sdk.FindProvenTxReqsArgs): Promise<number> { return await this.getCount(this.findProvenTxReqsQuery(args)) } override async countProvenTxs(args: sdk.FindProvenTxsArgs): Promise<number> { return await this.getCount(this.findProvenTxsQuery(args)) } override async countSyncStates(args: sdk.FindSyncStatesArgs): Promise<number> { return await this.getCount(this.findSyncStatesQuery(args)) } override async countTransactions(args: sdk.FindTransactionsArgs): Promise<number> { return await this.getCount(this.findTransactionsQuery(args, true)) } override async countTxLabelMaps(args: sdk.FindTxLabelMapsArgs): Promise<number> { return await this.getCount(this.findTxLabelMapsQuery(args)) } override async countTxLabels(args: sdk.FindTxLabelsArgs): Promise<number> { return await this.getCount(this.findTxLabelsQuery(args)) } override async countUsers(args: sdk.FindUsersArgs): Promise<number> { return await this.getCount(this.findUsersQuery(args)) } override async countMonitorEvents(args: sdk.FindMonitorEventsArgs): Promise<number> { return await this.getCount(this.findMonitorEventsQuery(args)) } override async destroy(): Promise<void> { await this.knex?.destroy() } override async migrate(storageName: string, storageIdentityKey: string): Promise<string> { const config = { migrationSource: new KnexMigrations(this.chain, storageName, storageIdentityKey, 1024) } await this.knex.migrate.latest(config) const version = await this.knex.migrate.currentVersion(config) return version } override async dropAllData(): Promise<void> { // Only using migrations to migrate down, don't need valid properties for settings table. const config = { migrationSource: new 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: unknown) { break } } } override async transaction<T>(scope: (trx: sdk.TrxToken) => Promise<T>, trx?: sdk.TrxToken): Promise<T> { if (trx) return await scope(trx) return await this.knex.transaction<T>(async knextrx => { const trx = knextrx as sdk.TrxToken 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?: sdk.TrxToken) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const db = !trx ? this.knex : <Knex.Transaction<any, any[]>>trx this.whenLastAccess = new Date() return db } async validateRawTransaction(t: TableTransaction, trx?: sdk.TrxToken): Promise<void> { // 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 } _verifiedReadyForDatabaseAccess: boolean = false /** * Make sure database is ready for access: * * - dateScheme is known * - foreign key constraints are enabled * * @param trx */ async verifyReadyForDatabaseAccess(trx?: sdk.TrxToken): Promise<DBType> { 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<T extends sdk.EntityTimeStamp>( update: Partial<T>, dateFields?: string[], booleanFields?: string[] ): Partial<T> { if (!this.dbtype) throw new sdk.WERR_INTERNAL('must call verifyReadyForDatabaseAccess first') const v: any = 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<T extends sdk.EntityTimeStamp>( entity: T, trx?: sdk.TrxToken, dateFields?: string[], booleanFields?: string[] ): Promise<any> { await this.verifyReadyForDatabaseAccess(trx) const v: any = { ...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 } override async getLabelsForTransactionId(transactionId?: number, trx?: sdk.TrxToken): Promise<TableTxLabel[]> { if (transactionId === undefined) return [] const labels = await this.toDb(trx)<TableTxLabel>('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']) } override async getTagsForOutputId(outputId: number, trx?: sdk.TrxToken): Promise<TableOutputTag[]> { const tags = await this.toDb(trx)<TableOutputTag>('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']) } override async purgeData(params: sdk.PurgeParams, trx?: sdk.TrxToken): Promise<sdk.PurgeResults> { return await purgeData(this, params, trx) } override async reviewStatus(args: { agedLimit: Date; trx?: sdk.TrxToken }): Promise<{ log: string }> { return await 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: number, basketId: number, excludeSending: boolean): Promise<number> { const status: sdk.TransactionStatus[] = ['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<TableOutput>('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: number, basketId: number, targetSatoshis: number, exactSatoshis: number | undefined, excludeSending: boolean, transactionId: number ): Promise<TableOutput | undefined> { const status: sdk.TransactionStatus[] = ['completed', 'unproven'] if (!excludeSending) status.push('sending') const statusText = status.map(s => `'${s}'`).join(',') const r: TableOutput | undefined = await this.knex.transaction(async trx => { const txStatusCondition = `AND (SELECT status FROM transactions WHERE outputs.transactionId = transactions.transactionId) in (${statusText})` let outputId: number | undefined const setOutputId = async (rawQuery: string): Promise<void> => { 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 = 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<T extends sdk.EntityTimeStamp>(entity: T, dateFields?: string[], booleanFields?: string[]): T { 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<T extends sdk.EntityTimeStamp>(entities: T[], dateFields?: string[], booleanFields?: string[]): T[] { for (let i = 0; i < entities.length; i++) { entities[i] = this.validateEntity(entities[i], dateFields, booleanFields) } return entities } async adminStats(adminIdentityKey: string): Promise<AdminStatsResult> { if (this.dbtype !== 'MySQL') throw new sdk.WERR_NOT_IMPLEMENTED('adminStats, only MySQL is supported') const monitorEvent = verifyOneOrNone( await this.findMonitorEvents({ partial: { event: 'MonitorCallHistory' }, orderDescending: true, paged: { limit: 1 } }) ) const monitorStats: ServicesCallHistory | undefined = 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_mont