Activities/Wallet Refactor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user