wallet-storage
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
1,450 lines (1,371 loc) • 46.3 kB
text/typescript
import {
ListActionsArgs,
ListActionsResult,
ListOutputsArgs,
ListOutputsResult
} from '@bsv/sdk'
import { sdk, verifyOne, verifyOneOrNone, verifyTruthy } from '../index.all'
import { KnexMigrations, table } from './index.all'
import { Knex } from 'knex'
import { StorageProvider, StorageProviderOptions } from './StorageProvider'
import { purgeData } from './methods/purgeData'
import { listActions } from './methods/listActions'
import { listOutputs } from './methods/listOutputs'
import { DBType } from './StorageReader'
import { reviewStatus } from './methods/reviewStatus'
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<table.Settings> {
return this.validateEntity(
verifyOne(await this.toDb(undefined)<table.Settings>('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
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')`
)
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', '>=', args.since)
return q
}
override async getProvenTxsForUser(
args: sdk.FindForUserSincePagedArgs
): Promise<table.ProvenTx[]> {
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', '>=', args.since)
return q
}
override async getProvenTxReqsForUser(
args: sdk.FindForUserSincePagedArgs
): Promise<table.ProvenTxReq[]> {
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<table.TxLabelMap[]> {
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<table.OutputTagMap[]> {
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: table.ProvenTx,
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)<table.ProvenTx>('proven_txs').insert(e)
tx.provenTxId = id
return tx.provenTxId
}
override async insertProvenTxReq(
tx: table.ProvenTxReq,
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)<table.ProvenTxReq>('proven_tx_reqs').insert(e)
tx.provenTxReqId = id
return tx.provenTxReqId
}
override async insertUser(
user: table.User,
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)<table.User>('users').insert(e)
user.userId = id
return user.userId
}
override async insertCertificateAuth(
auth: sdk.AuthId,
certificate: table.CertificateX
): 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: table.CertificateX,
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)<table.Certificate>('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: table.CertificateField,
trx?: sdk.TrxToken
): Promise<void> {
const e = await this.validateEntityForInsert(certificateField, trx)
await this.toDb(trx)<table.Certificate>('certificate_fields').insert(e)
}
override async insertOutputBasket(
basket: table.OutputBasket,
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)<table.OutputBasket>('output_baskets').insert(e)
basket.basketId = id
return basket.basketId
}
override async insertTransaction(
tx: table.Transaction,
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)<table.Transaction>('transactions').insert(e)
tx.transactionId = id
return tx.transactionId
}
override async insertCommission(
commission: table.Commission,
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)<table.Commission>('commissions').insert(e)
commission.commissionId = id
return commission.commissionId
}
override async insertOutput(
output: table.Output,
trx?: sdk.TrxToken
): Promise<number> {
const e = await this.validateEntityForInsert(output, trx)
if (e.outputId === 0) delete e.outputId
const [id] = await this.toDb(trx)<table.Output>('outputs').insert(e)
output.outputId = id
return output.outputId
}
override async insertOutputTag(
tag: table.OutputTag,
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)<table.OutputTag>('output_tags').insert(e)
tag.outputTagId = id
return tag.outputTagId
}
override async insertOutputTagMap(
tagMap: table.OutputTagMap,
trx?: sdk.TrxToken
): Promise<void> {
const e = await this.validateEntityForInsert(tagMap, trx, undefined, [
'isDeleted'
])
const [id] =
await this.toDb(trx)<table.OutputTagMap>('output_tags_map').insert(e)
}
override async insertTxLabel(
label: table.TxLabel,
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)<table.TxLabel>('tx_labels').insert(e)
label.txLabelId = id
return label.txLabelId
}
override async insertTxLabelMap(
labelMap: table.TxLabelMap,
trx?: sdk.TrxToken
): Promise<void> {
const e = await this.validateEntityForInsert(labelMap, trx, undefined, [
'isDeleted'
])
const [id] =
await this.toDb(trx)<table.TxLabelMap>('tx_labels_map').insert(e)
}
override async insertMonitorEvent(
event: table.MonitorEvent,
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)<table.MonitorEvent>('monitor_events').insert(e)
event.id = id
return event.id
}
override async insertSyncState(
syncState: table.SyncState,
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)<table.SyncState>('sync_states').insert(e)
syncState.syncStateId = id
return syncState.syncStateId
}
override async updateCertificateField(
certificateId: number,
fieldName: string,
update: Partial<table.CertificateField>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.CertificateField>('certificate_fields')
.where({ certificateId, fieldName })
.update(this.validatePartialForUpdate(update))
}
override async updateCertificate(
id: number,
update: Partial<table.Certificate>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.Certificate>('certificates')
.where({ certificateId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']))
}
override async updateCommission(
id: number,
update: Partial<table.Commission>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.Commission>('commissions')
.where({ commissionId: id })
.update(this.validatePartialForUpdate(update))
}
override async updateOutputBasket(
id: number,
update: Partial<table.OutputBasket>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.OutputBasket>('output_baskets')
.where({ basketId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']))
}
override async updateOutput(
id: number,
update: Partial<table.Output>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.Output>('outputs')
.where({ outputId: id })
.update(this.validatePartialForUpdate(update))
}
override async updateOutputTagMap(
outputId: number,
tagId: number,
update: Partial<table.OutputTagMap>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.OutputTagMap>('output_tags_map')
.where({ outputId, outputTagId: tagId })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']))
}
override async updateOutputTag(
id: number,
update: Partial<table.OutputTag>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.OutputTag>('output_tags')
.where({ outputTagId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']))
}
override async updateProvenTxReq(
id: number | number[],
update: Partial<table.ProvenTxReq>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
let r: number
if (Array.isArray(id)) {
r = await this.toDb(trx)<table.ProvenTxReq>('proven_tx_reqs')
.whereIn('provenTxReqId', id)
.update(this.validatePartialForUpdate(update))
} else if (Number.isInteger(id)) {
r = await this.toDb(trx)<table.ProvenTxReq>('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<table.ProvenTx>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.ProvenTx>('proven_txs')
.where({ provenTxId: id })
.update(this.validatePartialForUpdate(update))
}
override async updateSyncState(
id: number,
update: Partial<table.SyncState>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.SyncState>('sync_states')
.where({ syncStateId: id })
.update(this.validatePartialForUpdate(update, ['when'], ['init']))
}
override async updateTransaction(
id: number | number[],
update: Partial<table.Transaction>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
let r: number
if (Array.isArray(id)) {
r = await this.toDb(trx)<table.Transaction>('transactions')
.whereIn('transactionId', id)
.update(await this.validatePartialForUpdate(update))
} else if (Number.isInteger(id)) {
r = await this.toDb(trx)<table.Transaction>('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<table.TxLabelMap>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.TxLabelMap>('tx_labels_map')
.where({ transactionId, txLabelId })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']))
}
override async updateTxLabel(
id: number,
update: Partial<table.TxLabel>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.TxLabel>('tx_labels')
.where({ txLabelId: id })
.update(this.validatePartialForUpdate(update, undefined, ['isDeleted']))
}
override async updateUser(
id: number,
update: Partial<table.User>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.User>('users')
.where({ userId: id })
.update(this.validatePartialForUpdate(update))
}
override async updateMonitorEvent(
id: number,
update: Partial<table.MonitorEvent>,
trx?: sdk.TrxToken
): Promise<number> {
await this.verifyReadyForDatabaseAccess(trx)
return await this.toDb(trx)<table.MonitorEvent>('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.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 = table.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 && args.txids.length > 0) q.whereIn('txid', args.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 = table.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<table.Certificate[]> {
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<table.OutputBasket[]> {
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<table.Output[]> {
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<table.CertificateField[]> {
return this.validateEntities(await this.findCertificateFieldsQuery(args))
}
override async findCertificates(
args: sdk.FindCertificatesArgs
): Promise<table.Certificate[]> {
const q = this.findCertificatesQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isDeleted'])
}
override async findCommissions(
args: sdk.FindCommissionsArgs
): Promise<table.Commission[]> {
const q = this.findCommissionsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isRedeemed'])
}
override async findOutputBaskets(
args: sdk.FindOutputBasketsArgs
): Promise<table.OutputBasket[]> {
const q = this.findOutputBasketsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isDeleted'])
}
override async findOutputs(
args: sdk.FindOutputsArgs
): Promise<table.Output[]> {
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<table.OutputTagMap[]> {
const q = this.findOutputTagMapsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isDeleted'])
}
override async findOutputTags(
args: sdk.FindOutputTagsArgs
): Promise<table.OutputTag[]> {
const q = this.findOutputTagsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isDeleted'])
}
override async findProvenTxReqs(
args: sdk.FindProvenTxReqsArgs
): Promise<table.ProvenTxReq[]> {
const q = this.findProvenTxReqsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['notified'])
}
override async findProvenTxs(
args: sdk.FindProvenTxsArgs
): Promise<table.ProvenTx[]> {
const q = this.findProvenTxsQuery(args)
const r = await q
return this.validateEntities(r)
}
override async findSyncStates(
args: sdk.FindSyncStatesArgs
): Promise<table.SyncState[]> {
const q = this.findSyncStatesQuery(args)
const r = await q
return this.validateEntities(r, ['when'], ['init'])
}
override async findTransactions(
args: sdk.FindTransactionsArgs
): Promise<table.Transaction[]> {
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<table.TxLabelMap[]> {
const q = this.findTxLabelMapsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isDeleted'])
}
override async findTxLabels(
args: sdk.FindTxLabelsArgs
): Promise<table.TxLabel[]> {
const q = this.findTxLabelsQuery(args)
const r = await q
return this.validateEntities(r, undefined, ['isDeleted'])
}
override async findUsers(args: sdk.FindUsersArgs): Promise<table.User[]> {
const q = this.findUsersQuery(args)
const r = await q
return this.validateEntities(r)
}
override async findMonitorEvents(
args: sdk.FindMonitorEventsArgs
): Promise<table.MonitorEvent[]> {
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)
expect(r).toBeTruthy()
} 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: table.Transaction,
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
}
async validateOutputScript(
o: table.Output,
trx?: sdk.TrxToken
): Promise<void> {
// without offset and length values return what we have (make no changes)
if (!o.scriptLength || !o.scriptOffset || !o.txid) return
// if there is an outputScript and its length is the expected length return what we have.
if (o.lockingScript && o.lockingScript.length === o.scriptLength) return
// outputScript is missing or has incorrect length...
const script = await this.getRawTxOfKnownValidTransaction(
o.txid,
o.scriptOffset,
o.scriptLength,
trx
)
if (!script) return
o.lockingScript = script
}
_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<table.TxLabel[]> {
if (transactionId === undefined) return []
const labels = await this.toDb(trx)<table.TxLabel>('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 extendOutput(
o: table.Output,
includeBasket = false,
includeTags = false,
trx?: sdk.TrxToken
): Promise<table.OutputX> {
const ox = o as table.OutputX
if (includeBasket && ox.basketId)
ox.basket = await this.findOutputBasketById(o.basketId!, trx)
if (includeTags) {
ox.tags = await this.getTagsForOutputId(o.outputId)
}
return o
}
override async getTagsForOutputId(
outputId: number,
trx?: sdk.TrxToken
): Promise<table.OutputTag[]> {
const tags = await this.toDb(trx)<table.OutputTag>('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)
}
/**
* Finds closest matching available change output to use as input for new transaction.
*
* Transactionally allocate the output such that
*/
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<table.Output>('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<table.Output | undefined> {
const status: sdk.TransactionStatus[] = ['completed', 'unproven']
if (!excludeSending) status.push('sending')
const statusText = status.map(s => `'${s}'`).join(',')
const r: table.Output | 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
}
}