XO Wallet Integration Part IV
This commit is contained in:
+58
-2
@@ -35,6 +35,62 @@ A table of common prefixes (conventions) is below:
|
|||||||
| revert | Reverts a previous commit |
|
| revert | Reverts a previous commit |
|
||||||
|
|
||||||
|
|
||||||
|
## Coding Guidelines
|
||||||
|
|
||||||
|
These guidelines represent best practices. While exceptions may occasionally be necessary as "the lesser evil," strive to adhere to these principles whenever possible.
|
||||||
|
|
||||||
|
1. Try to minimize state
|
||||||
|
|
||||||
|
State is a complexity multiplier. Keep mutable class members to an absolute minimum, as each stateful property exponentially increases cognitive load and potential for bugs. When designing classes or components, first attempt a stateless approach, adding mutable properties only when demonstrably necessary.
|
||||||
|
|
||||||
|
2. Try to avoid `any` types
|
||||||
|
|
||||||
|
Use `unknown` instead, as it provides better type safety while maintaining flexibility when needed.
|
||||||
|
|
||||||
|
3. Prefer immutable patterns
|
||||||
|
|
||||||
|
Use immutable data structures where possible to improve code predictability and testability.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// GOOD:
|
||||||
|
const mappedData = originalData.map((item) => item.field);
|
||||||
|
|
||||||
|
// AVOID:
|
||||||
|
const mappedData = [];
|
||||||
|
|
||||||
|
for(let i = 0; i < originalData.length; i++) {
|
||||||
|
mappedData.push(originalData[i].field);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do note that there are some cases where it may make more sense to mutate data inline (e.g. as an optimization for very large data structures).
|
||||||
|
|
||||||
|
Use your best judgement, but treat immutability as the general rule.
|
||||||
|
|
||||||
|
4. Avoid Typescript, Linter and Formatter Directives
|
||||||
|
|
||||||
|
These exceptional cases should be minimized as they:
|
||||||
|
- Create inconsistency in code standards
|
||||||
|
- Make maintenance more difficult
|
||||||
|
- Often mask underlying issues that should be properly resolved
|
||||||
|
- Can lead to unintended side-effects throughout the codebase
|
||||||
|
|
||||||
|
4. Avoid additional dependencies.
|
||||||
|
|
||||||
|
Try to avoid pulling in additional packages. Not only can this bloat the project, but can also cause compatibility issues between environments (Web, Node, ESM, etc). We want `@xocash/stack` to "just work" without devs having to tailor build configurations.
|
||||||
|
|
||||||
|
Obviously this is an ideal: Sometimes it is simply not feasible or sensible to rewrite complex functionality that can be better done by a well-built package. However, please try to adhere to the following:
|
||||||
|
|
||||||
|
- Ensure the package is of high quality (well-written, well-tested).
|
||||||
|
- Does not drag in additional dependencies itself (note that `devDependencies` are fine. These will not be installed when leveraged from our packages).
|
||||||
|
- If the package being used **is heavy** and we have no other choice but to use it, consider whether we should package it separately to avoid problems. For example, if we build a `BlockchainBCHN` adapter, we will likely want to package this on its own under `@xocash/blockchain-bchn` as it only works in NodeJS environments.
|
||||||
|
|
||||||
|
5. "When in Rome..."
|
||||||
|
|
||||||
|
For conventions not explicitly mentioned, follow the "when in Rome" principle. Try to identify and use the patterns and conventions that are already established elsewhere in the codebase.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Dependency Injection
|
### Dependency Injection
|
||||||
@@ -77,7 +133,7 @@ class SomeClass {
|
|||||||
|
|
||||||
### Factory Patterns
|
### Factory Patterns
|
||||||
|
|
||||||
When creating classes, create static `from(...)` (or `fromSpecificThing(...)`) methods.
|
When creating classes, create static `from(...)` (or `fromSpecificThing(...)`) methods and place these at the top of the class (so that they appear first in the generated documentation).
|
||||||
|
|
||||||
In Javascript, a `constructor` cannot be asynchronous and we want to ensure that we can `await` class instantiation to mitigate against stateful errors.
|
In Javascript, a `constructor` cannot be asynchronous and we want to ensure that we can `await` class instantiation to mitigate against stateful errors.
|
||||||
|
|
||||||
@@ -223,7 +279,7 @@ Avoid revalidation at every code-path: This is not only inefficient, but a giant
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
// You can accept other types (this is a nice convenience).
|
// You can accept other types (this is a nice convenience).
|
||||||
broadcastTransaction(transaction: Transaction | Uint8Array): Transaction {
|
broadcastTransaction(transaction: Transaction | Uint8Array | string): Transaction {
|
||||||
// ... but always cast to the Primitive (to ensure correctness).
|
// ... but always cast to the Primitive (to ensure correctness).
|
||||||
if(!(transaction instanceof Transaction) {
|
if(!(transaction instanceof Transaction) {
|
||||||
transaction = Transaction.from(transaction);
|
transaction = Transaction.from(transaction);
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import typedocSidebar from '../api/typedoc-sidebar.json';
|
|||||||
|
|
||||||
// Special cases map for acronyms and exceptions
|
// Special cases map for acronyms and exceptions
|
||||||
const SPECIAL_CASES = {
|
const SPECIAL_CASES = {
|
||||||
|
cryptocompare: 'CryptoCompare',
|
||||||
hd: 'HD',
|
hd: 'HD',
|
||||||
http: 'HTTP',
|
http: 'HTTP',
|
||||||
indexeddb: 'IndexedDB',
|
indexeddb: 'IndexedDB',
|
||||||
inmemory: 'InMemory',
|
inmemory: 'InMemory',
|
||||||
json: 'JSON',
|
json: 'JSON',
|
||||||
|
localstorage: 'LocalStorage',
|
||||||
p2p: 'P2P',
|
p2p: 'P2P',
|
||||||
p2pkh: 'P2PKH',
|
p2pkh: 'P2PKH',
|
||||||
p2sh: 'P2SH',
|
p2sh: 'P2SH',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BaseBlockchain } from './base-blockchain.js';
|
import { BaseBlockchain } from './base-blockchain.js';
|
||||||
import {
|
import {
|
||||||
|
type AddressStatusPayload,
|
||||||
|
type BlockHeightPayload,
|
||||||
type BlockchainCallback,
|
type BlockchainCallback,
|
||||||
type BlockchainTransaction,
|
type BlockchainTransaction,
|
||||||
type BlockchainUTXO,
|
type BlockchainUTXO,
|
||||||
@@ -54,16 +56,6 @@ export type BlockchainElectrumParams = {
|
|||||||
opts?: Partial<BlockchainElectrumOpts>;
|
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<
|
export class BlockchainElectrum extends BaseBlockchain<
|
||||||
AddressStatusPayload,
|
AddressStatusPayload,
|
||||||
BlockHeightPayload
|
BlockHeightPayload
|
||||||
|
|||||||
@@ -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 './base-blockchain.js';
|
||||||
export * from './blockchain-electrum.js';
|
|
||||||
export * from './blockchain-inmemory.js';
|
export * from './blockchain-inmemory.js';
|
||||||
export * from './traits.js';
|
export * from './traits.js';
|
||||||
export * from './types.js';
|
export * from './types.js';
|
||||||
|
|||||||
@@ -32,3 +32,14 @@ export type BlockchainUTXO = {
|
|||||||
|
|
||||||
// NOTE: To minimize refactors later, use this name elsewhere; not the `ExtMap<Type>` form.
|
// NOTE: To minimize refactors later, use this name elsewhere; not the `ExtMap<Type>` form.
|
||||||
export type BlockchainUTXOs = ExtMap<BlockchainUTXO>;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,3 +7,16 @@ export * from './rates/index.js';
|
|||||||
export * from './storage/index.js';
|
export * from './storage/index.js';
|
||||||
export * from './utils/index.js';
|
export * from './utils/index.js';
|
||||||
export * from './wallet/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';
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
// Export all of our stack-specific modules.
|
// 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.browser.js';
|
||||||
export * from './index.node.js';
|
export * from './index.node.js';
|
||||||
|
|||||||
@@ -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 './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 './rates-composite.js';
|
||||||
export * from './traits.js';
|
export * from './traits.js';
|
||||||
|
|||||||
@@ -23,18 +23,22 @@ export interface Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define the snapshot structure
|
// Define the snapshot structure
|
||||||
export type SnapshotByType = {
|
export type SnapshotState = {
|
||||||
[type: string]: {
|
[key: string]: {
|
||||||
[entityId: string]: Activity;
|
[entityId: string]: Activity;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActivitiesIndexedByKey<T extends Activity = Activity> = {
|
export type SnapshotData<T extends Activity = Activity> = {
|
||||||
[entityId: string]: T;
|
[keyOrEntityId: string]: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActivitiesIndexedByEntityId<T extends Activity = Activity> = {
|
export type SnapshotIndexedByKey<T extends Activity = Activity> = {
|
||||||
[key: string]: T;
|
[key: string]: { [entityId: string]: T };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SnapshotIndexedByEntityId<T extends Activity = Activity> = {
|
||||||
|
[entityId: string]: { [key: string]: T };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Policy type definition
|
// Policy type definition
|
||||||
@@ -45,6 +49,7 @@ export type PolicyFunction = (
|
|||||||
|
|
||||||
export type ActivitiesEvents = {
|
export type ActivitiesEvents = {
|
||||||
snapshotUpdated: undefined;
|
snapshotUpdated: undefined;
|
||||||
|
updated: { activity: Activity };
|
||||||
[key: `updated:${string}`]: { 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:
|
// 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.
|
// 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).
|
// 2. Caching of snapshots likely becomes possible (because you can use a storage adapter).
|
||||||
public snapshot: SnapshotByType = {};
|
public snapshot: SnapshotState = {};
|
||||||
private policies = new Map<string, PolicyFunction>();
|
|
||||||
private storeKeyMap = new Map<string, string>();
|
// 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.
|
* Create an Activities instance from the given composite store.
|
||||||
@@ -93,7 +104,7 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
|
|||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(store: StoreComposite) {
|
protected constructor(store: StoreComposite) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// Assign our dependencies.
|
// Assign our dependencies.
|
||||||
@@ -167,6 +178,10 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
|
|||||||
return removedActivity;
|
return removedActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSnapshot() {
|
||||||
|
return this.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific activity by type and id with type inference
|
* 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;
|
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
|
* Get all activities of a specific type
|
||||||
*/
|
*/
|
||||||
public getByKey<T extends Activity>(
|
public getByKey<T extends Activity>(key: T['key']): SnapshotData<T> {
|
||||||
key: T['key'],
|
|
||||||
): ActivitiesIndexedByKey<T> {
|
|
||||||
const activities = this.snapshot[key];
|
const activities = this.snapshot[key];
|
||||||
if (!activities) {
|
if (!activities) {
|
||||||
return {};
|
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.
|
// 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.
|
// 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
|
* Register a policy for handling conflicts between activities of the same type
|
||||||
* @param type Activity type to register policy for
|
* @param type Activity type to register policy for
|
||||||
@@ -239,6 +281,9 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
|
|||||||
}
|
}
|
||||||
this.storeKeyMap.set(snapshotKey, storeKey);
|
this.storeKeyMap.set(snapshotKey, storeKey);
|
||||||
|
|
||||||
|
// Emit the updated activity.
|
||||||
|
this.emit('updated', { activity });
|
||||||
|
|
||||||
// Emit the updated activity.
|
// Emit the updated activity.
|
||||||
this.emit(`updated:${activity.key}`, { activity });
|
this.emit(`updated:${activity.key}`, { activity });
|
||||||
|
|
||||||
@@ -254,7 +299,7 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
|
|||||||
* Compile a full snapshot from scratch
|
* Compile a full snapshot from scratch
|
||||||
* This happens automatically on initialization
|
* This happens automatically on initialization
|
||||||
*/
|
*/
|
||||||
private async compileSnapshot(): Promise<SnapshotByType> {
|
private async compileSnapshot(): Promise<SnapshotState> {
|
||||||
// Clear existing snapshot
|
// Clear existing snapshot
|
||||||
this.snapshot = {};
|
this.snapshot = {};
|
||||||
this.storeKeyMap.clear();
|
this.storeKeyMap.clear();
|
||||||
@@ -297,11 +342,33 @@ export class Activities extends EventEmitter<ActivitiesEvents> {
|
|||||||
* Set an activity in the snapshot, creating necessary object structure
|
* Set an activity in the snapshot, creating necessary object structure
|
||||||
*/
|
*/
|
||||||
private setActivityInSnapshot(activity: Activity): void {
|
private setActivityInSnapshot(activity: Activity): void {
|
||||||
if (!this.snapshot[activity.key]) {
|
// Assign the key for convenience.
|
||||||
this.snapshot[activity.key] = {};
|
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;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export type StorageOpts = {
|
|||||||
encryption?: Encrypter;
|
encryption?: Encrypter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Do we extend from this at store construction or at `all()`?
|
||||||
|
export type StoreRecords = Record<string, unknown>;
|
||||||
|
|
||||||
export type StoreEvents = {
|
export type StoreEvents = {
|
||||||
[key: `set:${string}`]: any;
|
[key: `set:${string}`]: any;
|
||||||
set: { key: string; value: any };
|
set: { key: string; value: any };
|
||||||
|
|||||||
@@ -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 './activities.js';
|
||||||
export * from './base-store.js';
|
export * from './base-store.js';
|
||||||
export * from './store-composite.js';
|
export * from './store-composite.js';
|
||||||
export * from './store-inmemory.js';
|
export * from './store-inmemory.js';
|
||||||
export * from './store-inmemory-synced.js';
|
export * from './store-inmemory-synced.js';
|
||||||
export * from './store-remote-http.js';
|
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ import { type BlockchainUTXOs } from '../blockchain/index.js';
|
|||||||
import { type Activities, type BaseStore } from '../storage/index.js';
|
import { type Activities, type BaseStore } from '../storage/index.js';
|
||||||
import { EventEmitter } from '../utils/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 = {
|
export type WalletEvents = {
|
||||||
shouldMonitorUpdated: boolean;
|
shouldMonitorUpdated: boolean;
|
||||||
@@ -33,32 +41,69 @@ export type WalletEvents = {
|
|||||||
export abstract class BaseWallet<
|
export abstract class BaseWallet<
|
||||||
WalletEventsType extends WalletEvents = WalletEvents,
|
WalletEventsType extends WalletEvents = WalletEvents,
|
||||||
> extends EventEmitter<WalletEventsType> {
|
> 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.
|
// Dependencies.
|
||||||
abstract readonly activities: Activities;
|
abstract readonly activities: Activities;
|
||||||
abstract readonly blockchain: WalletBlockchain;
|
abstract readonly blockchain: WalletBlockchain;
|
||||||
abstract readonly cache: BaseStore;
|
abstract readonly cache: BaseStore;
|
||||||
|
|
||||||
|
// Genesis Data.
|
||||||
|
abstract readonly genesisData: WalletGenesisData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID for this wallet.
|
* Get the ID for this wallet.
|
||||||
*/
|
*/
|
||||||
abstract getId(): Promise<string>;
|
async getId() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
/**
|
return Bytes.from(
|
||||||
* Derive a key using secret data from this wallet.
|
await this.cache.getOrSet('walletId', async () =>
|
||||||
*/
|
BaseWallet.deriveKey(this.genesisData, 'walletId'),
|
||||||
abstract deriveKey(keyName: string): Promise<Uint8Array>;
|
),
|
||||||
|
).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the encryption key to use for this wallet.
|
* 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.
|
* 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.
|
* Start monitoring this wallet for any transaction activity.
|
||||||
|
|||||||
@@ -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 './activities.js';
|
||||||
export * from './base-wallet.js';
|
export * from './base-wallet.js';
|
||||||
export * from './transaction-builder.js';
|
export * from './transaction-builder.js';
|
||||||
export * from './types.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';
|
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ import {
|
|||||||
Mnemonic,
|
Mnemonic,
|
||||||
calculateBalanceSats,
|
calculateBalanceSats,
|
||||||
calculateBalanceTokens,
|
calculateBalanceTokens,
|
||||||
encodeExtendedJson,
|
|
||||||
} from '@xocash/primitives';
|
} from '@xocash/primitives';
|
||||||
|
|
||||||
import { hash256, binToHex, hexToBin, utf8ToBin } from '@bitauth/libauth';
|
|
||||||
|
|
||||||
export const DERIVATION_PATH = "m/44'/145'/0'";
|
export const DERIVATION_PATH = "m/44'/145'/0'";
|
||||||
export const ADDRESS_GAP = 20;
|
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.
|
// Instantiate the HDPrivateNode from the type specified in our genesis data.
|
||||||
if ('seed' in genesisData) {
|
if ('seed' in genesisData) {
|
||||||
this.hdPrivateNode = HDPrivateNode.fromSeed(hexToBin(genesisData.seed));
|
this.hdPrivateNode = HDPrivateNode.fromSeed(genesisData.seed);
|
||||||
} else if ('mnemonic' in genesisData) {
|
} else if ('mnemonic' in genesisData) {
|
||||||
this.hdPrivateNode = HDPrivateNode.fromMnemonic(
|
this.hdPrivateNode = HDPrivateNode.fromMnemonic(
|
||||||
Mnemonic.fromPhrase(genesisData.mnemonic),
|
Mnemonic.fromPhrase(genesisData.mnemonic),
|
||||||
@@ -126,40 +123,6 @@ export class WalletHD extends BaseWallet {
|
|||||||
this.wallets[CHAIN_INTERNAL] = [];
|
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() {
|
async start() {
|
||||||
// Get the address gap.
|
// Get the address gap.
|
||||||
// TODO: This should come from activities.
|
// TODO: This should come from activities.
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
type Input,
|
type Input,
|
||||||
binToHex,
|
binToHex,
|
||||||
hash256,
|
|
||||||
utf8ToBin,
|
|
||||||
walletTemplateP2pkhNonHd,
|
walletTemplateP2pkhNonHd,
|
||||||
walletTemplateToCompilerBCH,
|
walletTemplateToCompilerBCH,
|
||||||
} from '@bitauth/libauth';
|
} from '@bitauth/libauth';
|
||||||
@@ -161,40 +159,6 @@ export class WalletP2PKH extends BaseWallet {
|
|||||||
this.privateKey = PrivateKey.fromHex(genesisData.privateKey);
|
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() {
|
async start() {
|
||||||
// Do nothing if already started.
|
// Do nothing if already started.
|
||||||
if (this.isStarted) {
|
if (this.isStarted) {
|
||||||
|
|||||||
Reference in New Issue
Block a user