@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
1,570 lines (1,449 loc) • 83.1 kB
text/typescript
import {
Beef,
CachedKeyDeriver,
CreateActionArgs,
CreateActionOutput,
CreateActionResult,
HexString,
KeyDeriver,
KeyDeriverApi,
P2PKH,
PrivateKey,
PublicKey,
SatoshiValue,
SignActionArgs,
SignActionResult,
Transaction,
Utils,
WalletAction,
WalletActionInput,
WalletActionOutput,
WalletCertificate,
WalletInterface
} from '@bsv/sdk'
import path from 'path'
import { promises as fsp } from 'fs'
import {
asArray,
randomBytesBase64,
randomBytesHex,
sdk,
StorageProvider,
StorageKnex,
StorageSyncReader,
verifyTruthy,
Wallet,
Monitor,
Services,
WalletStorageManager,
verifyOne,
StorageClient,
TableOutputBasket,
TableOutput,
TableMonitorEvent,
TableProvenTxReq,
TableProvenTx,
TableCommission,
TableTransaction,
TableTxLabelMap,
TableTxLabel,
TableUser,
TableSyncState,
TableCertificate,
TableCertificateField,
TableOutputTagMap,
TableOutputTag,
ScriptTemplateBRC29,
Setup
} from '../../src/index.all'
import { Knex, knex as makeKnex } from 'knex'
import * as dotenv from 'dotenv'
import { TrxToken, WalletServicesOptions } from '../../src/sdk'
import { StorageIdb } from '../../src/storage/StorageIdb'
dotenv.config()
const localMySqlConnection = process.env.MYSQL_CONNECTION || ''
export interface TuEnvFlags {
chain: sdk.Chain
runMySQL: boolean
runSlowTests: boolean
logTests: boolean
}
export interface TuEnv extends TuEnvFlags {
chain: sdk.Chain
identityKey: string
identityKey2: string
taalApiKey: string
bitailsApiKey: string
whatsonchainApiKey: string
devKeys: Record<string, string>
/**
* file path to local sqlite file for identityKey
*/
filePath?: string
/**
* identityKey for automated test wallet on this chain
*/
testIdentityKey?: string
/**
* file path to local sqlite file for testIdentityKey
*/
testFilePath?: string
cloudMySQLConnection?: string
}
export abstract class TestUtilsWalletStorage {
/**
* @param chain
* @returns true if .env has truthy idenityKey, idenityKey2 values for chain
*/
static noEnv(chain: sdk.Chain): boolean {
try {
Setup.getEnv(chain)
return false
} catch {
return true
}
}
/**
* @param chain
* @returns true if .env is not valid for chain or testIdentityKey or testFilePath are undefined or empty.
*/
static noTestEnv(chain: sdk.Chain): boolean {
try {
const env = _tu.getEnv(chain)
return !env.testIdentityKey || !env.testFilePath
} catch {
return true
}
}
static getEnvFlags(chain: sdk.Chain): TuEnvFlags {
const logTests = !!process.env.LOGTESTS
const runMySQL = !!process.env.RUNMYSQL
const runSlowTests = !!process.env.RUNSLOWTESTS
return {
chain,
runMySQL,
runSlowTests,
logTests
}
}
static getEnv(chain: sdk.Chain): TuEnv {
const flagsEnv = _tu.getEnvFlags(chain)
// Identity keys of the lead maintainer of this repo...
const identityKey = (chain === 'main' ? process.env.MY_MAIN_IDENTITY : process.env.MY_TEST_IDENTITY) || ''
const filePath = chain === 'main' ? process.env.MY_MAIN_FILEPATH : process.env.MY_TEST_FILEPATH
const identityKey2 = (chain === 'main' ? process.env.MY_MAIN_IDENTITY2 : process.env.MY_TEST_IDENTITY2) || ''
const testIdentityKey = chain === 'main' ? process.env.TEST_MAIN_IDENTITY : process.env.TEST_TEST_IDENTITY
const testFilePath = chain === 'main' ? process.env.TEST_MAIN_FILEPATH : process.env.TEST_TEST_FILEPATH
const cloudMySQLConnection =
chain === 'main' ? process.env.MAIN_CLOUD_MYSQL_CONNECTION : process.env.TEST_CLOUD_MYSQL_CONNECTION
const DEV_KEYS = process.env.DEV_KEYS || '{}'
const taalApiKey = (chain === 'main' ? process.env.MAIN_TAAL_API_KEY : process.env.TEST_TAAL_API_KEY) || ''
const bitailsApiKey = (chain === 'main' ? process.env.MAIN_BITAILS_API_KEY : process.env.TEST_BITAILS_API_KEY) || ''
const whatsonchainApiKey =
(chain === 'main' ? process.env.MAIN_WHATSONCHAIN_API_KEY : process.env.TEST_WHATSONCHAIN_API_KEY) || ''
return {
...flagsEnv,
identityKey,
identityKey2,
taalApiKey,
bitailsApiKey,
whatsonchainApiKey,
devKeys: JSON.parse(DEV_KEYS) as Record<string, string>,
filePath,
testIdentityKey,
testFilePath,
cloudMySQLConnection
}
}
static async createMainReviewSetup(): Promise<{
env: TuEnv
storage: StorageKnex
services: Services
}> {
const env = _tu.getEnv('main')
if (!env.cloudMySQLConnection) throw new sdk.WERR_INVALID_PARAMETER('env.cloudMySQLConnection', 'valid')
const knex = Setup.createMySQLKnex(env.cloudMySQLConnection)
const storage = new StorageKnex({
chain: env.chain,
knex: knex,
commissionSatoshis: 0,
commissionPubKeyHex: undefined,
feeModel: { model: 'sat/kb', value: 1 }
})
const servicesOptions = Services.createDefaultOptions(env.chain)
if (env.whatsonchainApiKey) servicesOptions.whatsOnChainApiKey = env.whatsonchainApiKey
const services = new Services(servicesOptions)
storage.setServices(services)
await storage.makeAvailable()
return { env, storage, services }
}
static async createNoSendP2PKHTestOutpoint(
address: string,
satoshis: number,
noSendChange: string[] | undefined,
wallet: WalletInterface
): Promise<{
noSendChange: string[]
txid: string
cr: CreateActionResult
sr: SignActionResult
}> {
return await _tu.createNoSendP2PKHTestOutpoints(1, address, satoshis, noSendChange, wallet)
}
static async createNoSendP2PKHTestOutpoints(
count: number,
address: string,
satoshis: number,
noSendChange: string[] | undefined,
wallet: WalletInterface
): Promise<{
noSendChange: string[]
txid: string
cr: CreateActionResult
sr: SignActionResult
}> {
const outputs: CreateActionOutput[] = []
for (let i = 0; i < count; i++) {
outputs.push({
basket: `test-p2pkh-output-${i}`,
satoshis,
lockingScript: _tu.getLockP2PKH(address).toHex(),
outputDescription: `p2pkh ${i}`
})
}
const createArgs: CreateActionArgs = {
description: `to ${address}`,
outputs,
options: {
noSendChange,
randomizeOutputs: false,
signAndProcess: false,
noSend: true
}
}
const cr = await wallet.createAction(createArgs)
noSendChange = cr.noSendChange
expect(cr.noSendChange).toBeTruthy()
expect(cr.sendWithResults).toBeUndefined()
expect(cr.tx).toBeUndefined()
expect(cr.txid).toBeUndefined()
expect(cr.signableTransaction).toBeTruthy()
const st = cr.signableTransaction!
expect(st.reference).toBeTruthy()
// const tx = Transaction.fromAtomicBEEF(st.tx) // Transaction doesn't support V2 Beef yet.
const atomicBeef = Beef.fromBinary(st.tx)
const tx = atomicBeef.txs[atomicBeef.txs.length - 1].tx!
for (const input of tx.inputs) {
expect(atomicBeef.findTxid(input.sourceTXID!)).toBeTruthy()
}
// Spending authorization check happens here...
//expect(st.amount > 242 && st.amount < 300).toBe(true)
// sign and complete
const signArgs: SignActionArgs = {
reference: st.reference,
spends: {},
options: {
returnTXIDOnly: true,
noSend: true
}
}
const sr = await wallet.signAction(signArgs)
let txid = sr.txid!
// Update the noSendChange txid to final signed value.
noSendChange = noSendChange!.map(op => `${txid}.${op.split('.')[1]}`)
return { noSendChange, txid, cr, sr }
}
static getKeyPair(priv?: string | PrivateKey): TestKeyPair {
if (priv === undefined) priv = PrivateKey.fromRandom()
else if (typeof priv === 'string') priv = new PrivateKey(priv, 'hex')
const pub = PublicKey.fromPrivateKey(priv)
const address = pub.toAddress()
return { privateKey: priv, publicKey: pub, address }
}
static getLockP2PKH(address: string) {
const p2pkh = new P2PKH()
const lock = p2pkh.lock(address)
return lock
}
static getUnlockP2PKH(priv: PrivateKey, satoshis: number) {
const p2pkh = new P2PKH()
const lock = _tu.getLockP2PKH(_tu.getKeyPair(priv).address)
// Prepare to pay with SIGHASH_ALL and without ANYONE_CAN_PAY.
// In otherwords:
// - all outputs must remain in the current order, amount and locking scripts.
// - all inputs must remain from the current outpoints and sequence numbers.
// (unlock scripts are never signed)
const unlock = p2pkh.unlock(priv, 'all', false, satoshis, lock)
return unlock
}
static async createWalletOnly(args: {
chain?: sdk.Chain
rootKeyHex?: string
active?: sdk.WalletStorageProvider
backups?: sdk.WalletStorageProvider[]
privKeyHex?: string
}): Promise<TestWalletOnly> {
args.chain ||= 'test'
args.rootKeyHex ||= '1'.repeat(64)
const rootKey = PrivateKey.fromHex(args.rootKeyHex)
const identityKey = rootKey.toPublicKey().toString()
const keyDeriver = new CachedKeyDeriver(rootKey)
const chain = args.chain
const storage = new WalletStorageManager(identityKey, args.active, args.backups)
if (storage.canMakeAvailable()) await storage.makeAvailable()
const env = _tu.getEnv(args.chain!)
const serviceOptions: WalletServicesOptions = Services.createDefaultOptions(env.chain)
serviceOptions.taalApiKey = env.taalApiKey
serviceOptions.arcConfig.apiKey = env.taalApiKey
serviceOptions.bitailsApiKey = env.bitailsApiKey
serviceOptions.whatsOnChainApiKey = env.whatsonchainApiKey
const services = new Services(serviceOptions)
const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services)
const monitor = new Monitor(monopts)
monitor.addDefaultTasks()
let privilegedKeyManager: sdk.PrivilegedKeyManager | undefined = undefined
if (args.privKeyHex) {
const privKey = PrivateKey.fromString(args.privKeyHex)
privilegedKeyManager = new sdk.PrivilegedKeyManager(async () => privKey)
}
const wallet = new Wallet({
chain,
keyDeriver,
storage,
services,
monitor,
privilegedKeyManager
})
const r: TestWalletOnly = {
rootKey,
identityKey,
keyDeriver,
chain,
storage,
services,
monitor,
wallet
}
return r
}
/**
* Creates a wallet with both local sqlite and cloud stores, the local store is left active.
*
* Requires a valid .env file with chain matching testIdentityKey and testFilePath properties valid.
* Or `args` with those properties.
*
* Verifies wallet has at least 1000 satoshis in at least 10 change utxos.
*
* @param chain
*
* @returns {TestWalletNoSetup}
*/
static async createTestWallet(args: sdk.Chain | CreateTestWalletArgs): Promise<TestWalletNoSetup> {
let chain: sdk.Chain
let rootKeyHex: string
let filePath: string
let addLocalBackup = false
let setActiveClient = false
let useMySQLConnectionForClient = false
if (typeof args === 'string') {
chain = args
const env = _tu.getEnv(chain)
if (!env.testIdentityKey || !env.testFilePath) {
throw new sdk.WERR_INVALID_PARAMETER('env.testIdentityKey and env.testFilePath', 'valid')
}
rootKeyHex = env.devKeys[env.testIdentityKey!]
filePath = env.testFilePath
} else {
chain = args.chain
rootKeyHex = args.rootKeyHex
filePath = args.filePath
addLocalBackup = args.addLocalBackup === true
setActiveClient = args.setActiveClient === true
useMySQLConnectionForClient = args.useMySQLConnectionForClient === true
}
const databaseName = path.parse(filePath).name
const setup = await _tu.createSQLiteTestWallet({
filePath,
rootKeyHex,
databaseName,
chain
})
setup.localStorageIdentityKey = setup.storage.getActiveStore()
let client: sdk.WalletStorageProvider
if (useMySQLConnectionForClient) {
const env = _tu.getEnv(chain)
if (!env.cloudMySQLConnection) throw new sdk.WERR_INVALID_PARAMETER('env.cloundMySQLConnection', 'valid')
const connection = JSON.parse(env.cloudMySQLConnection)
client = new StorageKnex({
...StorageKnex.defaultOptions(),
knex: _tu.createMySQLFromConnection(connection),
chain: env.chain
})
} else {
const endpointUrl =
chain === 'main' ? 'https://storage.babbage.systems' : 'https://staging-storage.babbage.systems'
client = new StorageClient(setup.wallet, endpointUrl)
}
setup.clientStorageIdentityKey = (await client.makeAvailable()).storageIdentityKey
await setup.wallet.storage.addWalletStorageProvider(client)
if (addLocalBackup) {
const backupName = `${databaseName}_backup`
const backupPath = filePath.replace(databaseName, backupName)
const localBackup = new StorageKnex({
...StorageKnex.defaultOptions(),
knex: _tu.createLocalSQLite(backupPath),
chain
})
await localBackup.migrate(backupName, randomBytesHex(33))
setup.localBackupStorageIdentityKey = (await localBackup.makeAvailable()).storageIdentityKey
await setup.wallet.storage.addWalletStorageProvider(localBackup)
}
// SETTING ACTIVE
// SETTING ACTIVE
// SETTING ACTIVE
const log = await setup.storage.setActive(
setActiveClient ? setup.clientStorageIdentityKey : setup.localStorageIdentityKey
)
logger(log)
let needsBackup = false
if (setup.storage.getActiveStore() === setup.localStorageIdentityKey) {
const basket = verifyOne(
await setup.activeStorage.findOutputBaskets({
partial: {
userId: setup.storage.getActiveUser().userId,
name: 'default'
}
})
)
if (basket.minimumDesiredUTXOValue !== 5 || basket.numberOfDesiredUTXOs < 32) {
needsBackup = true
await setup.activeStorage.updateOutputBasket(basket.basketId, {
minimumDesiredUTXOValue: 5,
numberOfDesiredUTXOs: 32
})
}
}
const balance = await setup.wallet.balanceAndUtxos()
if (balance.total < 1000) {
throw new sdk.WERR_INSUFFICIENT_FUNDS(1000, 1000 - balance.total)
}
if (balance.utxos.length <= 10) {
const args: CreateActionArgs = {
description: 'spread change'
}
await setup.wallet.createAction(args)
needsBackup = true
}
if (needsBackup) {
const log2 = await setup.storage.updateBackups()
console.log(log2)
}
return setup
}
static async createTestWalletWithStorageClient(args: {
rootKeyHex?: string
endpointUrl?: string
chain?: sdk.Chain
}): Promise<TestWalletOnly> {
args.chain ||= 'test'
const wo = await _tu.createWalletOnly({
chain: args.chain,
rootKeyHex: args.rootKeyHex
})
args.endpointUrl ||=
args.chain === 'main' ? 'https://storage.babbage.systems' : 'https://staging-storage.babbage.systems'
const client = new StorageClient(wo.wallet, args.endpointUrl)
await wo.storage.addWalletStorageProvider(client)
return wo
}
static async createKnexTestWalletWithSetup<T>(args: {
knex: Knex<any, any[]>
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
dropAll?: boolean
privKeyHex?: string
insertSetup: (storage: StorageKnex, identityKey: string) => Promise<T>
}): Promise<TestWallet<T>> {
const wo = await _tu.createWalletOnly({
chain: args.chain,
rootKeyHex: args.rootKeyHex,
privKeyHex: args.privKeyHex
})
const activeStorage = new StorageKnex({
chain: wo.chain,
knex: args.knex,
commissionSatoshis: 0,
commissionPubKeyHex: undefined,
feeModel: { model: 'sat/kb', value: 1 }
})
if (args.dropAll) await activeStorage.dropAllData()
await activeStorage.migrate(args.databaseName, randomBytesHex(33))
await activeStorage.makeAvailable()
const setup = await args.insertSetup(activeStorage, wo.identityKey)
await wo.storage.addWalletStorageProvider(activeStorage)
const { user, isNew } = await activeStorage.findOrInsertUser(wo.identityKey)
const userId = user.userId
const r: TestWallet<T> = {
...wo,
activeStorage,
setup,
userId
}
return r
}
/**
* Returns path to temporary file in project's './test/data/tmp/' folder.
*
* Creates any missing folders.
*
* Optionally tries to delete any existing file. This may fail if the file file is locked
* by another process.
*
* Optionally copies filename (or if filename has no dir component, a file of the same filename from the project's './test/data' folder) to initialize file's contents.
*
* CAUTION: returned file path will include four random hex digits unless tryToDelete is true. Files must be purged periodically.
*
* @param filename target filename without path, optionally just extension in which case random name is used
* @param tryToDelete true to attempt to delete an existing file at the returned file path.
* @param copyToTmp true to copy file of same filename from './test/data' (or elsewhere if filename has path) to tmp folder
* @param reuseExisting true to use existing file if found, otherwise a random string is added to filename.
* @returns path in './test/data/tmp' folder.
*/
static async newTmpFile(
filename = '',
tryToDelete = false,
copyToTmp = false,
reuseExisting = false
): Promise<string> {
const tmpFolder = './test/data/tmp/'
const p = path.parse(filename)
const dstDir = tmpFolder
const dstName = `${p.name}${tryToDelete || reuseExisting ? '' : randomBytesHex(6)}`
const dstExt = p.ext || 'tmp'
const dstPath = path.resolve(`${dstDir}${dstName}${dstExt}`)
await fsp.mkdir(tmpFolder, { recursive: true })
if (!reuseExisting && (tryToDelete || copyToTmp))
try {
await fsp.unlink(dstPath)
} catch (eu: unknown) {
const e = sdk.WalletError.fromUnknown(eu)
if (e.name !== 'ENOENT') {
throw e
}
}
if (copyToTmp) {
const srcPath = p.dir ? path.resolve(filename) : path.resolve(`./test/data/${filename}`)
await fsp.copyFile(srcPath, dstPath)
}
return dstPath
}
static async copyFile(srcPath: string, dstPath: string): Promise<void> {
await fsp.copyFile(srcPath, dstPath)
}
static async existingDataFile(filename: string): Promise<string> {
const folder = './test/data/'
return folder + filename
}
static createLocalSQLite(filename: string): Knex {
const config: Knex.Config = {
client: 'sqlite3',
connection: { filename },
useNullAsDefault: true
}
const knex = makeKnex(config)
return knex
}
static createMySQLFromConnection(connection: object): Knex {
const config: Knex.Config = {
client: 'mysql2',
connection,
useNullAsDefault: true,
pool: { min: 0, max: 7, idleTimeoutMillis: 15000 }
}
const knex = makeKnex(config)
return knex
}
static createLocalMySQL(database: string): Knex {
const connection = JSON.parse(localMySqlConnection || '{}')
connection['database'] = database
const config: Knex.Config = {
client: 'mysql2',
connection,
useNullAsDefault: true,
pool: { min: 0, max: 7, idleTimeoutMillis: 15000 }
}
const knex = makeKnex(config)
return knex
}
static async createMySQLTestWallet(args: {
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
dropAll?: boolean
}): Promise<TestWallet<{}>> {
return await this.createKnexTestWallet({
...args,
knex: _tu.createLocalMySQL(args.databaseName)
})
}
static async createMySQLTestSetup1Wallet(args: {
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
}): Promise<TestWallet<TestSetup1>> {
return await this.createKnexTestSetup1Wallet({
...args,
dropAll: true,
knex: _tu.createLocalMySQL(args.databaseName)
})
}
static async createMySQLTestSetup2Wallet(args: {
databaseName: string
mockData: MockData
chain?: sdk.Chain
rootKeyHex?: string
}): Promise<TestWallet<TestSetup2>> {
return await this.createKnexTestSetup2Wallet({
...args,
dropAll: true,
knex: _tu.createLocalMySQL(args.databaseName)
})
}
static async createSQLiteTestWallet(args: {
filePath?: string
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
dropAll?: boolean
privKeyHex?: string
}): Promise<TestWalletNoSetup> {
const localSQLiteFile = args.filePath || (await _tu.newTmpFile(`${args.databaseName}.sqlite`, false, false, true))
return await this.createKnexTestWallet({
...args,
knex: _tu.createLocalSQLite(localSQLiteFile)
})
}
static async createSQLiteTestSetup1Wallet(args: {
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
}): Promise<TestWallet<TestSetup1>> {
const localSQLiteFile = await _tu.newTmpFile(`${args.databaseName}.sqlite`, false, false, true)
return await this.createKnexTestSetup1Wallet({
...args,
dropAll: true,
knex: _tu.createLocalSQLite(localSQLiteFile)
})
}
static async createSQLiteTestSetup2Wallet(args: {
databaseName: string
mockData: MockData
chain?: sdk.Chain
rootKeyHex?: string
}): Promise<TestWallet<TestSetup2>> {
const localSQLiteFile = await _tu.newTmpFile(`${args.databaseName}.sqlite`, false, false, true)
return await this.createKnexTestSetup2Wallet({
...args,
dropAll: true,
knex: _tu.createLocalSQLite(localSQLiteFile)
})
}
static async createKnexTestWallet(args: {
knex: Knex<any, any[]>
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
dropAll?: boolean
privKeyHex?: string
}): Promise<TestWalletNoSetup> {
return await _tu.createKnexTestWalletWithSetup({
...args,
insertSetup: insertEmptySetup
})
}
static async createKnexTestSetup1Wallet(args: {
knex: Knex<any, any[]>
databaseName: string
chain?: sdk.Chain
rootKeyHex?: string
dropAll?: boolean
}): Promise<TestWallet<TestSetup1>> {
return await _tu.createKnexTestWalletWithSetup({
...args,
insertSetup: _tu.createTestSetup1
})
}
static async createKnexTestSetup2Wallet(args: {
knex: Knex<any, any[]>
databaseName: string
mockData: MockData
chain?: sdk.Chain
rootKeyHex?: string
dropAll?: boolean
}): Promise<TestWallet<TestSetup2>> {
return await _tu.createKnexTestWalletWithSetup({
...args,
insertSetup: async (storage: StorageKnex, identityKey: string) => {
return _tu.createTestSetup2(storage, identityKey, args.mockData)
}
})
}
static async fileExists(file: string): Promise<boolean> {
try {
const f = await fsp.open(file, 'r')
await f.close()
return true
} catch (eu: unknown) {
return false
}
}
//if (await _tu.fileExists(walletFile))
static async createLegacyWalletSQLiteCopy(databaseName: string): Promise<TestWalletNoSetup> {
const walletFile = await _tu.newTmpFile(`${databaseName}.sqlite`, false, false, true)
const walletKnex = _tu.createLocalSQLite(walletFile)
return await _tu.createLegacyWalletCopy(databaseName, walletKnex, walletFile)
}
static async createLegacyWalletMySQLCopy(databaseName: string): Promise<TestWalletNoSetup> {
const walletKnex = _tu.createLocalMySQL(databaseName)
return await _tu.createLegacyWalletCopy(databaseName, walletKnex)
}
static async createLiveWalletSQLiteWARNING(
databaseFullPath: string = './test/data/walletLiveTestData.sqlite'
): Promise<TestWalletNoSetup> {
return await this.createKnexTestWallet({
chain: 'test',
rootKeyHex: _tu.legacyRootKeyHex,
databaseName: 'walletLiveTestData',
knex: _tu.createLocalSQLite(databaseFullPath)
})
}
static async createWalletSQLite(
databaseFullPath: string = './test/data/tmp/walletNewTestData.sqlite',
databaseName: string = 'walletNewTestData'
): Promise<TestWalletNoSetup> {
return await this.createSQLiteTestWallet({
filePath: databaseFullPath,
databaseName,
chain: 'test',
rootKeyHex: '1'.repeat(64),
dropAll: true
})
}
static legacyRootKeyHex = '153a3df216' + '686f55b253991c' + '7039da1f648' + 'ffc5bfe93d6ac2c25ac' + '2d4070918d'
static async createLegacyWalletCopy(
databaseName: string,
walletKnex: Knex<any, any[]>,
tryCopyToPath?: string
): Promise<TestWalletNoSetup> {
const readerFile = await _tu.existingDataFile(`walletLegacyTestData.sqlite`)
let useReader = true
if (tryCopyToPath) {
await _tu.copyFile(readerFile, tryCopyToPath)
//console.log('USING FILE COPY INSTEAD OF SOURCE DB SYNC')
useReader = false
}
const chain: sdk.Chain = 'test'
const rootKeyHex = _tu.legacyRootKeyHex
const identityKey = '03ac2d10bdb0023f4145cc2eba2fcd2ad3070cb2107b0b48170c46a9440e4cc3fe'
const rootKey = PrivateKey.fromHex(rootKeyHex)
const keyDeriver = new CachedKeyDeriver(rootKey)
const activeStorage = new StorageKnex({
chain,
knex: walletKnex,
commissionSatoshis: 0,
commissionPubKeyHex: undefined,
feeModel: { model: 'sat/kb', value: 1 }
})
if (useReader) await activeStorage.dropAllData()
await activeStorage.migrate(databaseName, randomBytesHex(33))
await activeStorage.makeAvailable()
const storage = new WalletStorageManager(identityKey, activeStorage)
await storage.makeAvailable()
if (useReader) {
const readerKnex = _tu.createLocalSQLite(readerFile)
const reader = new StorageKnex({
chain,
knex: readerKnex,
commissionSatoshis: 0,
commissionPubKeyHex: undefined,
feeModel: { model: 'sat/kb', value: 1 }
})
await reader.makeAvailable()
await storage.syncFromReader(identityKey, new StorageSyncReader({ identityKey }, reader))
await reader.destroy()
}
const services = new Services(chain)
const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services)
const monitor = new Monitor(monopts)
const wallet = new Wallet({ chain, keyDeriver, storage, services, monitor })
const userId = verifyTruthy(await activeStorage.findUserByIdentityKey(identityKey)).userId
const r: TestWallet<{}> = {
rootKey,
identityKey,
keyDeriver,
chain,
activeStorage,
storage,
setup: {},
services,
monitor,
wallet,
userId
}
return r
}
static wrapProfiling(o: Object, name: string): Record<string, { count: number; totalMsecs: number }> {
const getFunctionsNames = (obj: Object) => {
let fNames: string[] = []
do {
fNames = fNames.concat(
Object.getOwnPropertyNames(obj).filter(p => p !== 'constructor' && typeof obj[p] === 'function')
)
} while ((obj = Object.getPrototypeOf(obj)) && obj !== Object.prototype)
return fNames
}
const notifyPerformance = (fn, performanceDetails) => {
setTimeout(() => {
let { functionName, args, startTime, endTime } = performanceDetails
let _args = args
if (Array.isArray(args)) {
_args = args.map(arg => {
if (typeof arg === 'function') {
let fName = arg.name
if (!fName) {
fName = 'function'
} else if (fName === 'callbackWrapper') {
fName = 'callback'
}
arg = `[${fName} Function]`
}
return arg
})
}
fn({ functionName, args: _args, startTime, endTime })
}, 0)
}
const stats: Record<string, { count: number; totalMsecs: number }> = {}
function logger(args: { functionName: string; args: any; startTime: number; endTime: number }) {
let s = stats[args.functionName]
if (!s) {
s = { count: 0, totalMsecs: 0 }
stats[args.functionName] = s
}
s.count++
s.totalMsecs += args.endTime - args.startTime
}
const performanceWrapper = (obj: Object, objectName: string, performanceNotificationCallback: any) => {
let _notifyPerformance = notifyPerformance.bind(null, performanceNotificationCallback)
let fNames = getFunctionsNames(obj)
for (let fName of fNames) {
let originalFunction = obj[fName]
let wrapperFunction = (...args) => {
let callbackFnIndex = -1
let startTime = Date.now()
let _callBack = args.filter((arg, i) => {
let _isFunction = typeof arg === 'function'
if (_isFunction) {
callbackFnIndex = i
}
return _isFunction
})[0]
if (_callBack) {
let callbackWrapper = (...callbackArgs) => {
let endTime = Date.now()
_notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
_callBack.apply(null, callbackArgs)
}
args[callbackFnIndex] = callbackWrapper
}
let originalReturnObject = originalFunction.apply(obj, args)
let isPromiseType =
originalReturnObject &&
typeof originalReturnObject.then === 'function' &&
typeof originalReturnObject.catch === 'function'
if (isPromiseType) {
return originalReturnObject
.then(resolveArgs => {
let endTime = Date.now()
_notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
return Promise.resolve(resolveArgs)
})
.catch((...rejectArgs) => {
let endTime = Date.now()
_notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
return Promise.reject(...rejectArgs)
})
}
if (!_callBack && !isPromiseType) {
let endTime = Date.now()
_notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
}
return originalReturnObject
}
obj[fName] = wrapperFunction
}
return obj
}
const functionNames = getFunctionsNames(o)
performanceWrapper(o, name, logger)
return stats
}
static async createIdbLegacyWalletCopy(databaseName: string): Promise<TestWalletProviderNoSetup> {
const chain: sdk.Chain = 'test'
const readerFile = await _tu.existingDataFile(`walletLegacyTestData.sqlite`)
const readerKnex = _tu.createLocalSQLite(readerFile)
const reader = new StorageKnex({
chain,
knex: readerKnex,
commissionSatoshis: 0,
commissionPubKeyHex: undefined,
feeModel: { model: 'sat/kb', value: 1 }
})
await reader.makeAvailable()
const rootKeyHex = _tu.legacyRootKeyHex
const identityKey = '03ac2d10bdb0023f4145cc2eba2fcd2ad3070cb2107b0b48170c46a9440e4cc3fe'
const rootKey = PrivateKey.fromHex(rootKeyHex)
const keyDeriver = new CachedKeyDeriver(rootKey)
const activeStorage = new StorageIdb({
chain,
commissionSatoshis: 0,
commissionPubKeyHex: undefined,
feeModel: { model: 'sat/kb', value: 1 }
})
await activeStorage.dropAllData()
await activeStorage.migrate(databaseName, randomBytesHex(33))
await activeStorage.makeAvailable()
const storage = new WalletStorageManager(identityKey, activeStorage)
await storage.makeAvailable()
await storage.syncFromReader(identityKey, new StorageSyncReader({ identityKey }, reader))
await reader.destroy()
const services = new Services(chain)
const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services)
const monitor = new Monitor(monopts)
const wallet = new Wallet({ chain, keyDeriver, storage, services, monitor })
const userId = verifyTruthy(await activeStorage.findUserByIdentityKey(identityKey)).userId
const r: TestWalletProvider<{}> = {
rootKey,
identityKey,
keyDeriver,
chain,
activeStorage,
storage,
setup: {},
services,
monitor,
wallet,
userId
}
return r
}
static makeSampleCert(subject?: string): {
cert: WalletCertificate
subject: string
certifier: PrivateKey
} {
subject ||= PrivateKey.fromRandom().toPublicKey().toString()
const certifier = PrivateKey.fromRandom()
const verifier = PrivateKey.fromRandom()
const cert: WalletCertificate = {
type: Utils.toBase64(new Array(32).fill(1)),
serialNumber: Utils.toBase64(new Array(32).fill(2)),
revocationOutpoint: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef.1',
subject,
certifier: certifier.toPublicKey().toString(),
fields: {
name: 'Alice',
email: 'alice@example.com',
organization: 'Example Corp'
},
signature: ''
}
return { cert, subject, certifier }
}
static async insertTestProvenTx(storage: StorageProvider, txid?: string, trx?: TrxToken) {
const now = new Date()
const ptx: TableProvenTx = {
created_at: now,
updated_at: now,
provenTxId: 0,
txid: txid || randomBytesHex(32),
height: 1,
index: 0,
merklePath: [1, 2, 3, 4, 5, 6, 7, 8],
rawTx: [4, 5, 6],
blockHash: randomBytesHex(32),
merkleRoot: randomBytesHex(32)
}
await storage.insertProvenTx(ptx, trx)
return ptx
}
static async insertTestProvenTxReq(
storage: StorageProvider,
txid?: string,
provenTxId?: number,
onlyRequired?: boolean
) {
const now = new Date()
const ptxreq: TableProvenTxReq = {
// Required:
created_at: now,
updated_at: now,
provenTxReqId: 0,
txid: txid || randomBytesHex(32),
status: 'nosend',
attempts: 0,
notified: false,
history: '{}',
notify: '{}',
rawTx: [4, 5, 6],
// Optional:
provenTxId: provenTxId || undefined,
batch: onlyRequired ? undefined : randomBytesBase64(10),
inputBEEF: onlyRequired ? undefined : [1, 2, 3]
}
await storage.insertProvenTxReq(ptxreq)
return ptxreq
}
static async insertTestUser(storage: StorageProvider, identityKey?: string) {
const now = new Date()
const e: TableUser = {
created_at: now,
updated_at: now,
userId: 0,
identityKey: identityKey || randomBytesHex(33),
activeStorage: storage.getSettings().storageIdentityKey
}
await storage.insertUser(e)
return e
}
static async insertTestCertificate(storage: StorageProvider, u?: TableUser) {
const now = new Date()
u ||= await _tu.insertTestUser(storage)
const e: TableCertificate = {
created_at: now,
updated_at: now,
certificateId: 0,
userId: u.userId,
type: randomBytesBase64(33),
serialNumber: randomBytesBase64(33),
certifier: randomBytesHex(33),
subject: randomBytesHex(33),
verifier: undefined,
revocationOutpoint: `${randomBytesHex(32)}.999`,
signature: randomBytesHex(50),
isDeleted: false
}
await storage.insertCertificate(e)
return e
}
static async insertTestCertificateField(storage: StorageProvider, c: TableCertificate, name: string, value: string) {
const now = new Date()
const e: TableCertificateField = {
created_at: now,
updated_at: now,
certificateId: c.certificateId,
userId: c.userId,
fieldName: name,
fieldValue: value,
masterKey: randomBytesBase64(40)
}
await storage.insertCertificateField(e)
return e
}
static async insertTestOutputBasket(
storage: StorageProvider,
u?: TableUser | number,
partial?: Partial<TableOutputBasket>
) {
const now = new Date()
let user: TableUser
if (u === undefined) {
user = await _tu.insertTestUser(storage)
} else if (typeof u === 'number') {
user = verifyOne(await storage.findUsers({ partial: { userId: u } })) as TableUser
} else {
user = u
}
const e: TableOutputBasket = {
created_at: now,
updated_at: now,
basketId: 0,
userId: user.userId,
name: randomBytesHex(6),
numberOfDesiredUTXOs: 42,
minimumDesiredUTXOValue: 1642,
isDeleted: false,
...(partial || {})
}
await storage.insertOutputBasket(e)
return e
}
static async insertTestTransaction(
storage: StorageProvider,
u?: TableUser,
onlyRequired?: boolean,
partial?: Partial<TableTransaction>
) {
const now = new Date()
u ||= await _tu.insertTestUser(storage)
const e: TableTransaction = {
// Required:
created_at: now,
updated_at: now,
transactionId: 0,
userId: u.userId,
status: 'nosend',
reference: randomBytesBase64(10),
isOutgoing: true,
satoshis: 9999,
description: 'buy me a river',
// Optional:
version: onlyRequired ? undefined : 0,
lockTime: onlyRequired ? undefined : 500000000,
txid: onlyRequired ? undefined : randomBytesHex(32),
inputBEEF: onlyRequired ? undefined : new Beef().toBinary(),
rawTx: onlyRequired ? undefined : [1, 2, 3],
...(partial || {})
}
await storage.insertTransaction(e)
return { tx: e, user: u }
}
static async insertTestOutput(
storage: StorageProvider,
t: TableTransaction,
vout: number,
satoshis: number,
basket?: TableOutputBasket,
requiredOnly?: boolean,
partial?: Partial<TableOutput>
) {
const now = new Date()
const e: TableOutput = {
created_at: now,
updated_at: now,
outputId: 0,
userId: t.userId,
transactionId: t.transactionId,
basketId: basket ? basket.basketId : undefined,
spendable: true,
change: true,
outputDescription: 'not mutch to say',
vout,
satoshis,
providedBy: 'you',
purpose: 'secret',
type: 'custom',
txid: requiredOnly ? undefined : randomBytesHex(32),
senderIdentityKey: requiredOnly ? undefined : randomBytesHex(32),
derivationPrefix: requiredOnly ? undefined : randomBytesHex(16),
derivationSuffix: requiredOnly ? undefined : randomBytesHex(16),
spentBy: undefined, // must be a valid transactionId
sequenceNumber: requiredOnly ? undefined : 42,
spendingDescription: requiredOnly ? undefined : randomBytesHex(16),
scriptLength: requiredOnly ? undefined : 36,
scriptOffset: requiredOnly ? undefined : 12,
lockingScript: requiredOnly ? undefined : asArray(randomBytesHex(36)),
...(partial || {})
}
await storage.insertOutput(e)
return e
}
static async insertTestOutputTag(storage: StorageProvider, u: TableUser, partial?: Partial<TableOutputTag>) {
const now = new Date()
const e: TableOutputTag = {
created_at: now,
updated_at: now,
outputTagId: 0,
userId: u.userId,
tag: randomBytesHex(6),
isDeleted: false,
...(partial || {})
}
await storage.insertOutputTag(e)
return e
}
static async insertTestOutputTagMap(storage: StorageProvider, o: TableOutput, tag: TableOutputTag) {
const now = new Date()
const e: TableOutputTagMap = {
created_at: now,
updated_at: now,
outputTagId: tag.outputTagId,
outputId: o.outputId,
isDeleted: false
}
await storage.insertOutputTagMap(e)
return e
}
static async insertTestTxLabel(storage: StorageProvider, u: TableUser, partial?: Partial<TableTxLabel>) {
const now = new Date()
const e: TableTxLabel = {
created_at: now,
updated_at: now,
txLabelId: 0,
userId: u.userId,
label: randomBytesHex(6),
isDeleted: false,
...(partial || {})
}
await storage.insertTxLabel(e)
return e
}
static async insertTestTxLabelMap(
storage: StorageProvider,
tx: TableTransaction,
label: TableTxLabel,
partial?: Partial<TableTxLabelMap>
) {
const now = new Date()
const e: TableTxLabelMap = {
created_at: now,
updated_at: now,
txLabelId: label.txLabelId,
transactionId: tx.transactionId,
isDeleted: false,
...(partial || {})
}
await storage.insertTxLabelMap(e)
return e
}
static async insertTestSyncState(storage: StorageProvider, u: TableUser) {
const now = new Date()
const settings = await storage.getSettings()
const e: TableSyncState = {
created_at: now,
updated_at: now,
syncStateId: 0,
userId: u.userId,
storageIdentityKey: settings.storageIdentityKey,
storageName: settings.storageName,
status: 'unknown',
init: false,
refNum: randomBytesBase64(10),
syncMap: '{}'
}
await storage.insertSyncState(e)
return e
}
static async insertTestMonitorEvent(storage: StorageProvider) {
const now = new Date()
const e: TableMonitorEvent = {
created_at: now,
updated_at: now,
id: 0,
event: 'nothing much happened'
}
await storage.insertMonitorEvent(e)
return e
}
static async insertTestCommission(storage: StorageProvider, t: TableTransaction) {
const now = new Date()
const e: TableCommission = {
created_at: now,
updated_at: now,
commissionId: 0,
userId: t.userId,
transactionId: t.transactionId,
satoshis: 200,
keyOffset: randomBytesBase64(32),
isRedeemed: false,
lockingScript: [1, 2, 3]
}
await storage.insertCommission(e)
return e
}
static async createTestSetup1(storage: StorageProvider, u1IdentityKey?: string): Promise<TestSetup1> {
const u1 = await _tu.insertTestUser(storage, u1IdentityKey)
const u1basket1 = await _tu.insertTestOutputBasket(storage, u1)
const u1basket2 = await _tu.insertTestOutputBasket(storage, u1)
const u1label1 = await _tu.insertTestTxLabel(storage, u1)
const u1label2 = await _tu.insertTestTxLabel(storage, u1)
const u1tag1 = await _tu.insertTestOutputTag(storage, u1)
const u1tag2 = await _tu.insertTestOutputTag(storage, u1)
const { tx: u1tx1 } = await _tu.insertTestTransaction(storage, u1)
const u1comm1 = await _tu.insertTestCommission(storage, u1tx1)
const u1tx1label1 = await _tu.insertTestTxLabelMap(storage, u1tx1, u1label1)
const u1tx1label2 = await _tu.insertTestTxLabelMap(storage, u1tx1, u1label2)
const u1tx1o0 = await _tu.insertTestOutput(storage, u1tx1, 0, 101, u1basket1)
const u1o0tag1 = await _tu.insertTestOutputTagMap(storage, u1tx1o0, u1tag1)
const u1o0tag2 = await _tu.insertTestOutputTagMap(storage, u1tx1o0, u1tag2)
const u1tx1o1 = await _tu.insertTestOutput(storage, u1tx1, 1, 111, u1basket2)
const u1o1tag1 = await _tu.insertTestOutputTagMap(storage, u1tx1o1, u1tag1)
const u1cert1 = await _tu.insertTestCertificate(storage, u1)
const u1cert1field1 = await _tu.insertTestCertificateField(storage, u1cert1, 'bob', 'your uncle')
const u1cert1field2 = await _tu.insertTestCertificateField(storage, u1cert1, 'name', 'alice')
const u1cert2 = await _tu.insertTestCertificate(storage, u1)
const u1cert2field1 = await _tu.insertTestCertificateField(storage, u1cert2, 'name', 'alice')
const u1cert3 = await _tu.insertTestCertificate(storage, u1)
const u1sync1 = await _tu.insertTestSyncState(storage, u1)
const u2 = await _tu.insertTestUser(storage)
const u2basket1 = await _tu.insertTestOutputBasket(storage, u2)
const u2label1 = await _tu.insertTestTxLabel(storage, u2)
const { tx: u2tx1 } = await _tu.insertTestTransaction(storage, u2, true)
const u2comm1 = await _tu.insertTestCommission(storage, u2tx1)
const u2tx1label1 = await _tu.insertTestTxLabelMap(storage, u2tx1, u2label1)
const u2tx1o0 = await _tu.insertTestOutput(storage, u2tx1, 0, 101, u2basket1)
const { tx: u2tx2 } = await _tu.insertTestTransaction(storage, u2, true)
const u2comm2 = await _tu.insertTestCommission(storage, u2tx2)
const proven1 = await _tu.insertTestProvenTx(storage)
const req1 = await _tu.insertTestProvenTxReq(storage, undefined, undefined, true)
const req2 = await _tu.insertTestProvenTxReq(storage, proven1.txid, proven1.provenTxId)
const we1 = await _tu.insertTestMonitorEvent(storage)
return {
u1,
u1basket1,
u1basket2,
u1label1,
u1label2,
u1tag1,
u1tag2,
u1tx1,
u1comm1,
u1tx1label1,
u1tx1label2,
u1tx1o0,
u1o0tag1,
u1o0tag2,
u1tx1o1,
u1o1tag1,
u1cert1,
u1cert1field1,
u1cert1field2,
u1cert2,
u1cert2field1,
u1cert3,
u1sync1,
u2,
u2basket1,
u2label1,
u2tx1,
u2comm1,
u2tx1label1,
u2tx1o0,
u2tx2,
u2comm2,
proven1,
req1,
req2,
we1
}
}
static async createTestSetup2(
storage: StorageProvider,
identityKey: string,
mockData: MockData = { actions: [] }
): Promise<TestSetup2> {
if (!mockData || !mockData.actions) {
throw new Error('mockData.actions is required')
}
const now = new Date()
const inputTxMap: Record<string, any> = {}
const outputMap: Record<string, any> = {}
// only one user
const user = await _tu.insertTestUser(storage, identityKey)
// First create your output that represent your inputs
for (const action of mockData.actions) {
for (const input of action.inputs || []) {
let prevOutput = outputMap[input.sourceOutpoint]
if (!prevOutput) {
const { tx: transaction } = await _tu.insertTestTransaction(storage, user, false, {
txid: input.sourceOutpoint.split('.')[0],
satoshis: input.sourceSatoshis,
status: 'confirmed' as sdk.TransactionStatus,
description: 'Generated transaction for input',
lockTime: 0,
version: 1,
inputBEEF: [1, 2, 3, 4],
rawTx: [4, 3, 2, 1]
})
const basket = await _tu.insertTestOutputBasket(storage, user, {
name: randomBytesHex(6)
})
// Need to convert
const lockingScriptValue = input.sourceLockingScript
? Utils.toArray(input.sourceLockingScript, 'hex')
: undefined
prevOutput = await _tu.insertTestOutput(
storage,
transaction,
0,
input.sourceSatoshis,
basket,
true, // Needs to be spendable
{
outputDescription: input.inputDescription,
spendable: true,
vout: Number(input.sourceOutpoint.split('.')[1]),
lockingScript: lockingScriptValue,
txid: transaction.txid
}
)
// Store in maps for later use
inputTxMap[input.sourceOutpoint] = transaction
outputMap[input.sourceOutpoint] = prevOutput
}
}
}
// Process transactions that spend those previous outputs
for (const action of mockData.actions) {
const { tx: transaction } = await _tu.insertTestTransaction(storage, user, false, {
txid: `${action.txid}` || `tx_${action.satoshis}_${Date.now()}`,
satoshis: action.satoshis,
status: action.status as sdk.TransactionStatus,
description: action.description,
lockTime: action.lockTime,
version: action.version,
inputBEEF: [1, 2, 3, 4],
rawTx: [4, 3, 2, 1]
})
// Loop through action inputs and update chosen outputs
for (const input of action.inputs || []) {
// Output must exist before updating
const prevOutput = outputMap[input.sourceOutpoint]
if (!prevOutput) {
throw new Error(`UTXO not found in outputMap for sourceOutpoint: ${input.sourceOutpoint}`)
}
// Set correct output fields as per input fields
await storage.updateOutput(prevOutput.outputId, {
spendable: false, // Mark output as spent
spentBy: transaction.transactionId, // Reference the new transaction
spendingDescription: input.inputDescription, // Store description
sequenceNumber: input.sequenceNumber // Store sequence number
})
}
// Insert any new outputs for the transaction
if (action.outputs) {
for (const output of action.outputs) {
const basket = await _tu.insertTestOutputBasket(storage, user, {
name: output.basket
})
const insertedOutput = await _tu.insertTestOutput(
storage,
transaction,
output.outputIndex,
output.satoshis,
basket,
false,
{
outputDescription: output.outputDescription,
spendable: output.spendable,
txid: transaction.txid
}
)
// Store this output in the map for future transactions to reference
outputMap[`${action.txid}.${output.outputIndex}`] = insertedOutput
}
}
// Labels inserted
if (action.labels) {
for (const label of action.labels) {
const l = await _tu.insertTestTxLabel(storage, user, {
label,
isDeleted: false,
created_at: now,
updated_at: now,
txLabelId: 0,
userId: user.userId
})
await _tu.insertTestTxLabelMap(storage, transaction, l)
}