@bsv/wallet-toolbox-client
Version:
Client only Wallet Storage
410 lines • 21.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.KnexMigrations = void 0;
exports.determineDBType = determineDBType;
const StorageKnex_1 = require("../StorageKnex");
const WalletError_1 = require("../../sdk/WalletError");
const WERR_errors_1 = require("../../sdk/WERR_errors");
class KnexMigrations {
/**
* @param chain
* @param storageName human readable name for this storage instance
* @param maxOutputScriptLength limit for scripts kept in outputs table, longer scripts will be pulled from rawTx
*/
constructor(chain, storageName, storageIdentityKey, maxOutputScriptLength) {
this.chain = chain;
this.storageName = storageName;
this.storageIdentityKey = storageIdentityKey;
this.maxOutputScriptLength = maxOutputScriptLength;
this.migrations = {};
this.migrations = this.setupMigrations(chain, storageName, storageIdentityKey, maxOutputScriptLength);
}
async getMigrations() {
return Object.keys(this.migrations).sort();
}
getMigrationName(migration) {
return migration;
}
async getMigration(migration) {
return this.migrations[migration];
}
async getLatestMigration() {
const ms = await this.getMigrations();
return ms[ms.length - 1];
}
static async latestMigration() {
const km = new KnexMigrations('test', 'dummy', '1'.repeat(64), 100);
return await km.getLatestMigration();
}
setupMigrations(chain, storageName, storageIdentityKey, maxOutputScriptLength) {
const migrations = {};
const addTimeStamps = (knex, table, dbtype) => {
if (dbtype === 'MySQL') {
table.timestamp('created_at', { precision: 3 }).defaultTo(knex.fn.now(3)).notNullable();
table.timestamp('updated_at', { precision: 3 }).defaultTo(knex.fn.now(3)).notNullable();
}
else {
table.timestamp('created_at', { precision: 3 }).defaultTo(knex.fn.now()).notNullable();
table.timestamp('updated_at', { precision: 3 }).defaultTo(knex.fn.now()).notNullable();
}
};
migrations['2025-05-13-001 add monitor events event index'] = {
async up(knex) {
await knex.schema.alterTable('monitor_events', table => {
table.index('event');
});
},
async down(knex) {
await knex.schema.alterTable('monitor_events', table => {
table.dropIndex('event');
});
}
};
migrations['2025-03-03-001 descriptions to 2000'] = {
async up(knex) {
await knex.schema.alterTable('transactions', table => {
table.string('description', 2048).alter();
});
await knex.schema.alterTable('outputs', table => {
table.string('outputDescription', 2048).alter();
table.string('spendingDescription', 2048).alter();
});
},
async down(knex) { }
};
migrations['2025-03-01-001 reset req history'] = {
async up(knex) {
const storage = new StorageKnex_1.StorageKnex({
...StorageKnex_1.StorageKnex.defaultOptions(),
chain: chain,
knex
});
const settings = await storage.makeAvailable();
await knex.raw(`update proven_tx_reqs set history = '{}'`);
},
async down(knex) {
// No way back...
}
};
migrations['2025-02-28-001 derivations to 200'] = {
async up(knex) {
await knex.schema.alterTable('outputs', table => {
table.string('derivationPrefix', 200).alter();
table.string('derivationSuffix', 200).alter();
});
},
async down(knex) {
await knex.schema.alterTable('outputs', table => {
table.string('derivationPrefix', 32).alter();
table.string('derivationSuffix', 32).alter();
});
}
};
migrations['2025-02-22-001 nonNULL activeStorage'] = {
async up(knex) {
const storage = new StorageKnex_1.StorageKnex({
...StorageKnex_1.StorageKnex.defaultOptions(),
chain: chain,
knex
});
const settings = await storage.makeAvailable();
await knex.raw(`update users set activeStorage = '${settings.storageIdentityKey}' where activeStorage is NULL`);
await knex.schema.alterTable('users', table => {
table.string('activeStorage').notNullable().alter();
});
},
async down(knex) {
await knex.schema.alterTable('users', table => {
table.string('activeStorage').nullable().alter();
});
}
};
migrations['2025-01-21-001 add activeStorage to users'] = {
async up(knex) {
await knex.schema.alterTable('users', table => {
table.string('activeStorage', 130).nullable().defaultTo(null);
});
},
async down(knex) {
await knex.schema.alterTable('users', table => {
table.dropColumn('activeStorage');
});
}
};
migrations['2024-12-26-001 initial migration'] = {
async up(knex) {
const dbtype = await determineDBType(knex);
await knex.schema.createTable('proven_txs', table => {
addTimeStamps(knex, table, dbtype);
table.increments('provenTxId').notNullable();
table.string('txid', 64).notNullable().unique();
table.integer('height').unsigned().notNullable();
table.integer('index').unsigned().notNullable();
table.binary('merklePath').notNullable();
table.binary('rawTx').notNullable();
table.string('blockHash', 64).notNullable();
table.string('merkleRoot', 64).notNullable();
});
await knex.schema.createTable('proven_tx_reqs', table => {
addTimeStamps(knex, table, dbtype);
table.increments('provenTxReqId');
table.integer('provenTxId').unsigned().references('provenTxId').inTable('proven_txs');
table.string('status', 16).notNullable().defaultTo('unknown');
table.integer('attempts').unsigned().defaultTo(0).notNullable();
table.boolean('notified').notNullable().defaultTo(false);
table.string('txid', 64).notNullable().unique();
table.string('batch', 64).nullable();
table.text('history', 'longtext').notNullable().defaultTo('{}');
table.text('notify', 'longtext').notNullable().defaultTo('{}');
table.binary('rawTx').notNullable();
table.binary('inputBEEF');
table.index('status');
table.index('batch');
});
await knex.schema.createTable('users', table => {
addTimeStamps(knex, table, dbtype);
table.increments('userId');
table.string('identityKey', 130).notNullable().unique();
});
await knex.schema.createTable('certificates', table => {
addTimeStamps(knex, table, dbtype);
table.increments('certificateId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.string('serialNumber', 100).notNullable();
table.string('type', 100).notNullable();
table.string('certifier', 100).notNullable();
table.string('subject', 100).notNullable();
table.string('verifier', 100).nullable();
table.string('revocationOutpoint', 100).notNullable();
table.string('signature', 255).notNullable();
table.boolean('isDeleted').notNullable().defaultTo(false);
table.unique(['userId', 'type', 'certifier', 'serialNumber']);
});
await knex.schema.createTable('certificate_fields', table => {
addTimeStamps(knex, table, dbtype);
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.integer('certificateId').unsigned().references('certificateId').inTable('certificates').notNullable();
table.string('fieldName', 100).notNullable();
table.string('fieldValue').notNullable();
table.string('masterKey', 255).defaultTo('').notNullable();
table.unique(['fieldName', 'certificateId']);
});
await knex.schema.createTable('output_baskets', table => {
addTimeStamps(knex, table, dbtype);
table.increments('basketId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.string('name', 300).notNullable();
table.integer('numberOfDesiredUTXOs', 6).defaultTo(6).notNullable();
table.integer('minimumDesiredUTXOValue', 15).defaultTo(10000).notNullable();
table.boolean('isDeleted').notNullable().defaultTo(false);
table.unique(['name', 'userId']);
});
await knex.schema.createTable('transactions', table => {
addTimeStamps(knex, table, dbtype);
table.increments('transactionId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.integer('provenTxId').unsigned().references('provenTxId').inTable('proven_txs');
table.string('status', 64).notNullable();
table.string('reference', 64).notNullable().unique();
table.boolean('isOutgoing').notNullable();
table.bigint('satoshis').defaultTo(0).notNullable();
table.integer('version').unsigned().nullable();
table.integer('lockTime').unsigned().nullable();
table.string('description', 500).notNullable();
table.string('txid', 64);
table.binary('inputBEEF');
table.binary('rawTx');
table.index('status');
});
await knex.schema.createTable('commissions', table => {
addTimeStamps(knex, table, dbtype);
table.increments('commissionId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table
.integer('transactionId')
.unsigned()
.references('transactionId')
.inTable('transactions')
.notNullable()
.unique();
table.integer('satoshis', 15).notNullable();
table.string('keyOffset', 130).notNullable();
table.boolean('isRedeemed').defaultTo(false).notNullable();
table.binary('lockingScript').notNullable();
table.index('transactionId');
});
await knex.schema.createTable('outputs', table => {
addTimeStamps(knex, table, dbtype);
table.increments('outputId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.integer('transactionId').unsigned().references('transactionId').inTable('transactions').notNullable();
table.integer('basketId').unsigned().references('basketId').inTable('output_baskets');
table.boolean('spendable').defaultTo(false).notNullable();
table.boolean('change').defaultTo(false).notNullable();
table.integer('vout', 10).notNullable();
table.bigint('satoshis').notNullable();
table.string('providedBy', 130).notNullable();
table.string('purpose', 20).notNullable();
table.string('type', 50).notNullable();
table.string('outputDescription', 300); // allow extra room for encryption and imports
table.string('txid', 64);
table.string('senderIdentityKey', 130);
table.string('derivationPrefix', 32);
table.string('derivationSuffix', 32);
table.string('customInstructions', 2500);
table.integer('spentBy').unsigned().references('transactionId').inTable('transactions');
table.integer('sequenceNumber').unsigned().nullable();
table.string('spendingDescription');
table.bigint('scriptLength').unsigned().nullable();
table.bigint('scriptOffset').unsigned().nullable();
table.binary('lockingScript');
table.unique(['transactionId', 'vout', 'userId']);
});
await knex.schema.createTable('output_tags', table => {
addTimeStamps(knex, table, dbtype);
table.increments('outputTagId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.string('tag', 150).notNullable();
table.boolean('isDeleted').notNullable().defaultTo(false);
table.unique(['tag', 'userId']);
});
await knex.schema.createTable('output_tags_map', table => {
addTimeStamps(knex, table, dbtype);
table.integer('outputTagId').unsigned().references('outputTagId').inTable('output_tags').notNullable();
table.integer('outputId').unsigned().references('outputId').inTable('outputs').notNullable();
table.boolean('isDeleted').notNullable().defaultTo(false);
table.unique(['outputTagId', 'outputId']);
table.index('outputId');
});
await knex.schema.createTable('tx_labels', table => {
addTimeStamps(knex, table, dbtype);
table.increments('txLabelId');
table.integer('userId').unsigned().references('userId').inTable('users').notNullable();
table.string('label', 300).notNullable();
table.boolean('isDeleted').notNullable().defaultTo(false);
table.unique(['label', 'userId']);
});
await knex.schema.createTable('tx_labels_map', table => {
addTimeStamps(knex, table, dbtype);
table.integer('txLabelId').unsigned().references('txLabelId').inTable('tx_labels').notNullable();
table.integer('transactionId').unsigned().references('transactionId').inTable('transactions').notNullable();
table.boolean('isDeleted').notNullable().defaultTo(false);
table.unique(['txLabelId', 'transactionId']);
table.index('transactionId');
});
await knex.schema.createTable('monitor_events', table => {
addTimeStamps(knex, table, dbtype);
table.increments('id');
table.string('event', 64).notNullable();
table.text('details', 'longtext').nullable();
});
await knex.schema.createTable('settings', table => {
addTimeStamps(knex, table, dbtype);
table.string('storageIdentityKey', 130).notNullable();
table.string('storageName', 128).notNullable();
table.string('chain', 10).notNullable();
table.string('dbtype', 10).notNullable();
table.integer('maxOutputScript', 15).notNullable();
});
await knex.schema.createTable('sync_states', table => {
addTimeStamps(knex, table, dbtype);
table.increments('syncStateId');
table.integer('userId').unsigned().notNullable().references('userId').inTable('users');
table.string('storageIdentityKey', 130).notNullable().defaultTo('');
table.string('storageName').notNullable();
table.string('status').notNullable().defaultTo('unknown');
table.boolean('init').notNullable().defaultTo(false);
table.string('refNum', 100).notNullable().unique();
table.text('syncMap', 'longtext').notNullable();
table.dateTime('when');
table.bigint('satoshis');
table.text('errorLocal', 'longtext');
table.text('errorOther', 'longtext');
table.index('status');
table.index('refNum');
});
if (dbtype === 'MySQL') {
await knex.raw('ALTER TABLE proven_tx_reqs MODIFY COLUMN rawTx LONGBLOB');
await knex.raw('ALTER TABLE proven_tx_reqs MODIFY COLUMN inputBEEF LONGBLOB');
await knex.raw('ALTER TABLE proven_txs MODIFY COLUMN rawTx LONGBLOB');
await knex.raw('ALTER TABLE transactions MODIFY COLUMN rawTx LONGBLOB');
await knex.raw('ALTER TABLE transactions MODIFY COLUMN inputBEEF LONGBLOB');
await knex.raw('ALTER TABLE outputs MODIFY COLUMN lockingScript LONGBLOB');
}
else {
await knex.schema.alterTable('proven_tx_reqs', table => {
table.binary('rawTx', 10000000).alter();
table.binary('beef', 10000000).alter();
});
await knex.schema.alterTable('outputs', table => {
table.binary('lockingScript', 10000000).alter();
});
await knex.schema.alterTable('proven_txs', table => {
table.binary('rawTx', 10000000).alter();
});
await knex.schema.alterTable('transactions', table => {
table.binary('rawTx', 10000000).alter();
table.binary('beef', 10000000).alter();
});
}
await knex('settings').insert({
storageIdentityKey,
storageName,
chain,
dbtype,
maxOutputScript: maxOutputScriptLength
});
},
async down(knex) {
await knex.schema.dropTable('sync_states');
await knex.schema.dropTable('settings');
await knex.schema.dropTable('monitor_events');
await knex.schema.dropTable('certificate_fields');
await knex.schema.dropTable('certificates');
await knex.schema.dropTable('commissions');
await knex.schema.dropTable('output_tags_map');
await knex.schema.dropTable('output_tags');
await knex.schema.dropTable('outputs');
await knex.schema.dropTable('output_baskets');
await knex.schema.dropTable('tx_labels_map');
await knex.schema.dropTable('tx_labels');
await knex.schema.dropTable('transactions');
await knex.schema.dropTable('users');
await knex.schema.dropTable('proven_tx_reqs');
await knex.schema.dropTable('proven_txs');
}
};
return migrations;
}
}
exports.KnexMigrations = KnexMigrations;
/**
* @param knex
* @returns {DBType} connected database engine variant
*/
async function determineDBType(knex) {
try {
const q = `SELECT
CASE
WHEN (SELECT VERSION() LIKE '%MariaDB%') = 1 THEN 'Unknown'
WHEN (SELECT VERSION()) IS NOT NULL THEN 'MySQL'
ELSE 'Unknown'
END AS database_type;`;
let r = await knex.raw(q);
if (!r[0]['database_type'])
r = r[0];
if (r['rows'])
r = r.rows;
const dbtype = r[0].database_type;
if (dbtype === 'Unknown')
throw new WERR_errors_1.WERR_NOT_IMPLEMENTED(`Attempting to create database on unsuported engine.`);
return dbtype;
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
if (e.code === 'SQLITE_ERROR')
return 'SQLite';
throw new WERR_errors_1.WERR_NOT_IMPLEMENTED(`Attempting to create database on unsuported engine.`);
}
}
//# sourceMappingURL=KnexMigrations.js.map