UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

323 lines (299 loc) 9.56 kB
import { BEEF, CachedKeyDeriver, CreateActionArgs, CreateActionOptions, CreateActionOutput, CreateActionResult, KeyDeriver, KeyDeriverApi, LockingScript, P2PKH, PrivateKey, PublicKey, ScriptTemplateUnlock, WalletInterface } from '@bsv/sdk' import { KeyPairAddress, SetupClientWalletArgs, SetupWallet, SetupWalletClient } from './SetupWallet' import { StorageIdb } from './storage/StorageIdb' import { WalletStorageManager } from './storage/WalletStorageManager' import { Services } from './services/Services' import { Monitor } from './monitor/Monitor' import { PrivilegedKeyManager } from './sdk/PrivilegedKeyManager' import { Wallet } from './Wallet' import { Chain } from './sdk/types' import { randomBytesHex } from './utility/utilityHelpers' import { StorageClient } from './storage/remoting/StorageClient' /** * The 'Setup` class provides static setup functions to construct BRC-100 compatible * wallets in a variety of configurations. * * It serves as a starting point for experimentation and customization. */ export abstract class SetupClient { /** * Create a `Wallet`. Storage can optionally be provided or configured later. * * The following components are configured: KeyDeriver, WalletStorageManager, WalletService, WalletStorage. * Optionally, PrivilegedKeyManager is also configured. * * @publicbody */ static async createWallet(args: SetupClientWalletArgs): Promise<SetupWallet> { const chain = args.chain const rootKey = PrivateKey.fromHex(args.rootKeyHex) const identityKey = rootKey.toPublicKey().toString() const keyDeriver = new CachedKeyDeriver(rootKey) const storage = new WalletStorageManager(identityKey, args.active, args.backups) if (storage.canMakeAvailable()) await storage.makeAvailable() const serviceOptions = Services.createDefaultOptions(chain) serviceOptions.taalApiKey = args.taalApiKey const services = new Services(serviceOptions) const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services) const monitor = new Monitor(monopts) monitor.addDefaultTasks() const privilegedKeyManager = args.privilegedKeyGetter ? new PrivilegedKeyManager(args.privilegedKeyGetter) : undefined const wallet = new Wallet({ chain, keyDeriver, storage, services, monitor, privilegedKeyManager }) const r: SetupWallet = { rootKey, identityKey, keyDeriver, chain, storage, services, monitor, wallet } return r } /** * Setup a new `Wallet` without requiring a .env file. * * @param args.chain - 'main' or 'test' * @param args.rootKeyHex - Root private key for wallet's key deriver. * @param args.storageUrl - Optional. `StorageClient` and `chain` compatible endpoint URL. * @param args.privilegedKeyGetter - Optional. Method that will return the privileged `PrivateKey`, on demand. */ static async createWalletClientNoEnv(args: { chain: Chain rootKeyHex: string storageUrl?: string privilegedKeyGetter?: () => Promise<PrivateKey> }): Promise<Wallet> { const chain = args.chain const endpointUrl = args.storageUrl || `https://${args.chain !== 'main' ? 'staging-' : ''}storage.babbage.systems` const rootKey = PrivateKey.fromHex(args.rootKeyHex) const keyDeriver = new CachedKeyDeriver(rootKey) const storage = new WalletStorageManager(keyDeriver.identityKey) const services = new Services(chain) const privilegedKeyManager = args.privilegedKeyGetter ? new PrivilegedKeyManager(args.privilegedKeyGetter) : undefined const wallet = new Wallet({ chain, keyDeriver, storage, services, privilegedKeyManager }) const client = new StorageClient(wallet, endpointUrl) await storage.addWalletStorageProvider(client) await storage.makeAvailable() return wallet } /** * @publicbody */ static async createWalletClient(args: SetupClientWalletClientArgs): Promise<SetupWalletClient> { const wo = await SetupClient.createWallet(args) const endpointUrl = args.endpointUrl || `https://${args.chain !== 'main' ? 'staging-' : ''}storage.babbage.systems` const client = new StorageClient(wo.wallet, endpointUrl) await wo.storage.addWalletStorageProvider(client) await wo.storage.makeAvailable() return { ...wo, endpointUrl } } /** * @publicbody */ static getKeyPair(priv?: string | PrivateKey): KeyPairAddress { 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 } } /** * @publicbody */ static getLockP2PKH(address: string): LockingScript { const p2pkh = new P2PKH() const lock = p2pkh.lock(address) return lock } /** * @publicbody */ static getUnlockP2PKH(priv: PrivateKey, satoshis: number): ScriptTemplateUnlock { const p2pkh = new P2PKH() const lock = SetupClient.getLockP2PKH(SetupClient.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 } /** * @publicbody */ static createP2PKHOutputs( outputs: { address: string satoshis: number outputDescription?: string basket?: string tags?: string[] }[] ): CreateActionOutput[] { const os: CreateActionOutput[] = [] const count = outputs.length for (let i = 0; i < count; i++) { const o = outputs[i] os.push({ basket: o.basket, tags: o.tags, satoshis: o.satoshis, lockingScript: SetupClient.getLockP2PKH(o.address).toHex(), outputDescription: o.outputDescription || `p2pkh ${i}` }) } return os } /** * @publicbody */ static async createP2PKHOutputsAction( wallet: WalletInterface, outputs: { address: string satoshis: number outputDescription?: string basket?: string tags?: string[] }[], options?: CreateActionOptions ): Promise<{ cr: CreateActionResult outpoints: string[] | undefined }> { const os = SetupClient.createP2PKHOutputs(outputs) const createArgs: CreateActionArgs = { description: `createP2PKHOutputs`, outputs: os, options: { ...options, // Don't randomize so we can simplify outpoint creation randomizeOutputs: false } } const cr = await wallet.createAction(createArgs) let outpoints: string[] | undefined if (cr.txid) { outpoints = os.map((o, i) => `${cr.txid}.${i}`) } return { cr, outpoints } } /** * @publicbody */ static async fundWalletFromP2PKHOutpoints( wallet: WalletInterface, outpoints: string[], p2pkhKey: KeyPairAddress, inputBEEF?: BEEF ) { // TODO } /** * Adds `indexedDB` based storage to a `Wallet` configured by `SetupClient.createWalletOnly` * * @param args.databaseName Name for this storage. For MySQL, the schema name within the MySQL instance. * @param args.chain Which chain this wallet is on: 'main' or 'test'. Defaults to 'test'. * @param args.rootKeyHex * * @publicbody */ static async createWalletIdb(args: SetupWalletIdbArgs): Promise<SetupWalletIdb> { const wo = await SetupClient.createWallet(args) const activeStorage = await SetupClient.createStorageIdb(args) await wo.storage.addWalletStorageProvider(activeStorage) const { user, isNew } = await activeStorage.findOrInsertUser(wo.identityKey) const userId = user.userId const r: SetupWalletIdb = { ...wo, activeStorage, userId } return r } /** * @returns {StorageIdb} - `Knex` based storage provider for a wallet. May be used for either active storage or backup storage. */ static async createStorageIdb(args: SetupWalletIdbArgs): Promise<StorageIdb> { const storage = new StorageIdb({ chain: args.chain, commissionSatoshis: 0, commissionPubKeyHex: undefined, feeModel: { model: 'sat/kb', value: 1 } }) await storage.migrate(args.databaseName, randomBytesHex(33)) await storage.makeAvailable() return storage } } /** * */ export interface SetupWalletIdbArgs extends SetupClientWalletArgs { databaseName: string } /** * */ export interface SetupWalletIdb extends SetupWallet { activeStorage: StorageIdb userId: number rootKey: PrivateKey identityKey: string keyDeriver: KeyDeriverApi chain: Chain storage: WalletStorageManager services: Services monitor: Monitor wallet: Wallet } /** * Extension `SetupWalletClientArgs` of `SetupWalletArgs` is used by `createWalletClient` * to construct a `SetupWalletClient`. */ export interface SetupClientWalletClientArgs extends SetupClientWalletArgs { /** * The endpoint URL of a service hosting the `StorageServer` JSON-RPC service to * which a `StorageClient` instance should connect to function as * the active storage provider of the newly created wallet. */ endpointUrl?: string }