Doc commit tag conventions

This commit is contained in:
Jim Hamill
2025-03-10 05:36:16 +00:00
parent 307650e9fc
commit 0346f3f79e
23 changed files with 1290 additions and 286 deletions
+55 -3
View File
@@ -66,6 +66,51 @@ export type PolicyFunction<T = any> = (
newActivity: Activity<string, T>,
) => Activity<string, T>;
/**
* Implements an event sourcing pattern for managing application state.
*
* Event sourcing is an architectural pattern where:
* - All changes to application state are stored as a sequence of immutable events
* - The current state is derived by replaying these events in chronological order
* - Events serve as both the source of truth and a complete audit log
*
* This implementation provides:
* - Eventually consistent state across distributed systems
* - Conflict resolution through configurable policies
* - Point-in-time state reconstruction capability
* - Separation between event storage and derived state (snapshot)
*
* Events (Activities) in this system:
* - Have a unique identifier
* - Include a timestamp for ordering
* - Contain a type and associated value
* - Are processed according to registered type-specific policies
*
* @example
* ```typescript
* // Create Activities manager with a store implementation
* const activities = new Activities(new MyBaseStore());
*
* // Register a custom policy for wallet name conflicts
* activities.registerPolicy('WalletName', (existing, newActivity) => {
* // Keep the most recent wallet name
* return existing?.timestamp! > newActivity.timestamp! ? existing : newActivity;
* });
*
* // Initialize by compiling snapshot from stored events
* await activities.compileSnapshot();
*
* // Add a new activity
* await activities.addActivity({
* type: 'WalletName',
* id: 'wallet123',
* value: 'My Primary Wallet'
* });
* ```
*
* @template SnapshotByType - Structured representation of the current system state
* @template PolicyFunction - Function type for conflict resolution strategies
*/
export class Activities {
// TODO: Consider splitting into its own mutable class: ActivitySnapshot.
// This might be cleaner as we'll likely want to be able to view a raw timeline of activities.
@@ -110,6 +155,10 @@ export class Activities {
storeKey: string,
activity: Activity<string, any>,
) {
if (!activity) {
return;
}
if (!activity.timestamp) {
console.warn('Activity without timestamp encountered:', storeKey);
return;
@@ -132,12 +181,12 @@ export class Activities {
const oldStoreKey = this.storeKeyMap.get(snapshotKey);
if (oldStoreKey) {
// Delete the old activity from the store
await this.store.delete(oldStoreKey);
await this.store.set(oldStoreKey, null);
}
this.storeKeyMap.set(snapshotKey, storeKey);
} else {
// If we kept the existing activity, delete the new activity from the store
await this.store.delete(storeKey);
await this.store.set(storeKey, null);
}
}
@@ -167,8 +216,11 @@ export class Activities {
const activities = await this.store.all<Activity<string, any>>();
const entries = Object.entries(activities);
// Filter for null payloads
const filteredEntries = entries.filter(([_, payload]) => payload);
// Sort activities by timestamp using UUID v7's embedded timestamp
const activitiesSorted = entries.sort(([keyA], [keyB]) => {
const activitiesSorted = filteredEntries.sort(([keyA], [keyB]) => {
const timestampA = getTimestampFromUUID(keyA);
const timestampB = getTimestampFromUUID(keyB);
return Number(timestampA - timestampB);
+15
View File
@@ -145,4 +145,19 @@ export abstract class BaseWallet<
abstract addActivity<T>(activity: T): Promise<void>;
abstract export(): Promise<string>;
/**
* Get the ID for this wallet.
*/
abstract getId(): Promise<string>;
/**
* Get the encryption key to use for this wallet.
*/
abstract getEncryptionKey(): Promise<Uint8Array>;
/**
* Get the signing key to use for this wallet.
*/
abstract getSigningKey(): Promise<Uint8Array>;
}
+54 -18
View File
@@ -29,7 +29,7 @@ import {
encodeExtendedJson,
} from '@xocash/primitives';
import { hexToBin } from '@bitauth/libauth';
import { hash256, binToHex, hexToBin, utf8ToBin } from '@bitauth/libauth';
export const DERIVATION_PATH = "m/44'/145'/0'";
export const ADDRESS_GAP = 20;
@@ -91,23 +91,6 @@ export class WalletHD<
// Store child wallets, grouped by chain ("change") path in BIP44 spec.
public wallets: { [chainPath: number]: Array<WalletType> } = {};
constructor(
public blockchain: BaseBlockchain,
public store: BaseStore,
public cache: BaseStore,
public hdPrivateNode: HDPrivateNode,
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH,
) {
super();
// 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');
}
static async from<T extends typeof WalletHD>(
this: T,
services: WalletServices,
@@ -166,6 +149,46 @@ export class WalletHD<
) as InstanceType<T>;
}
constructor(
public blockchain: BaseBlockchain,
public store: BaseStore,
public cache: BaseStore,
public hdPrivateNode: HDPrivateNode,
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH,
) {
super();
// 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() {
// NOTE: Use the cache to optimize future calls.
return binToHex(
await this.cache.getOrSet('walletId', async () =>
this.deriveKey('walletId'),
),
);
}
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() {
// Get the genesis data.
const genesisData = await this.genesisData.getOrFail();
@@ -504,6 +527,19 @@ 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]));
}
}
//-----------------------------------------------------------------------------
+47 -16
View File
@@ -22,6 +22,7 @@ import {
import {
type Input,
binToHex,
hash256,
sha256,
utf8ToBin,
walletTemplateP2pkhNonHd,
@@ -54,19 +55,6 @@ export class WalletP2PKH extends BaseWallet<WalletP2PKHEvents> {
public isStarted = false;
public status: string | null = null;
constructor(
// Dependencies.
public blockchain: BaseBlockchain,
// Persistent Storage.
public store: BaseStore,
// Cache.
public cache: BaseStore,
// Properties.
public privateKey: PrivateKey,
) {
super();
}
static async from<T extends typeof WalletP2PKH>(
this: T,
services: WalletServices,
@@ -170,10 +158,40 @@ export class WalletP2PKH extends BaseWallet<WalletP2PKHEvents> {
});
}
async getId() {
const genesisData = await this.store.getOrFail('genesis');
constructor(
// Dependencies.
public blockchain: BaseBlockchain,
// Persistent Storage.
public store: BaseStore,
// Cache.
public cache: BaseStore,
// Properties.
public privateKey: PrivateKey,
) {
super();
}
return sha256.hash(utf8ToBin(JSON.stringify(genesisData)));
async getId() {
// NOTE: Use the cache to optimize future calls.
return binToHex(
await this.cache.getOrSet('walletId', async () =>
this.deriveKey('walletId'),
),
);
}
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() {
@@ -431,6 +449,19 @@ export class WalletP2PKH extends BaseWallet<WalletP2PKHEvents> {
// Store the data.
await this.store.set<T>(activityId, activity);
}
private 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]));
}
}
//-----------------------------------------------------------------------------