Doc commit tag conventions
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user