XO Wallet Integration Part IV

This commit is contained in:
Jim Hamill
2025-05-27 23:17:41 +00:00
parent 25b58853a3
commit 2bfeed8587
15 changed files with 249 additions and 123 deletions
+2
View File
@@ -4,11 +4,13 @@ import typedocSidebar from '../api/typedoc-sidebar.json';
// Special cases map for acronyms and exceptions
const SPECIAL_CASES = {
cryptocompare: 'CryptoCompare',
hd: 'HD',
http: 'HTTP',
indexeddb: 'IndexedDB',
inmemory: 'InMemory',
json: 'JSON',
localstorage: 'LocalStorage',
p2p: 'P2P',
p2pkh: 'P2PKH',
p2sh: 'P2SH',
@@ -1,5 +1,7 @@
import { BaseBlockchain } from './base-blockchain.js';
import {
type AddressStatusPayload,
type BlockHeightPayload,
type BlockchainCallback,
type BlockchainTransaction,
type BlockchainUTXO,
@@ -54,16 +56,6 @@ export type BlockchainElectrumParams = {
opts?: Partial<BlockchainElectrumOpts>;
};
export type AddressStatusPayload = {
type: 'address-status';
status: string | null;
};
export type BlockHeightPayload = {
type: 'block-height';
height: number | null;
};
export class BlockchainElectrum extends BaseBlockchain<
AddressStatusPayload,
BlockHeightPayload
+4 -1
View File
@@ -1,5 +1,8 @@
// NOTE: Do not export specific providers in this file.
// Do this in the parent index.ts file instead (otherwise, we mingle interfaces and providers, which can create circular dependencies)..
// The exception to this rule is with the InMemory and Composite providers.
// These are considered built-ins and do not have any other interface dependencies.
export * from './base-blockchain.js';
export * from './blockchain-electrum.js';
export * from './blockchain-inmemory.js';
export * from './traits.js';
export * from './types.js';
+11
View File
@@ -32,3 +32,14 @@ export type BlockchainUTXO = {
// NOTE: To minimize refactors later, use this name elsewhere; not the `ExtMap<Type>` form.
export type BlockchainUTXOs = ExtMap<BlockchainUTXO>;
// NOTE: These payloads are used by Electrum.
export type AddressStatusPayload = {
type: 'address-status';
status: string | null;
};
export type BlockHeightPayload = {
type: 'block-height';
height: number | null;
};
+13
View File
@@ -7,3 +7,16 @@ export * from './rates/index.js';
export * from './storage/index.js';
export * from './utils/index.js';
export * from './wallet/index.js';
// Export providers that work across both Browser and NodeJS.
// NOTE: For browser-only providers, export these from index.browser.ts.
// For node-only providers, export these from index.node.ts.
export * from './blockchain/blockchain-electrum.js';
export * from './storage/store-remote-http.js';
export * from './rates/rates-bitpay.js';
export * from './rates/rates-coinbase.js';
export * from './rates/rates-cryptocompare.js';
export * from './wallet/wallet-hd.js';
export * from './wallet/wallet-p2pkh.js';
export * from './wallet/wallet-p2sh20.js';
export * from './wallet/wallet-p2sh32.js';
+3
View File
@@ -1,3 +1,6 @@
// Export all of our stack-specific modules.
// NOTE: This file exists as an entry point for generating our documentation.
// In our documentation, we want to include both Browser and Node-specific providers.
// TODO: For clarity, consider whether we should rename this to index.docs.ts.
export * from './index.browser.js';
export * from './index.node.js';
+4 -3
View File
@@ -1,6 +1,7 @@
// NOTE: Do not export specific providers in this file.
// Do this in the parent index.ts file instead (otherwise, we mingle interfaces and providers, which can create circular dependencies)..
// The exception to this rule is with the InMemory and Composite providers.
// These are considered built-ins and do not have any other interface dependencies.
export * from './base-rates.js';
export * from './rates-bitpay.js';
export * from './rates-coinbase.js';
export * from './rates-cryptocompare.js';
export * from './rates-composite.js';
export * from './traits.js';
+85 -18
View File
@@ -23,18 +23,22 @@ export interface Activity {
}
// Define the snapshot structure
export type SnapshotByType = {
[type: string]: {
export type SnapshotState = {
[key: string]: {
[entityId: string]: Activity;
};
};
export type ActivitiesIndexedByKey<T extends Activity = Activity> = {
[entityId: string]: T;
export type SnapshotData<T extends Activity = Activity> = {
[keyOrEntityId: string]: T;
};
export type ActivitiesIndexedByEntityId<T extends Activity = Activity> = {
[key: string]: T;
export type SnapshotIndexedByKey<T extends Activity = Activity> = {
[key: string]: { [entityId: string]: T };
};
export type SnapshotIndexedByEntityId<T extends Activity = Activity> = {
[entityId: string]: { [key: string]: T };
};
// Policy type definition
@@ -45,6 +49,7 @@ export type PolicyFunction = (
export type ActivitiesEvents = {
snapshotUpdated: undefined;
updated: { activity: Activity };
[key: `updated:${string}`]: { activity: Activity };
};
@@ -79,9 +84,15 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
// TODO: If you can refactor this around your new Storage mechanism, things become a lot simpler:
// 1. You don't need to define such a clunky snapshot format because you have indexes.
// 2. Caching of snapshots likely becomes possible (because you can use a storage adapter).
public snapshot: SnapshotByType = {};
private policies = new Map<string, PolicyFunction>();
private storeKeyMap = new Map<string, string>();
public snapshot: SnapshotState = {};
// Indexes.
// TODO: We want to be able to remove these in favor of the TODO note above.
// public snapshotIndexedByEntity: SnapshotIndexedByEntityId = {};
// public snapshotIndexedByKey: SnapshotIndexedByKey = {};
protected policies = new Map<string, PolicyFunction>();
protected storeKeyMap = new Map<string, string>();
/**
* Create an Activities instance from the given composite store.
@@ -93,7 +104,7 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
return activities;
}
private constructor(store: StoreComposite) {
protected constructor(store: StoreComposite) {
super();
// Assign our dependencies.
@@ -167,6 +178,10 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
return removedActivity;
}
public getSnapshot() {
return this.snapshot;
}
/**
* Get a specific activity by type and id with type inference
*/
@@ -178,12 +193,23 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
return activity as T | undefined;
}
public getByEntity(entityId: string = '') {
return Object.keys(this.snapshot).reduce(
(result, key) => {
const activity = this.snapshot[key][entityId];
if (activity) {
result[key] = activity;
}
return result;
},
{} as Record<string, Activity>,
);
}
/**
* Get all activities of a specific type
*/
public getByKey<T extends Activity>(
key: T['key'],
): ActivitiesIndexedByKey<T> {
public getByKey<T extends Activity>(key: T['key']): SnapshotData<T> {
const activities = this.snapshot[key];
if (!activities) {
return {};
@@ -191,9 +217,25 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
// NOTE: We want to return a new object, not a reference to our current object.
// But we only need to "clone" the first level as each activity itself is immutable.
return { ...activities } as ActivitiesIndexedByKey<T>;
return { ...activities } as SnapshotData<T>;
}
/*
public getByEntityId<T extends Activity>(
entityId: string = '',
): SnapshotData<T> {
const activities = this.snapshotIndexedByEntity[entityId];
if (!activities) {
return {};
}
// NOTE: We want to return a new object, not a reference to our current object.
// But we only need to "clone" the first level as each activity itself is immutable.
return { ...activities } as SnapshotData<T>;
}
*/
/**
* Register a policy for handling conflicts between activities of the same type
* @param type Activity type to register policy for
@@ -239,6 +281,9 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
}
this.storeKeyMap.set(snapshotKey, storeKey);
// Emit the updated activity.
this.emit('updated', { activity });
// Emit the updated activity.
this.emit(`updated:${activity.key}`, { activity });
@@ -254,7 +299,7 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
* Compile a full snapshot from scratch
* This happens automatically on initialization
*/
private async compileSnapshot(): Promise<SnapshotByType> {
private async compileSnapshot(): Promise<SnapshotState> {
// Clear existing snapshot
this.snapshot = {};
this.storeKeyMap.clear();
@@ -297,11 +342,33 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
* Set an activity in the snapshot, creating necessary object structure
*/
private setActivityInSnapshot(activity: Activity): void {
if (!this.snapshot[activity.key]) {
this.snapshot[activity.key] = {};
// Assign the key for convenience.
const key = activity.key;
// Assign the entityId to the provided entityId - or to the root ("") entity if none provided.
const entityId = activity.entityId || '';
if (!this.snapshot[key]) {
this.snapshot[key] = {};
}
this.snapshot[activity.key][activity.entityId || ''] = activity;
this.snapshot[key][entityId] = activity;
/*
// Index the activity by entityId:key.
if (!this.snapshotIndexedByEntity[entityId]) {
this.snapshot[entityId] = {};
}
this.snapshotIndexedByEntity[entityId][key] = activity;
// Index the activity by key:entityId.
if (!this.snapshotIndexedByKey[key]) {
this.snapshot[key] = {};
}
this.snapshotIndexedByKey[key][entityId] = activity;
*/
}
/**
+3
View File
@@ -5,6 +5,9 @@ export type StorageOpts = {
encryption?: Encrypter;
};
// TODO: Do we extend from this at store construction or at `all()`?
export type StoreRecords = Record<string, unknown>;
export type StoreEvents = {
[key: `set:${string}`]: any;
set: { key: string; value: any };
+4 -1
View File
@@ -1,6 +1,9 @@
// NOTE: Do not export specific providers in this file.
// Do this in the parent index.ts file instead (otherwise, we mingle interfaces and providers, which can create circular dependencies)..
// The exception to this rule is with the InMemory and Composite providers.
// These are considered built-ins and do not have any other interface dependencies.
export * from './activities.js';
export * from './base-store.js';
export * from './store-composite.js';
export * from './store-inmemory.js';
export * from './store-inmemory-synced.js';
export * from './store-remote-http.js';
+55 -10
View File
@@ -11,7 +11,15 @@ import { type BlockchainUTXOs } from '../blockchain/index.js';
import { type Activities, type BaseStore } from '../storage/index.js';
import { EventEmitter } from '../utils/index.js';
import { type TokenBalances, type Address } from '@xocash/primitives';
import {
type TokenBalances,
type Address,
Bytes,
Hasher,
encodeExtendedJson,
} from '@xocash/primitives';
export type WalletGenesisData = Record<string, unknown>;
export type WalletEvents = {
shouldMonitorUpdated: boolean;
@@ -33,32 +41,69 @@ export type WalletEvents = {
export abstract class BaseWallet<
WalletEventsType extends WalletEvents = WalletEvents,
> extends EventEmitter<WalletEventsType> {
static readonly deriveKey: (keyName: string) => Promise<Uint8Array>;
/**
* Derive a key to use based on the given Genesis Data.
*
* @remarks
*
* These keys can be used for Wallet Id's, Signing Keys, Encryption Keys and for Dapp Samdboxes.
*/
static deriveKey(genesisData: WalletGenesisData, keyName: string) {
// Hash the genesis data using Hash256 and the key ("WalletID") for this derivation using Hash256.
const genesisDataHashed = Hasher.fromUTF8(
encodeExtendedJson(genesisData),
).toHash256Bytes();
const keyHashed = Hasher.fromUTF8(keyName).toHash256Bytes();
// Combine the hashes and do one final hash for good measure.
return Hasher.fromBytes(
new Uint8Array([...genesisDataHashed, ...keyHashed]),
).toHash256Bytes();
}
static deriveId(genesisData: WalletGenesisData) {
return BaseWallet.deriveKey(genesisData, 'walletId');
}
// Dependencies.
abstract readonly activities: Activities;
abstract readonly blockchain: WalletBlockchain;
abstract readonly cache: BaseStore;
// Genesis Data.
abstract readonly genesisData: WalletGenesisData;
/**
* Get the ID for this wallet.
*/
abstract getId(): Promise<string>;
/**
* Derive a key using secret data from this wallet.
*/
abstract deriveKey(keyName: string): Promise<Uint8Array>;
async getId() {
// NOTE: Use the cache to optimize future calls.
return Bytes.from(
await this.cache.getOrSet('walletId', async () =>
BaseWallet.deriveKey(this.genesisData, 'walletId'),
),
).toHex();
}
/**
* Get the encryption key to use for this wallet.
*/
abstract getEncryptionKey(): Promise<Uint8Array>;
async getEncryptionKey() {
// NOTE: Use the cache to optimize future calls.
return await this.cache.getOrSet('encryptionKey', async () =>
BaseWallet.deriveKey(this.genesisData, 'encryptionKey'),
);
}
/**
* Get the signing key to use for this wallet.
*/
abstract getSigningKey(): Promise<Uint8Array>;
async getSigningKey() {
// NOTE: Use the cache to optimize future calls.
return this.cache.getOrSet('signingKey', async () =>
BaseWallet.deriveKey(this.genesisData, 'signingKey'),
);
}
/**
* Start monitoring this wallet for any transaction activity.
+4 -4
View File
@@ -1,8 +1,8 @@
// NOTE: Do not export specific providers in this file.
// Do this in the parent index.ts file instead (otherwise, we mingle interfaces and providers, which can create circular dependencies)..
// The exception to this rule is with the InMemory and Composite providers.
// These are considered built-ins and do not have any other interface dependencies.
export * from './activities.js';
export * from './base-wallet.js';
export * from './transaction-builder.js';
export * from './types.js';
export * from './wallet-hd.js';
export * from './wallet-p2pkh.js';
export * from './wallet-p2sh20.js';
export * from './wallet-p2sh32.js';
+1 -38
View File
@@ -13,11 +13,8 @@ import {
Mnemonic,
calculateBalanceSats,
calculateBalanceTokens,
encodeExtendedJson,
} from '@xocash/primitives';
import { hash256, binToHex, hexToBin, utf8ToBin } from '@bitauth/libauth';
export const DERIVATION_PATH = "m/44'/145'/0'";
export const ADDRESS_GAP = 20;
@@ -112,7 +109,7 @@ export class WalletHD extends BaseWallet {
// Instantiate the HDPrivateNode from the type specified in our genesis data.
if ('seed' in genesisData) {
this.hdPrivateNode = HDPrivateNode.fromSeed(hexToBin(genesisData.seed));
this.hdPrivateNode = HDPrivateNode.fromSeed(genesisData.seed);
} else if ('mnemonic' in genesisData) {
this.hdPrivateNode = HDPrivateNode.fromMnemonic(
Mnemonic.fromPhrase(genesisData.mnemonic),
@@ -126,40 +123,6 @@ export class WalletHD extends BaseWallet {
this.wallets[CHAIN_INTERNAL] = [];
}
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(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 () =>
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 address gap.
// TODO: This should come from activities.
-36
View File
@@ -22,8 +22,6 @@ import {
import {
type Input,
binToHex,
hash256,
utf8ToBin,
walletTemplateP2pkhNonHd,
walletTemplateToCompilerBCH,
} from '@bitauth/libauth';
@@ -161,40 +159,6 @@ export class WalletP2PKH extends BaseWallet {
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) {