import { BaseWallet } from './base-wallet.js'; import { type TransactionTemplate, type WalletBlockchain, type WalletDependencies, } from './types.js'; import { TransactionBuilder } from './transaction-builder.js'; import { type AddressStatusPayload, type BlockchainTransaction, } from '../blockchain/index.js'; import { type BaseStore, Activities, StoreInMemory } from '../storage/index.js'; import { ExtMap, PrivateKey, calculateBalanceSats, calculateBalanceTokens, } from '@xocash/primitives'; import { type Input, binToHex, hash256, utf8ToBin, walletTemplateP2pkhNonHd, walletTemplateToCompilerBCH, } from '@bitauth/libauth'; export type WalletP2PKHGenesisData = { type: 'WalletP2PKH'; privateKey: string; }; /** * @example * ```ts * // Instantiate P2PKH Wallet. * const walletP2PKH = WalletP2PKH.fromWIF('someWif'); * * // Start monitoring the balance. * await walletP2PKH.startMonitoring(); * ``` */ export class WalletP2PKH extends BaseWallet { static async from( this: T, deps: WalletDependencies, genesisData: WalletP2PKHGenesisData, ) { // Assign defaults if not otherwise provideed. const activities = deps.activities ? deps.activities : await Activities.from(); const cache = deps.cache ? deps.cache : StoreInMemory.from(); // Assign our final dependencies. const depsFinal = { activities, cache, blockchain: deps.blockchain, }; // Return a new instance of Wallet P2PKH. const wallet = new this(depsFinal, genesisData) as InstanceType; // Return the wallet instance. return wallet; } static fromBytes( this: T, services: WalletDependencies, privateKeyBytes: Uint8Array, ) { // Create a Private Key entity from the provided bytes. const privateKey = PrivateKey.fromBytes(privateKeyBytes); // Return a new instance of Wallet P2PKH. return this.from(services, { type: 'WalletP2PKH', privateKey: privateKey.toHex(), }); } static fromHex( this: T, services: WalletDependencies, privateKeyHex: string, ) { // Create a Private Key entity from the provided bytes. const privateKey = PrivateKey.fromHex(privateKeyHex); // Return a new instance of Wallet P2PKH. return this.from(services, { type: 'WalletP2PKH', privateKey: privateKey.toHex(), }); } static fromWIF( this: T, services: WalletDependencies, privateKeyWif: string, ) { // Create a Private Key entity from the provided bytes. const privateKey = PrivateKey.fromWIF(privateKeyWif); // Return a new instance of Wallet P2PKH. return this.from(services, { type: 'WalletP2PKH', privateKey: privateKey.toHex(), }); } static generateRandom( this: T, services: WalletDependencies, ) { // Create a Private Key entity generated from random bytes. const privateKey = PrivateKey.generateRandom(); // Return a new instance of Wallet P2PKH. return this.from(services, { type: 'WalletP2PKH', privateKey: privateKey.toHex(), }); } // Dependencies. public readonly activities: Activities; public readonly blockchain: WalletBlockchain; public readonly cache: BaseStore; // Genesis Data. public readonly genesisData: WalletP2PKHGenesisData; // Optimizations/Simplifications. public readonly privateKey: PrivateKey; // Mutable State. public isStarted = false; public status: string | null = null; constructor( dependencies: Required, genesisData: WalletP2PKHGenesisData, ) { super(); // Assign our dependencies. this.activities = dependencies.activities; this.blockchain = dependencies.blockchain; this.cache = dependencies.cache; // Assign our Genesis Data. this.genesisData = genesisData; // Optimizations/Simplifications. this.privateKey = PrivateKey.fromHex(genesisData.privateKey); } async getId() { // NOTE: Use the cache to optimize future calls. return binToHex( await this.cache.getOrSet('walletId', async () => this.deriveKey('walletId'), ), ); } async deriveKey(keyName: string): Promise { // Hash the genesis data using Hash256 and the key ("WalletID") for this derivation using Hash256. const genesisDataHashed = hash256( utf8ToBin(JSON.stringify(this.genesisData)), ); const keyHashed = hash256(utf8ToBin(keyName)); // Combine the hashes and do one final hash for good measure. return hash256(new Uint8Array([...genesisDataHashed, ...keyHashed])); } async getEncryptionKey() { // NOTE: Use the cache to optimize future calls. return await this.cache.getOrSet('encryptionKey', async () => this.deriveKey('encryptionKey'), ); } async getSigningKey() { // NOTE: Use the cache to optimize future calls. return this.cache.getOrSet('signingKey', async () => this.deriveKey('signingKey'), ); } async start() { // Do nothing if already started. if (this.isStarted) { return; } // Get the receiving address of this wallet so we can monitor it. const receivingAddress = await this.getReceivingAddress(); // Start monitoring the receiving address. await this.blockchain.subscribeAddress( receivingAddress.toCashAddr(), this.onAddressNotification.bind(this), ); // Mark this wallet as started. this.isStarted = true; } async stop() { // Do nothing if not started. if (!this.isStarted) { return; } // Get the receiving address of this wallet so we can stop monitoring it. const receivingAddress = await this.getReceivingAddress(); // Stop monitoring the receiving address. await this.blockchain.unsubscribeAddress( receivingAddress.toCashAddr(), this.onAddressNotification.bind(this), ); // Mark this wallet as stopped. this.isStarted = false; } async destroy() { this.removeAllListeners(); } async getTransactions() { // Get this node's address. const address = await this.getReceivingAddress(); // Get transactions involving this address. const transactions = await this.blockchain.fetchAddressHistory( address.toCashAddr(), ); // Define a function to get a single source output for an input. const getSourceOutput = async (input: Input) => { const sourceTx = await this.blockchain.fetchTransaction( binToHex(input.outpointTransactionHash), ); const sourceOutput = sourceTx.getOutputs().at(input.outpointIndex); if (!sourceOutput) { throw new Error(`Failed to find source output`); } return sourceOutput; }; // Define a function to get all source outputs for a tx. const getSourceOutputs = async (tx: BlockchainTransaction) => { const sourceOutputPromises = tx.transaction .getInputs() .map((input) => getSourceOutput(input)); return await Promise.all(sourceOutputPromises); }; // Get the source outputs for each of the transactions. const walletTxs = await Promise.all( transactions.toArray().map(async (tx) => ({ ...tx, sourceOutputs: await getSourceOutputs(tx), })), ); // Wait for source outputs to be populated. const walletTxsIndexed = ExtMap.fromArray(walletTxs, (tx) => tx.hash.toHex(), ); // TODO: Merge activity information into a WalletTransaction type. // 2024-11-17: Actually, that might not belong here? // Emit an event to notify that transactions have been updated. this.emit('transactionsUpdated', walletTxsIndexed); // Return the fetched transactions. return walletTxsIndexed; } async getUnspents() { // Get this wallet's address. const address = await this.getReceivingAddress(); // Fetch unspents belonging to this address. const unspents = await this.blockchain.fetchUnspents(address.toCashAddr()); // TODO: Merge activity information into a WalletUnspent type. // Emit an event to notify that unspents have been updated. this.emit('unspentsUpdated', unspents); // Return the unspents. return unspents; } async getUnspentDirectives() { // Get a list of unspent outputs. const unspentOutputs = await this.getUnspents(); // Create our P2PKH Compiler. const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd); // Transform our inputs into unlocking directives. const inputDirectives = unspentOutputs.map((blockchainUTXO) => ({ outpointIndex: blockchainUTXO.utxo.outpoint.outpointIndex, outpointTransactionHash: blockchainUTXO.utxo.outpoint.outpointTransactionHash, sequenceNumber: 0, unlockingBytecode: { compiler: compilerP2PKH, data: { keys: { privateKeys: { key: this.privateKey.toBytes() } }, }, script: 'unlock', token: blockchainUTXO.utxo.output.token, valueSatoshis: blockchainUTXO.utxo.output.valueSatoshis, }, })); // Return the directives. return inputDirectives; } async getBalanceSats() { // Fetch our unspents. const unspents = await this.getUnspents(); // Get the individual UTXOs. const utxos = unspents .map((blockchainUTXO) => blockchainUTXO.utxo) .toArray(); // Calculate the balance. return calculateBalanceSats(utxos); } async getBalanceTokens() { // Fetch our unspents. const unspents = await this.getUnspents(); // Get the individual UTXOs. const utxos = unspents .map((blockchainUTXO) => blockchainUTXO.utxo) .toArray(); // Calculate the token balances. return calculateBalanceTokens(utxos); } async getAddresses() { const addresses = await Promise.all([this.getReceivingAddress()]); return ExtMap.fromArray(addresses, (address) => address.toLockscriptHex()); } async getReceivingAddress() { return this.privateKey.derivePublicKey().deriveAddress(); } async signTransaction(txTemplate: Partial) { // Populate the final transaction template by merging with defaults. // NOTE: This is just a convenience to make "send" easier to use. const txTemplateFinal: TransactionTemplate = { ...{ version: 2, locktime: 0, inputs: [], outputs: [], }, ...txTemplate, }; // Get the receiving address of this wallet. const receivingAddress = await this.getReceivingAddress(); // Get the change locking bytecode (change address) for this wallet. const changeLockingBytecode = receivingAddress.toLockscriptBytes(); // Fetch unspents that are usable by this wallet. const unspents = await this.getUnspentDirectives(); // Build the transaction. const tx = TransactionBuilder.build( txTemplateFinal, unspents.toArray(), changeLockingBytecode, ); // Generate the transaction. const generatedTxBytes = tx.toBytes(); return generatedTxBytes; } async sendTransaction(txTemplate: Partial) { // Generate the transaction. const generatedTxBytes = await this.signTransaction(txTemplate); // Broadcast the transaction to the blockchain. await this.blockchain.broadcastTransaction(generatedTxBytes); } private async onAddressNotification( _address: string, payload?: AddressStatusPayload, ) { // If wallet state has changed... if (payload && payload.status !== this.status) { // Set our new status. this.status = payload.status; // Emit an event. this.emit('stateUpdated', this.status); } } } //----------------------------------------------------------------------------- // Factories Types/Functions //----------------------------------------------------------------------------- export type WalletP2PKHFactory = ( dependencies: WalletDependencies, genesisData: WalletP2PKHGenesisData, ) => Promise; export async function useWalletP2PKH( dependencies: WalletDependencies, genesisData: WalletP2PKHGenesisData, ): Promise { return (await WalletP2PKH.from(dependencies, genesisData)) as T; }