Activities/Wallet Refactor

This commit is contained in:
Jim Hamill
2025-04-18 05:17:20 +00:00
parent 408d3b8d8c
commit 7e4508cf5a
18 changed files with 706 additions and 1111 deletions
+83 -148
View File
@@ -1,23 +1,11 @@
import { BaseWallet } from './base-wallet.js';
import { type TransactionTemplate } from './types.js';
import { TransactionBuilder } from './transaction-builder.js';
import { type WalletServices } from './types.js';
import {
WalletP2PKH,
WalletP2PKHFactory,
useWalletP2PKH,
} from './wallet-p2pkh.js';
import { type WalletDependencies } from './types.js';
import { WalletP2PKH } from './wallet-p2pkh.js';
import {
type BaseBlockchain,
type BlockchainTransactions,
type BlockchainUTXOs,
} from '../blockchain';
import {
type BaseStore,
type StoreValueProxy,
StoreInMemory,
} from '../storage';
import { type BaseBlockchain } from '../blockchain';
import { type BaseStore, Activities, StoreInMemory } from '../storage';
import {
type TokenBalances,
@@ -45,15 +33,6 @@ export type WalletHDGenesisData = {
addressGap?: number;
} & ({ seed: string } | { mnemonic: string } | { xpriv: string });
export type WalletHDEvents<WalletType> = {
walletDataUpdated: WalletHDGenesisData;
shouldMonitorUpdated: boolean;
walletsUpdated: Array<WalletType>;
transactionsUpdated: BlockchainTransactions;
unspentsUpdated: BlockchainUTXOs;
stateUpdated: string | null;
};
/**
* A Hierarhical Determinstic (BIP44) Wallet.
*
@@ -78,92 +57,72 @@ export type WalletHDEvents<WalletType> = {
* await wallet = await walletHD.start();
* ```
*/
export class WalletHD<
WalletType extends WalletP2PKH = WalletP2PKH,
> extends BaseWallet<WalletHDEvents<WalletType>> {
// Mutable State.
public shouldMonitor = false;
// Store values that are proxied for convenience/clarity.
// NOTE: We define these for clarity so that we can see the state stored with this wallet.
public genesisData: StoreValueProxy<WalletHDGenesisData>;
// Store child wallets, grouped by chain ("change") path in BIP44 spec.
public wallets: { [chainPath: number]: Array<WalletType> } = {};
export class WalletHD extends BaseWallet {
static async from<T extends typeof WalletHD>(
this: T,
services: WalletServices,
genesisData?: WalletHDGenesisData,
walletFactory?: WalletP2PKHFactory<any>,
services: WalletDependencies,
genesisData: WalletHDGenesisData,
) {
// Assign in memory stores as defaults in case otherwise not provided.
const servicesFinal = {
...{
// Default to an in-memory-only Activity store.
activities: await Activities.fromStores(StoreInMemory.from()),
store: new StoreInMemory(),
cache: new StoreInMemory(),
},
...services,
};
// If we are attempting to create a new wallet...
if (genesisData) {
// Ensure that the provided store does not already contain data.
if (Object.keys(await servicesFinal.store.all()).length) {
throw new Error(`Provided store already contains data`);
}
// Store the wallet genesis data.
await servicesFinal.store.set<WalletHDGenesisData>(
'genesis',
genesisData,
);
}
// Retrieve our genesis data and extract the private key.
const retrievedGenesisData =
await servicesFinal.store.getOrFail<WalletHDGenesisData>('genesis');
let hdPrivateNode: HDPrivateNode;
// Instantiate the HDPrivateNode from the type specified in metadata.
if ('seed' in retrievedGenesisData) {
hdPrivateNode = HDPrivateNode.fromSeed(
hexToBin(retrievedGenesisData.seed),
);
} else if ('mnemonic' in retrievedGenesisData) {
hdPrivateNode = HDPrivateNode.fromMnemonic(
Mnemonic.fromPhrase(retrievedGenesisData.mnemonic),
);
} else {
hdPrivateNode = HDPrivateNode.fromXPriv(retrievedGenesisData.xpriv);
}
// Return a new instance of WalletHD.
return new this(
servicesFinal.blockchain,
servicesFinal.store,
servicesFinal.cache,
hdPrivateNode,
walletFactory,
) as InstanceType<T>;
return new this(servicesFinal, genesisData) as InstanceType<T>;
}
// Dependencies.
public readonly activities: Activities;
public readonly blockchain: BaseBlockchain;
public readonly cache: BaseStore;
// Genesis Data.
public readonly genesisData: WalletHDGenesisData;
// Optimizations/Simplifications.
public readonly hdPrivateNode: HDPrivateNode;
// Mutable State.
public shouldMonitor = false;
// Store child wallets, grouped by chain ("change") path in BIP44 spec.
public wallets: { [chainPath: number]: Array<WalletP2PKH> } = {};
constructor(
public blockchain: BaseBlockchain,
public store: BaseStore,
public cache: BaseStore,
public hdPrivateNode: HDPrivateNode,
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH,
dependencies: Required<WalletDependencies>,
genesisData: WalletHDGenesisData,
) {
super();
// Assign our dependencies.
this.activities = dependencies.activities;
this.blockchain = dependencies.blockchain;
this.cache = dependencies.cache;
// Assign our state.
this.genesisData = genesisData;
// Instantiate the HDPrivateNode from the type specified in our genesis data.
if ('seed' in genesisData) {
this.hdPrivateNode = HDPrivateNode.fromSeed(hexToBin(genesisData.seed));
} else if ('mnemonic' in genesisData) {
this.hdPrivateNode = HDPrivateNode.fromMnemonic(
Mnemonic.fromPhrase(genesisData.mnemonic),
);
} else {
this.hdPrivateNode = HDPrivateNode.fromXPriv(genesisData.xpriv);
}
// Initialize empty arrays for our wallets.
this.wallets[CHAIN_EXTERNAL] = [];
this.wallets[CHAIN_INTERNAL] = [];
// Initialize proxies to our stored data.
this.genesisData = this.store.proxy('genesis');
}
async getId() {
@@ -175,6 +134,17 @@ export class WalletHD<
);
}
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(encodeExtendedJson(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 () =>
@@ -189,12 +159,14 @@ export class WalletHD<
);
}
async start() {
// Get the genesis data.
const genesisData = await this.genesisData.getOrFail();
async getActivities(): Promise<Activities> {
return this.activities;
}
async start() {
// Get the address gap.
const addressGap = genesisData.addressGap || ADDRESS_GAP;
// TODO: This should come from activities.
const addressGap = this.genesisData.addressGap || ADDRESS_GAP;
// Scan the HDWallet for child wallets.
await this.scan(0, addressGap);
@@ -327,7 +299,7 @@ export class WalletHD<
async getAddresses() {
const chainPaths = [CHAIN_EXTERNAL, CHAIN_INTERNAL];
let addressPromises = [];
const addressPromises = [];
for (const chainPath of chainPaths) {
addressPromises.push(
@@ -384,17 +356,15 @@ export class WalletHD<
// Fetch unspents that are usable by this wallet.
const unspents = await this.getUnspentDirectives();
// Create an instance of our transaction builder and populate with our Wallet's UTXOs.
// NOTE: We must deal with tokens first and satoshis last.
// The reason is that the change output will be determined based on transaction fee.
const txBuilder = new TransactionBuilder(txTemplateFinal, unspents)
.addTokensFromUnspents()
.addTokenChangeOutputs(changeLockingBytecode)
.addSatoshisFromUnspents()
.addSatoshiChangeOutput(changeLockingBytecode);
// Build the transaction.
const tx = TransactionBuilder.build(
txTemplateFinal,
unspents,
changeLockingBytecode,
);
// Generate the transaction.
const generatedTxBytes = txBuilder.toBytes();
const generatedTxBytes = tx.toBytes();
return generatedTxBytes;
}
@@ -407,38 +377,18 @@ export class WalletHD<
await this.blockchain.broadcastTransaction(generatedTxBytes);
}
async addActivity<T>(activity: T) {
// Generate an Activity ID.
// TODO: Look more into how LibAuth intends to do this.
const activityId = Date.now().toString();
// Store the data.
await this.store.set<T>(activityId, activity);
}
async export() {
const allData = await this.store.all();
const asJson = encodeExtendedJson(allData);
return asJson;
}
async deriveWallets(
count: number,
chainPath = CHAIN_EXTERNAL,
startIndex = 0,
) {
// Get the derivation path from the genesis data.
const genesisData = await this.genesisData.getOrFail();
// Create an array to store our wallets.
const newWallets: Array<WalletType> = [];
const newWallets: Array<WalletP2PKH> = [];
// Create the given count of wallets.
for (let i = 0; i < count; i++) {
// Define the derivation path.
const derivationPath = `${genesisData.derivationPath}/${chainPath}/${startIndex + i}`;
const derivationPath = `${this.genesisData.derivationPath}/${chainPath}/${startIndex + i}`;
// Derive the private key at the given path.
const privateKeyHex = await this.cache.getOrSet(derivationPath, () => {
@@ -447,7 +397,7 @@ export class WalletHD<
});
// Create a child P2PKH wallet for this Private Key.
const wallet = await this.walletFactory(
const wallet = await WalletP2PKH.from(
{
blockchain: this.blockchain,
cache: this.cache,
@@ -485,7 +435,7 @@ export class WalletHD<
// Iterate over each of the provided chain (change) paths.
for (const chainPath of chainPaths) {
// Array to store active wallet nodes
const wallets: Array<WalletType> = [];
const wallets: Array<WalletP2PKH> = [];
let currentIndex = startIndex;
let emptyAddressCount = 0;
@@ -493,7 +443,7 @@ export class WalletHD<
// Continue scanning until we find 'addressGap' consecutive empty addresses
while (emptyAddressCount < addressGap) {
// Derive a number of wallets equivalent to our addressGap.
const batch: Array<WalletType> = await this.deriveWallets(
const batch: Array<WalletP2PKH> = await this.deriveWallets(
addressGap,
chainPath,
currentIndex,
@@ -527,19 +477,6 @@ export class WalletHD<
this.emit('stateUpdated', null);
}
protected async deriveKey(keyName: string): Promise<Uint8Array> {
// Get the genesis data for this wallet.
// TODO: This needs to change!
const genesisData = await this.store.getOrFail('genesis');
// Hash the genesis data using Hash256 and the key ("WalletID") for this derivation using Hash256.
const genesisDataHashed = hash256(utf8ToBin(JSON.stringify(genesisData)));
const keyHashed = hash256(utf8ToBin(keyName));
// Combine the hashes and do one final hash for good measure.
return hash256(new Uint8Array([...genesisDataHashed, ...keyHashed]));
}
}
//-----------------------------------------------------------------------------
@@ -547,15 +484,13 @@ export class WalletHD<
//-----------------------------------------------------------------------------
export type WalletHDFactory<T extends WalletHD = WalletHD> = (
services: WalletServices,
genesisData?: WalletHDGenesisData,
walletFactory?: WalletP2PKHFactory<any>,
services: WalletDependencies,
genesisData: WalletHDGenesisData,
) => Promise<T>;
export async function useWalletHD<T extends WalletHD = WalletHD>(
services: WalletServices,
genesisData?: WalletHDGenesisData,
walletFactory?: WalletP2PKHFactory<any>,
services: WalletDependencies,
genesisData: WalletHDGenesisData,
): Promise<T> {
return (await WalletHD.from(services, genesisData, walletFactory)) as T;
return (await WalletHD.from(services, genesisData)) as T;
}