UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

378 lines 17.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChaintracksStorageKnex = void 0; const ChaintracksKnexMigrations_1 = require("./ChaintracksKnexMigrations"); const ChaintracksStorageBase_1 = require("./ChaintracksStorageBase"); const blockHeaderUtilities_1 = require("../util/blockHeaderUtilities"); const utilityHelpers_1 = require("../../../../utility/utilityHelpers"); const HeightRange_1 = require("../util/HeightRange"); const WERR_errors_1 = require("../../../../sdk/WERR_errors"); const KnexMigrations_1 = require("../../../../storage/schema/KnexMigrations"); /** * Implements the ChaintracksStorageApi using Knex.js for both MySql and Sqlite support. * Also see `chaintracksStorageMemory` which leverages Knex support for an in memory database. */ class ChaintracksStorageKnex extends ChaintracksStorageBase_1.ChaintracksStorageBase { static createStorageKnexOptions(chain, knex) { const options = { ...ChaintracksStorageBase_1.ChaintracksStorageBase.createStorageBaseOptions(chain), knex }; return options; } constructor(options) { super(options); this.bulkFilesTableName = 'bulk_files'; this.headerTableName = `live_headers`; if (!options.knex) throw new Error('The knex options property is required.'); this.knex = options.knex; } get dbtype() { if (!this._dbtype) throw new WERR_errors_1.WERR_INVALID_OPERATION('must call makeAvailable first'); return this._dbtype; } async shutdown() { try { await this.knex.destroy(); } catch (_a) { /* ignore */ } } async makeAvailable() { if (this.isAvailable && this.hasMigrated) return; // Not a base class policy, but we want to ensure migrations are run before getting to business. if (!this.hasMigrated) { await this.migrateLatest(); } if (!this.isAvailable) { this._dbtype = await (0, KnexMigrations_1.determineDBType)(this.knex); await super.makeAvailable(); // Connect the bulk data file manager to the table provided by this storage class. await this.bulkManager.setStorage(this, this.log); } } async migrateLatest() { if (this.hasMigrated) return; await this.knex.migrate.latest({ migrationSource: new ChaintracksKnexMigrations_1.ChaintracksKnexMigrations(this.chain) }); await super.migrateLatest(); } async dropAllData() { // Only using migrations to migrate down, don't need valid properties for settings table. const config = { migrationSource: new ChaintracksKnexMigrations_1.ChaintracksKnexMigrations('test') }; const count = Object.keys(config.migrationSource.migrations).length; for (let i = 0; i < count; i++) { try { const r = await this.knex.migrate.down(config); if (!r) { console.error(`Migration returned falsy result await this.knex.migrate.down(config)`); break; } } catch (eu) { break; } } this.hasMigrated = false; await super.dropAllData(); } async destroy() { await this.knex.destroy(); } async findLiveHeightRange() { var _a, _b; return new HeightRange_1.HeightRange(((_a = (await this.knex(this.headerTableName).where({ isActive: true }).min('height as v')).pop()) === null || _a === void 0 ? void 0 : _a.v) || 0, ((_b = (await this.knex(this.headerTableName).where({ isActive: true }).max('height as v')).pop()) === null || _b === void 0 ? void 0 : _b.v) || -1); } async findLiveHeaderForHeaderId(headerId) { const [header] = await this.knex(this.headerTableName).where({ headerId: headerId }); if (!header) throw new Error(`HeaderId ${headerId} not found in live header database.`); return header; } async findChainTipHeader() { const [tip] = await this.knex(this.headerTableName).where({ isActive: true, isChainTip: true }); if (!tip) throw new Error('Database contains no active chain tip header.'); return tip; } async findChainTipHeaderOrUndefined() { const [tip] = await this.knex(this.headerTableName).where({ isActive: true, isChainTip: true }); return tip; } async findLiveHeaderForHeight(height) { const [header] = await this.knex(this.headerTableName).where({ height: height, isActive: true }); return header ? header : null; } async findLiveHeaderForBlockHash(hash) { const [header] = await this.knex(this.headerTableName).where({ hash: hash }); const result = header ? header : null; return result; } async findLiveHeaderForMerkleRoot(merkleRoot) { const [header] = await this.knex(this.headerTableName).where({ merkleRoot: merkleRoot }); return header; } async deleteBulkFile(fileId) { const count = await this.knex(this.bulkFilesTableName).where({ fileId: fileId }).del(); return count; } async insertBulkFile(file) { if (!file.fileId) delete file.fileId; const [id] = await this.knex(this.bulkFilesTableName).insert(file); file.fileId = id; return id; } async updateBulkFile(fileId, file) { const n = await this.knex(this.bulkFilesTableName).where({ fileId: fileId }).update(file); return n; } async getBulkFiles() { const files = await this.knex(this.bulkFilesTableName) .select('fileId', 'chain', 'fileName', 'firstHeight', 'count', 'prevHash', 'lastHash', 'fileHash', 'prevChainWork', 'lastChainWork', 'validated', 'sourceUrl') .orderBy('firstHeight', 'asc'); return files; } dbTypeSubstring(source, fromOffset, forLength) { if (this.dbtype === 'MySQL') return `substring(${source} from ${fromOffset} for ${forLength})`; return `substr(${source}, ${fromOffset}, ${forLength})`; } async getBulkFileData(fileId, offset, length) { await this.makeAvailable(); if (!Number.isInteger(fileId)) throw new WERR_errors_1.WERR_INVALID_PARAMETER('fileId', 'a valid, integer bulk_files fileId'); let data = undefined; if (Number.isInteger(offset) && Number.isInteger(length)) { let rs = await this.knex.raw(`select ${this.dbTypeSubstring('data', offset + 1, length)} as data from ${this.bulkFilesTableName} where fileId = '${fileId}'`); if (this.dbtype === 'MySQL') rs = rs[0]; const r = (0, utilityHelpers_1.verifyOneOrNone)(rs); if (r && r.data) { data = Uint8Array.from(r.data); } } else { const r = (0, utilityHelpers_1.verifyOneOrNone)(await this.knex(this.bulkFilesTableName).where({ fileId: fileId }).select('data')); if (r.data) data = Uint8Array.from(r.data); } return data; } /** * @param header Header to attempt to add to live storage. * @returns details of conditions found attempting to insert header */ async insertHeader(header) { const table = this.headerTableName; const r = { added: false, dupe: false, noPrev: false, badPrev: false, noActiveAncestor: false, isActiveTip: false, reorgDepth: 0, priorTip: undefined, noTip: false, deactivatedHeaders: [] }; await this.knex.transaction(async (trx) => { /* We ensure the header does not already exist. This needs to be done inside the transaction to avoid inserting multiple headers. If an identical header is found, there is no need to insert a new header. */ const [dupeCheck] = await trx(table).where({ hash: header.hash }).count(); if (dupeCheck['count(*)']) { r.dupe = true; return; } // This is the existing previous header to the one being inserted... let [oneBack] = await trx(table).where({ hash: header.previousHash }); if (!oneBack) { // Check if this is first live header... const cr = await trx(table).count(); const count = Number(cr[0]['count(*)']); if (count === 0) { // If this is the first live header, the last bulk header (if there is one) is the previous header. const lbf = await this.bulkManager.getLastFile(); if (!lbf) throw new WERR_errors_1.WERR_INVALID_OPERATION('bulk headers must exist before first live header can be added'); if (header.previousHash === lbf.lastHash && header.height === lbf.firstHeight + lbf.count) { // Valid first live header. Add it. const chainWork = (0, blockHeaderUtilities_1.addWork)(lbf.lastChainWork, (0, blockHeaderUtilities_1.convertBitsToWork)(header.bits)); r.isActiveTip = true; const newHeader = { ...header, previousHeaderId: null, chainWork, isChainTip: r.isActiveTip, isActive: r.isActiveTip }; // Success await trx(table).insert(newHeader); r.added = true; return; } } // Failure without a oneBack // First live header that does not follow last bulk header or // Not the first live header and live headers doesn't include a previousHash header. r.noPrev = true; return; } // This header's previousHash matches an existing live header's hash, if height isn't +1, reject it. if (oneBack.height + 1 != header.height) { r.badPrev = true; return; } if (oneBack.isActive && oneBack.isChainTip) { r.priorTip = oneBack; } else { ; [r.priorTip] = await trx(table).where({ isActive: true, isChainTip: true }); } if (!r.priorTip) { // No active chain tip found. This is a logic error in state of live headers. r.noTip = true; return; } // We have an acceptable new live header...and live headers has an active chain tip. const chainWork = (0, blockHeaderUtilities_1.addWork)(oneBack.chainWork, (0, blockHeaderUtilities_1.convertBitsToWork)(header.bits)); r.isActiveTip = (0, blockHeaderUtilities_1.isMoreWork)(chainWork, r.priorTip.chainWork); const newHeader = { ...header, previousHeaderId: oneBack.headerId, chainWork, isChainTip: r.isActiveTip, isActive: r.isActiveTip }; if (r.isActiveTip) { // Find newHeader's first active ancestor let activeAncestor = oneBack; while (!activeAncestor.isActive) { const [previousHeader] = await trx(table).where({ headerId: activeAncestor.previousHeaderId || -1 }); if (!previousHeader) { // live headers doesn't contain an active ancestor. This is a live header's logic error. r.noActiveAncestor = true; return; } activeAncestor = previousHeader; } if (!(oneBack.isActive && oneBack.isChainTip)) // If this is the new active chain tip, and oneBack was not, this is a reorg. r.reorgDepth = Math.min(r.priorTip.height, header.height) - activeAncestor.height; if (activeAncestor.headerId !== oneBack.headerId) { // Deactivate headers from the current active chain tip up to but excluding our activeAncestor: let [headerToDeactivate] = await trx(table).where({ isChainTip: true, isActive: true }); while (headerToDeactivate.headerId !== activeAncestor.headerId) { // Headers are deactivated until we reach the activeAncestor r.deactivatedHeaders.push(headerToDeactivate); await trx(table) .where({ headerId: headerToDeactivate.headerId }) .update({ isActive: false }); const [previousHeader] = await trx(table).where({ headerId: headerToDeactivate.previousHeaderId || -1 }); headerToDeactivate = previousHeader; } // The first header to activate is one before the one we are about to insert let headerToActivate = oneBack; while (headerToActivate.headerId !== activeAncestor.headerId) { // Headers are activated until we reach the active ancestor await trx(table).where({ headerId: headerToActivate.headerId }).update({ isActive: true }); const [previousHeader] = await trx(table).where({ headerId: headerToActivate.previousHeaderId || -1 }); headerToActivate = previousHeader; } } } if (oneBack.isChainTip) { // Deactivate the old chain tip await trx(table).where({ headerId: oneBack.headerId }).update({ isChainTip: false }); } await trx(table).insert(newHeader); r.added = true; }); if (r.added && r.isActiveTip) this.pruneLiveBlockHeaders(header.height); return r; } async findMaxHeaderId() { var _a; return ((_a = (await this.knex(this.headerTableName).max('headerId as v')).pop()) === null || _a === void 0 ? void 0 : _a.v) || -1; //const [resultrow] = await this.knex(this.headerTableName).max('headerId as maxHeaderId') //return resultrow?.maxHeaderId || 0 } async deleteLiveBlockHeaders() { const table = this.headerTableName; await this.knex.transaction(async (trx) => { await trx(table).update({ previousHeaderId: null }); await trx(table).del(); }); } async deleteBulkBlockHeaders() { const table = this.bulkFilesTableName; await this.knex.transaction(async (trx) => { await trx(table).del(); }); } async deleteOlderLiveBlockHeaders(maxHeight) { return this.knex.transaction(async (trx) => { try { const tableName = this.headerTableName; await trx(tableName) .whereIn('previousHeaderId', function () { this.select('headerId').from(tableName).where('height', '<=', maxHeight); }) .update({ previousHeaderId: null }); const deletedCount = await trx(tableName).where('height', '<=', maxHeight).del(); // Commit transaction await trx.commit(); return deletedCount; } catch (error) { // Rollback on error await trx.rollback(); throw error; } }); } async getLiveHeaders(range) { const headers = await this.knex(this.headerTableName) .where({ isActive: true }) .andWhere('height', '>=', range.minHeight) .andWhere('height', '<=', range.maxHeight) .orderBy('height'); return headers; } concatSerializedHeaders(bufs) { const r = [bufs.length * 80]; for (const bh of bufs) { for (const b of bh) { r.push(b); } } return r; } async liveHeadersForBulk(count) { const headers = await this.knex(this.headerTableName) .where({ isActive: true }) .limit(count) .orderBy('height'); return headers; } } exports.ChaintracksStorageKnex = ChaintracksStorageKnex; //# sourceMappingURL=ChaintracksStorageKnex.js.map