Files
xo-stack/packages/stack/src/wallet/wallet-p2pkh.ts
T
2025-05-05 03:19:47 +00:00

449 lines
12 KiB
TypeScript

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<T extends typeof WalletP2PKH>(
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<T>;
// Return the wallet instance.
return wallet;
}
static fromBytes<T extends typeof WalletP2PKH>(
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<T>(services, {
type: 'WalletP2PKH',
privateKey: privateKey.toHex(),
});
}
static fromHex<T extends typeof WalletP2PKH>(
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<T>(services, {
type: 'WalletP2PKH',
privateKey: privateKey.toHex(),
});
}
static fromWIF<T extends typeof WalletP2PKH>(
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<T>(services, {
type: 'WalletP2PKH',
privateKey: privateKey.toHex(),
});
}
static generateRandom<T extends typeof WalletP2PKH>(
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<T>(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<WalletDependencies>,
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<Uint8Array> {
// 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<TransactionTemplate>) {
// 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<TransactionTemplate>) {
// 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<T extends WalletP2PKH = WalletP2PKH> = (
dependencies: WalletDependencies,
genesisData: WalletP2PKHGenesisData,
) => Promise<T>;
export async function useWalletP2PKH<T extends WalletP2PKH = WalletP2PKH>(
dependencies: WalletDependencies,
genesisData: WalletP2PKHGenesisData,
): Promise<T> {
return (await WalletP2PKH.from(dependencies, genesisData)) as T;
}