@bsv/overlay
Version:
BSV Blockchain Overlay Services Engine
265 lines (224 loc) • 8.54 kB
text/typescript
import { Storage } from '../Storage.js'
import { Knex } from 'knex'
import type { Output } from '../../Output.js'
export class KnexStorage implements Storage {
knex: Knex
constructor (knex: Knex) {
this.knex = knex
}
async findOutput (txid: string, outputIndex: number, topic?: string, spent?: boolean, includeBEEF: boolean = false): Promise<Output | null> {
const search: {
'outputs.txid': string
'outputs.outputIndex': number
'outputs.topic'?: string
'outputs.spent'?: boolean
} = {
'outputs.txid': txid,
'outputs.outputIndex': outputIndex
}
if (topic !== undefined) search['outputs.topic'] = topic
if (spent !== undefined) search['outputs.spent'] = spent
// Base query to get the output
const query = this.knex('outputs').where(search)
// Select necessary fields from outputs and conditionally include beef from transactions
const selectFields = [
'outputs.txid',
'outputs.outputIndex',
'outputs.outputScript',
'outputs.topic',
'outputs.satoshis',
'outputs.outputsConsumed',
'outputs.spent',
'outputs.consumedBy',
'outputs.score'
]
if (includeBEEF) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
query.leftJoin('transactions', 'outputs.txid', 'transactions.txid')
selectFields.push('transactions.beef')
}
const output = await query.select(selectFields).first()
if (output === undefined || output === null) {
return null
}
return {
...output,
outputScript: [...output.outputScript],
beef: includeBEEF ? (output.beef !== undefined ? [...output.beef] : undefined) : undefined,
spent: Boolean(output.spent),
outputsConsumed: JSON.parse(output.outputsConsumed),
consumedBy: JSON.parse(output.consumedBy)
}
}
async findOutputsForTransaction (txid: string, includeBEEF: boolean = false): Promise<Output[]> {
// Base query to get outputs
const query = this.knex('outputs').where({ 'outputs.txid': txid })
// Select necessary fields from outputs and conditionally include beef from transactions
const selectFields = [
'outputs.txid',
'outputs.outputIndex',
'outputs.outputScript',
'outputs.topic',
'outputs.satoshis',
'outputs.outputsConsumed',
'outputs.spent',
'outputs.consumedBy',
'outputs.score'
]
if (includeBEEF) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
query.leftJoin('transactions', 'outputs.txid', 'transactions.txid')
selectFields.push('transactions.beef')
}
const outputs = await query.select(selectFields)
if (outputs === undefined || outputs.length === 0) {
return []
}
return outputs.map(output => ({
...output,
outputScript: [...output.outputScript],
beef: includeBEEF ? (output.beef !== undefined ? [...output.beef] : undefined) : undefined,
spent: Boolean(output.spent),
outputsConsumed: JSON.parse(output.outputsConsumed),
consumedBy: JSON.parse(output.consumedBy)
}))
}
async findUTXOsForTopic (topic: string, since?: number, limit?: number, includeBEEF: boolean = false): Promise<Output[]> {
// Base query to get outputs
const query = this.knex('outputs').where({ 'outputs.topic': topic, 'outputs.spent': false })
// If provided, additionally filters UTXOs by score
if (since !== undefined && since > 0) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
query.andWhere('outputs.score', '>=', since)
}
// Sort by score
// eslint-disable-next-line @typescript-eslint/no-floating-promises
query.orderBy('outputs.score', 'asc')
// Select necessary fields from outputs and conditionally include beef from transactions
const selectFields = [
'outputs.txid',
'outputs.outputIndex',
'outputs.outputScript',
'outputs.topic',
'outputs.satoshis',
'outputs.outputsConsumed',
'outputs.spent',
'outputs.consumedBy',
'outputs.score'
]
if (includeBEEF) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
query.leftJoin('transactions', 'outputs.txid', 'transactions.txid')
selectFields.push('transactions.beef')
}
// Apply limit if specified
if (limit !== undefined && limit > 0) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
query.limit(limit)
}
const outputs = await query.select(selectFields)
if (outputs === undefined || outputs.length === 0) {
return []
}
return outputs.map(output => ({
...output,
outputScript: [...output.outputScript],
beef: includeBEEF ? (output.beef !== undefined ? [...output.beef] : undefined) : undefined,
spent: Boolean(output.spent),
outputsConsumed: JSON.parse(output.outputsConsumed),
consumedBy: JSON.parse(output.consumedBy)
}))
}
async deleteOutput (txid: string, outputIndex: number, _: string): Promise<void> {
await this.knex.transaction(async trx => {
// Delete the specific output
await trx('outputs').where({ txid, outputIndex }).del()
// Check how many outputs reference the same transaction
const remainingOutputs = await trx('outputs').where({ txid }).count('* as count').first()
if (remainingOutputs !== undefined && Number(remainingOutputs.count) === 0) {
// If no more outputs reference the transaction, delete the beef
await trx('transactions').where({ txid }).del()
}
})
}
async insertOutput (output: Output): Promise<void> {
await this.knex.transaction(async trx => {
const existing = await trx('outputs').where({
txid: output.txid,
outputIndex: Number(output.outputIndex),
topic: output.topic
}).first()
if (existing === undefined || existing === null) {
await trx('outputs').insert({
txid: output.txid,
outputIndex: Number(output.outputIndex),
outputScript: Buffer.from(output.outputScript),
topic: output.topic,
satoshis: Number(output.satoshis),
outputsConsumed: JSON.stringify(output.outputsConsumed),
consumedBy: JSON.stringify(output.consumedBy),
spent: output.spent,
score: output.score
})
}
if (output.beef !== undefined) {
await trx('transactions').insert({
txid: output.txid,
beef: Buffer.from(output.beef)
}).onConflict('txid').ignore()
}
})
}
async markUTXOAsSpent (txid: string, outputIndex: number, topic?: string): Promise<void> {
await this.knex('outputs').where({
txid,
outputIndex,
topic
}).update('spent', true)
}
async updateConsumedBy (txid: string, outputIndex: number, topic: string, consumedBy: Array<{ txid: string, outputIndex: number }>): Promise<void> {
await this.knex('outputs').where({
txid,
outputIndex,
topic
}).update('consumedBy', JSON.stringify(consumedBy))
}
async updateTransactionBEEF (txid: string, beef: number[]): Promise<void> {
await this.knex('transactions').where({
txid
}).update('beef', Buffer.from(beef))
}
async updateOutputBlockHeight (txid: string, outputIndex: number, topic: string, blockHeight: number): Promise<void> {
await this.knex('outputs').where({
txid,
outputIndex,
topic
}).update('blockHeight', blockHeight)
}
async insertAppliedTransaction (tx: { txid: string, topic: string }): Promise<void> {
await this.knex('applied_transactions').insert({
txid: tx.txid,
topic: tx.topic
})
}
async doesAppliedTransactionExist (tx: { txid: string, topic: string }): Promise<boolean> {
const result = await this.knex('applied_transactions')
.where({ txid: tx.txid, topic: tx.topic })
.select(this.knex.raw('1'))
.first()
return !!result
}
async updateLastInteraction (host: string, topic: string, since: number): Promise<void> {
await this.knex('host_sync_state')
.insert({ host, topic, since })
.onConflict(['host', 'topic'])
.merge({ since })
}
async getLastInteraction (host: string, topic: string): Promise<number> {
const result = await this.knex('host_sync_state')
.where({ host, topic })
.select('since')
.first()
return result ? result.since : 0
}
}