@bsv/overlay
Version:
BSV Blockchain Overlay Services Engine
544 lines • 21.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KnexStorage = void 0;
const sdk_1 = require("@bsv/sdk");
const BASM_js_1 = require("../../BASM.js");
const OUTPUT_SELECT_FIELDS = [
'outputs.txid',
'outputs.outputIndex',
'outputs.outputScript',
'outputs.topic',
'outputs.satoshis',
'outputs.outputsConsumed',
'outputs.spent',
'outputs.consumedBy',
'outputs.blockHeight',
'outputs.score'
];
class KnexStorage {
constructor(knex) {
this.knex = knex;
}
parseOutputRelations(value) {
if (Array.isArray(value)) {
return value;
}
return JSON.parse(value);
}
parseOutputRecord(row, includeBEEF, beefOverride) {
return {
...row,
outputScript: Array.from(row.outputScript),
beef: (() => {
if (!includeBEEF)
return undefined;
if (beefOverride != null)
return beefOverride;
return row.beef != null ? Array.from(row.beef) : undefined;
})(),
spent: Boolean(row.spent),
outputsConsumed: this.parseOutputRelations(row.outputsConsumed),
consumedBy: this.parseOutputRelations(row.consumedBy)
};
}
binaryToNumberArray(value) {
if (value === undefined || value === null) {
return undefined;
}
return Array.from(Buffer.from(value));
}
binaryToHex(value) {
const array = this.binaryToNumberArray(value);
if (array === undefined) {
return undefined;
}
return Buffer.from(array).toString('hex');
}
transactionRecordFromBEEF(txid, beef) {
var _a;
const tx = sdk_1.Transaction.fromBEEF(beef);
const metadata = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath);
return {
txid,
beef: tx.merklePath === undefined ? beef : tx.toAtomicBEEF(),
rawTx: Array.from(tx.toBinary()),
merklePath: (_a = tx.merklePath) === null || _a === void 0 ? void 0 : _a.toBinary(),
blockHeight: metadata === null || metadata === void 0 ? void 0 : metadata.blockHeight,
blockIndex: metadata === null || metadata === void 0 ? void 0 : metadata.blockIndex,
merkleRoot: metadata === null || metadata === void 0 ? void 0 : metadata.merkleRoot
};
}
async fetchTransactionBeefMap(txids) {
if (txids.length === 0) {
return new Map();
}
const rows = await this.knex('transactions')
.whereIn('txid', txids)
.select(['txid', 'beef']);
const beefByTxid = new Map();
for (const row of rows) {
if (row.beef !== undefined && row.beef !== null) {
beefByTxid.set(row.txid, Array.from(row.beef));
}
}
return beefByTxid;
}
async findOutput(txid, outputIndex, topic, spent, includeBEEF = false) {
const search = {
'outputs.txid': txid,
'outputs.outputIndex': outputIndex
};
if (topic !== undefined)
search['outputs.topic'] = topic;
if (spent !== undefined)
search['outputs.spent'] = spent;
const query = this.knex('outputs').where(search);
const selectFields = [...OUTPUT_SELECT_FIELDS];
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 this.parseOutputRecord(output, includeBEEF);
}
async findOutputsByOutpoints(outpoints, includeBEEF = false) {
if (outpoints.length === 0) {
return [];
}
const deduped = new Map();
for (const outpoint of outpoints) {
deduped.set(`${outpoint.txid}:${outpoint.outputIndex}`, outpoint);
}
const rows = await this.knex('outputs')
.whereIn(['outputs.txid', 'outputs.outputIndex'], Array.from(deduped.values()).map(outpoint => [outpoint.txid, outpoint.outputIndex]))
.select([...OUTPUT_SELECT_FIELDS]);
if (rows === undefined || rows.length === 0) {
return [];
}
if (!includeBEEF) {
return rows.map(row => this.parseOutputRecord(row, false));
}
const txids = Array.from(new Set(rows.map(row => row.txid)));
const beefByTxid = await this.fetchTransactionBeefMap(txids);
return rows.map(row => this.parseOutputRecord(row, true, beefByTxid.get(row.txid)));
}
async findOutputsForTransaction(txid, includeBEEF = false) {
const outputs = await this.knex('outputs')
.where({ 'outputs.txid': txid })
.select([...OUTPUT_SELECT_FIELDS]);
if (outputs === undefined || outputs.length === 0) {
return [];
}
if (!includeBEEF) {
return outputs.map(output => this.parseOutputRecord(output, false));
}
const beefByTxid = await this.fetchTransactionBeefMap([txid]);
return outputs.map(output => this.parseOutputRecord(output, true, beefByTxid.get(output.txid)));
}
async findUTXOsForTopic(topic, since, limit, includeBEEF = false) {
// 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');
// 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([...OUTPUT_SELECT_FIELDS]);
if (outputs === undefined || outputs.length === 0) {
return [];
}
if (!includeBEEF) {
return outputs.map(output => this.parseOutputRecord(output, false));
}
const txids = Array.from(new Set(outputs.map(output => output.txid)));
const beefByTxid = await this.fetchTransactionBeefMap(txids);
return outputs.map(output => this.parseOutputRecord(output, true, beefByTxid.get(output.txid)));
}
async deleteOutput(txid, outputIndex, topic) {
await this.knex.transaction(async (trx) => {
// Delete the specific output
await trx('outputs').where({ txid, outputIndex, topic }).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) {
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,
blockHeight: output.blockHeight
});
}
if (output.beef !== undefined) {
const record = this.transactionRecordFromBEEF(output.txid, output.beef);
const transactionRecord = {
txid: output.txid,
updatedAt: new Date()
};
if (record.beef !== undefined)
transactionRecord.beef = Buffer.from(record.beef);
if (record.rawTx !== undefined)
transactionRecord.rawTx = Buffer.from(record.rawTx);
if (record.merklePath !== undefined)
transactionRecord.merklePath = Buffer.from(record.merklePath);
if (record.blockHeight !== undefined)
transactionRecord.blockHeight = record.blockHeight;
if (record.blockIndex !== undefined)
transactionRecord.blockIndex = record.blockIndex;
if (record.merkleRoot !== undefined)
transactionRecord.merkleRoot = record.merkleRoot;
const mergeRecord = { ...transactionRecord };
delete mergeRecord.txid;
await trx('transactions').insert(transactionRecord).onConflict('txid').merge(mergeRecord);
}
});
}
async markUTXOAsSpent(txid, outputIndex, topic) {
await this.knex('outputs').where({
txid,
outputIndex,
topic
}).update('spent', true);
}
async updateConsumedBy(txid, outputIndex, topic, consumedBy) {
await this.knex('outputs').where({
txid,
outputIndex,
topic
}).update('consumedBy', JSON.stringify(consumedBy));
}
async updateTransactionBEEF(txid, beef) {
await this.upsertTransactionRecord(this.transactionRecordFromBEEF(txid, beef));
}
async updateOutputBlockHeight(txid, outputIndex, topic, blockHeight) {
await this.knex('outputs').where({
txid,
outputIndex,
topic
}).update('blockHeight', blockHeight);
}
async upsertTransactionRecord(record) {
const insert = {
txid: record.txid,
updatedAt: new Date()
};
if (record.beef !== undefined)
insert.beef = Buffer.from(record.beef);
if (record.rawTx !== undefined)
insert.rawTx = Buffer.from(record.rawTx);
if (record.merklePath !== undefined)
insert.merklePath = Buffer.from(record.merklePath);
if (record.blockHeight !== undefined)
insert.blockHeight = record.blockHeight;
if (record.blockHash !== undefined)
insert.blockHash = record.blockHash;
if (record.blockIndex !== undefined)
insert.blockIndex = record.blockIndex;
if (record.merkleRoot !== undefined)
insert.merkleRoot = record.merkleRoot;
const merge = { ...insert };
delete merge.txid;
await this.knex('transactions')
.insert(insert)
.onConflict('txid')
.merge(merge);
}
async updateAppliedTransactionProof(record) {
await this.knex('applied_transactions')
.where({ txid: record.txid, topic: record.topic })
.update({
blockHeight: record.blockHeight,
blockHash: record.blockHash,
blockIndex: record.blockIndex,
merkleRoot: record.merkleRoot,
proven: true
});
if (record.blockHeight !== undefined) {
await this.knex('outputs')
.where({ txid: record.txid, topic: record.topic })
.update({ blockHeight: record.blockHeight });
}
}
async insertAppliedTransaction(tx) {
var _a;
await this.knex('applied_transactions').insert({
txid: tx.txid,
topic: tx.topic,
blockHeight: tx.blockHeight,
blockHash: tx.blockHash,
blockIndex: tx.blockIndex,
merkleRoot: tx.merkleRoot,
firstSeenHeight: tx.firstSeenHeight,
proven: (_a = tx.proven) !== null && _a !== void 0 ? _a : false
});
}
async doesAppliedTransactionExist(tx) {
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, topic, since) {
await this.knex('host_sync_state')
.insert({ host, topic, since })
.onConflict(['host', 'topic'])
.merge({ since });
}
async getLastInteraction(host, topic) {
const result = await this.knex('host_sync_state')
.where({ host, topic })
.select('since')
.first();
return result ? result.since : 0;
}
async findAdmittedTransactionsForBlock(topic, blockHeight, blockHash) {
let query = this.knex('applied_transactions')
.where({ topic, blockHeight, proven: true })
.whereNotNull('blockIndex');
if (blockHash !== undefined) {
query = query.andWhere({ blockHash });
}
const rows = await query
.select(['txid', 'blockIndex'])
.orderBy('blockIndex', 'asc');
return rows.map(row => ({
txid: row.txid,
blockIndex: Number(row.blockIndex)
}));
}
async upsertTopicBlockAnchor(anchor) {
await this.knex('topic_block_anchors')
.insert({
topic: anchor.topic,
blockHeight: anchor.blockHeight,
blockHash: anchor.blockHash,
basmRoot: anchor.basmRoot,
admittedCount: anchor.admittedCount,
tac: anchor.tac,
updatedAt: new Date()
})
.onConflict(['topic', 'blockHeight'])
.merge({
blockHash: anchor.blockHash,
basmRoot: anchor.basmRoot,
admittedCount: anchor.admittedCount,
tac: anchor.tac,
updatedAt: new Date()
});
}
async findTopicBlockAnchor(topic, blockHeight, blockHash) {
let query = this.knex('topic_block_anchors').where({ topic, blockHeight });
if (blockHash !== undefined) {
query = query.andWhere({ blockHash });
}
const row = await query.first();
if (row === undefined) {
return undefined;
}
return {
topic: row.topic,
blockHeight: Number(row.blockHeight),
blockHash: row.blockHash,
basmRoot: row.basmRoot,
admittedCount: Number(row.admittedCount),
tac: row.tac
};
}
async findTopicBlockAnchors(topic, fromHeight, toHeight) {
const rows = await this.knex('topic_block_anchors')
.where({ topic })
.andWhere('blockHeight', '>=', fromHeight)
.andWhere('blockHeight', '<=', toHeight)
.select(['topic', 'blockHeight', 'blockHash', 'basmRoot', 'admittedCount', 'tac'])
.orderBy('blockHeight', 'asc');
return rows.map(row => ({
topic: row.topic,
blockHeight: Number(row.blockHeight),
blockHash: row.blockHash,
basmRoot: row.basmRoot,
admittedCount: Number(row.admittedCount),
tac: row.tac
}));
}
async findTopicAnchorTip(topic) {
const row = await this.knex('topic_block_anchors')
.where({ topic })
.orderBy('blockHeight', 'desc')
.first();
if (row === undefined) {
return {
topic,
blockHeight: -1,
tac: BASM_js_1.BASM_ZERO_HASH
};
}
return {
topic: row.topic,
blockHeight: Number(row.blockHeight),
blockHash: row.blockHash,
basmRoot: row.basmRoot,
admittedCount: Number(row.admittedCount),
tac: row.tac
};
}
async findRawTransactions(txids) {
if (txids.length === 0)
return [];
const rows = await this.knex('transactions')
.whereIn('txid', txids)
.select(['txid', 'rawTx', 'beef']);
const records = [];
for (const row of rows) {
let rawTx = this.binaryToHex(row.rawTx);
if (rawTx === undefined) {
const beef = this.binaryToNumberArray(row.beef);
if (beef !== undefined) {
rawTx = sdk_1.Transaction.fromBEEF(beef, row.txid).toHex();
}
}
if (rawTx !== undefined) {
records.push({ txid: row.txid, rawTx });
}
}
return records;
}
async findTransactionMerklePaths(txids) {
var _a;
if (txids.length === 0)
return [];
const rows = await this.knex('transactions')
.whereIn('txid', txids)
.select(['txid', 'merklePath', 'blockHeight', 'blockHash', 'blockIndex', 'merkleRoot', 'beef']);
const records = [];
for (const row of rows) {
let merklePath = this.binaryToHex(row.merklePath);
if (merklePath === undefined) {
const beef = this.binaryToNumberArray(row.beef);
if (beef !== undefined) {
merklePath = (_a = sdk_1.Transaction.fromBEEF(beef, row.txid).merklePath) === null || _a === void 0 ? void 0 : _a.toHex();
}
}
if (merklePath !== undefined) {
records.push({
txid: row.txid,
merklePath,
blockHeight: row.blockHeight === undefined || row.blockHeight === null ? undefined : Number(row.blockHeight),
blockHash: row.blockHash,
blockIndex: row.blockIndex === undefined || row.blockIndex === null ? undefined : Number(row.blockIndex),
merkleRoot: row.merkleRoot
});
}
}
return records;
}
async findUnprovenAppliedTransactions(cutoffHeight, topic) {
let query = this.knex('applied_transactions')
.where(builder => {
builder.where({ proven: false }).orWhereNull('proven');
})
.whereNotNull('firstSeenHeight')
.andWhere('firstSeenHeight', '<=', cutoffHeight)
.select(['txid', 'topic', 'firstSeenHeight']);
if (topic !== undefined) {
query = query.andWhere({ topic });
}
const rows = await query;
const candidates = [];
for (const row of rows) {
const outputs = await this.knex('outputs')
.where({ txid: row.txid, topic: row.topic })
.select(['txid', 'outputIndex']);
candidates.push({
txid: row.txid,
topic: row.topic,
firstSeenHeight: row.firstSeenHeight === undefined || row.firstSeenHeight === null ? undefined : Number(row.firstSeenHeight),
outputs: outputs.map(output => ({
txid: output.txid,
outputIndex: Number(output.outputIndex)
}))
});
}
return candidates;
}
async deleteAppliedTransaction(txid, topic) {
await this.knex('applied_transactions').where({ txid, topic }).del();
}
async findProvenAppliedTransactionsByBlockHash(blockHash) {
const rows = await this.knex('applied_transactions')
.where({ blockHash, proven: true })
.whereNotNull('blockHeight')
.select(['txid', 'topic', 'blockHeight']);
return rows.map(row => ({
txid: row.txid,
topic: row.topic,
blockHeight: Number(row.blockHeight)
}));
}
async findProvenAppliedTransactionsInRange(fromHeight, toHeight, topic) {
let query = this.knex('applied_transactions')
.where({ proven: true })
.andWhere('blockHeight', '>=', fromHeight)
.andWhere('blockHeight', '<=', toHeight);
if (topic !== undefined) {
query = query.andWhere({ topic });
}
const rows = await query.select(['txid', 'topic', 'blockHeight', 'blockHash', 'merkleRoot']);
return rows.map(row => {
var _a, _b;
return ({
txid: row.txid,
topic: row.topic,
blockHeight: Number(row.blockHeight),
blockHash: (_a = row.blockHash) !== null && _a !== void 0 ? _a : undefined,
merkleRoot: (_b = row.merkleRoot) !== null && _b !== void 0 ? _b : undefined
});
});
}
async demoteAppliedTransactionToUnproven(txid, topic) {
await this.knex('applied_transactions')
.where({ txid, topic })
.update({
blockHeight: null,
blockHash: null,
blockIndex: null,
merkleRoot: null,
proven: false
});
await this.knex('outputs')
.where({ txid, topic })
.update({ blockHeight: null });
}
}
exports.KnexStorage = KnexStorage;
//# sourceMappingURL=KnexStorage.js.map