449 lines
12 KiB
TypeScript
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;
|
|
}
|