XO wallet integration
This commit is contained in:
@@ -2,10 +2,11 @@ import { Transaction } from './transaction.js';
|
||||
|
||||
import { binToHex } from '@bitauth/libauth';
|
||||
|
||||
// TODO: The below should probably be moved and renamed.
|
||||
/**
|
||||
* Sort transactions by the order in which they should be broadcast.
|
||||
*/
|
||||
export function sortTransactions(
|
||||
export function sortTransactionsByDependencies(
|
||||
transactions: Array<Transaction>,
|
||||
): Array<Transaction> {
|
||||
const transactionMap = transactions.reduce(
|
||||
@@ -139,6 +140,24 @@ export class ExtMap<T> extends Map<PropertyKey, T> {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ExtMap containing a subset of entries from the original Map
|
||||
* @param start - The starting index (inclusive)
|
||||
* @param end - The ending index (exclusive)
|
||||
* @returns A new ExtMap with the sliced entries
|
||||
*/
|
||||
slice(start: number, end?: number): ExtMap<T> {
|
||||
const keys = Array.from(this.keys());
|
||||
const slicedKeys = keys.slice(start, end);
|
||||
|
||||
const result = new ExtMap<T>();
|
||||
for (const key of slicedKeys) {
|
||||
result.set(key, this.get(key) as T);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the collection to a plain object
|
||||
*/
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './blockchain-electrum.js';
|
||||
export * from './blockchain-inmemory.js';
|
||||
export * from './traits.js';
|
||||
export * from './types.js';
|
||||
export * from './utils.js';
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// TODO: If we end up with several utilities, we might want to consider a refactor:
|
||||
// 1. Convert BlockchainTransactions into a class with a sort function.
|
||||
// 2. Have WalletTransactions extends from BlockchainTransactions.
|
||||
|
||||
import { type BlockchainTransactions } from './types.js';
|
||||
|
||||
export function sortTransactions(
|
||||
transactions: BlockchainTransactions,
|
||||
order: 'ASC' | 'DESC',
|
||||
): BlockchainTransactions {
|
||||
return transactions.sort((a, b) => {
|
||||
// Name the outcomes, or I'll go insane.
|
||||
const aFirst = order === 'DESC' ? -1 : 1;
|
||||
const bFirst = order === 'DESC' ? 1 : -1;
|
||||
|
||||
// Sort by mempool, then blockheight, then dependency.
|
||||
// height = 0 -- mempool
|
||||
// height > 0 -- confirmed
|
||||
// height < 0 -- mempool with dependencies
|
||||
|
||||
// If a is dependant on a tx in the mempool, while b is in the mempool or confirmed, sort a before b.
|
||||
if (a.height <= -1 && b.height >= 0) {
|
||||
return aFirst;
|
||||
}
|
||||
|
||||
// If b is dependant on a tx in the mempool, while a is in the mempool or confirmed, sort b before a.
|
||||
if (b.height <= -1 && a.height >= 0) {
|
||||
return bFirst;
|
||||
}
|
||||
|
||||
// If a is in the mempool, while b is confirmed, sort a before b.
|
||||
if (a.height <= 0 && b.height > 0) {
|
||||
return aFirst;
|
||||
}
|
||||
|
||||
// If b is in the mempool, while a is confirmed, sort b before a.
|
||||
if (b.height <= 0 && a.height > 0) {
|
||||
return bFirst;
|
||||
}
|
||||
|
||||
// If a has a higher blockheight than b, sort a before b.
|
||||
if (a.height > b.height) {
|
||||
return aFirst;
|
||||
}
|
||||
|
||||
// If b has a higher blockheight than a, sort b before a.
|
||||
if (b.height > a.height) {
|
||||
return bFirst;
|
||||
}
|
||||
|
||||
// There was no criteria to sort by, so return 0.
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './index.js';
|
||||
|
||||
// These exports will only work in a browser environment.
|
||||
export * from './storage/store-indexeddb.js';
|
||||
export * from './storage/store-localstorage.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseStore } from './base-store.js';
|
||||
import { StoreComposite } from './store-composite.js';
|
||||
|
||||
import { UUIDV7 } from '../utils/index.js';
|
||||
import { EventEmitter, UUIDV7 } from '../utils/index.js';
|
||||
|
||||
// Make the given property in a type optional.
|
||||
// TODO: This is an extremely useful utility. If we use it elsewhere, move to own "type-utils.ts" file.
|
||||
@@ -9,10 +9,17 @@ type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
// Base Activity interface
|
||||
export interface Activity {
|
||||
entityId: string;
|
||||
name: string;
|
||||
/** Key (Type) of this activity. */
|
||||
key: string;
|
||||
|
||||
/** Value for this activity. */
|
||||
value: unknown;
|
||||
|
||||
/** Timestamp for this activity. */
|
||||
timestamp: bigint;
|
||||
|
||||
/** Entity ID may refer to a transaction id or other specific identifier. */
|
||||
entityId?: string;
|
||||
}
|
||||
|
||||
// Define the snapshot structure
|
||||
@@ -28,6 +35,10 @@ export type PolicyFunction = (
|
||||
newActivity: Activity,
|
||||
) => Activity;
|
||||
|
||||
export type ActivitiesEvents = {
|
||||
[key: `updated:${string}`]: { activity: Activity };
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an event sourcing pattern for managing application state
|
||||
* with a simplified interface focused on the current snapshot.
|
||||
@@ -51,7 +62,14 @@ export type PolicyFunction = (
|
||||
* const savedActivity = await activities.add(walletName);
|
||||
* ```
|
||||
*/
|
||||
export class Activities {
|
||||
export class Activities extends EventEmitter<ActivitiesEvents> {
|
||||
// Dependencies.
|
||||
public readonly store: StoreComposite;
|
||||
|
||||
// State.
|
||||
// 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>();
|
||||
@@ -59,27 +77,20 @@ export class Activities {
|
||||
/**
|
||||
* Create an Activities instance from the given composite store.
|
||||
*/
|
||||
static async from(compositeStore: StoreComposite) {
|
||||
static async from(stores?: Array<BaseStore>) {
|
||||
const compositeStore = await StoreComposite.from(stores);
|
||||
const activities = new Activities(compositeStore);
|
||||
await activities.compileSnapshot();
|
||||
return activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Activities instance from the given primary and secondary stores
|
||||
*/
|
||||
static async fromStores(
|
||||
primaryStore: BaseStore,
|
||||
secondaryStores?: Array<BaseStore>,
|
||||
): Promise<Activities> {
|
||||
const compositeStore = await StoreComposite.from(
|
||||
primaryStore,
|
||||
secondaryStores,
|
||||
);
|
||||
return Activities.from(compositeStore);
|
||||
}
|
||||
private constructor(store: StoreComposite) {
|
||||
super();
|
||||
|
||||
private constructor(public store: StoreComposite) {
|
||||
// Assign our dependencies.
|
||||
this.store = store;
|
||||
|
||||
// Register callbacks.
|
||||
this.store.on(
|
||||
'set',
|
||||
async (payload: { key: string; value: Activity | null }) => {
|
||||
@@ -90,47 +101,78 @@ export class Activities {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Additional argument to "add": Something like a quarum.
|
||||
// But, instead of a quorum, it's a function that takes the resolve of all "set" promises.
|
||||
// That way, we can implement custom storage policies?
|
||||
|
||||
/**
|
||||
* Add a new activity to the store
|
||||
* @param activity The activity to add (id and timestamp are optional)
|
||||
* @returns The stored activity with all fields populated
|
||||
*/
|
||||
public async add<T extends Optional<Activity, 'timestamp'>>(
|
||||
activity: T,
|
||||
public async add<T extends Activity>(
|
||||
activity: Optional<T, 'entityId' | 'timestamp'>,
|
||||
): Promise<Activity> {
|
||||
// Generate timestamp if not provided
|
||||
const timestamp = activity.timestamp ?? BigInt(Date.now());
|
||||
|
||||
// Generate a UUID based on the timestamp
|
||||
const key = UUIDV7.fromTimestamp(timestamp).toString();
|
||||
const uuid = UUIDV7.fromTimestamp(timestamp).toString();
|
||||
|
||||
// Generate an ID if not provided
|
||||
const id = activity.entityId ?? key;
|
||||
// If this doesn't reference a specific entity id, put it at root level (empty string: "")
|
||||
const id = activity.entityId ?? '';
|
||||
|
||||
const completeActivity: Activity = {
|
||||
name: activity.name,
|
||||
key: activity.key,
|
||||
value: activity.value,
|
||||
entityId: id,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
await this.store.set(key, completeActivity);
|
||||
await this.store.set(uuid, completeActivity);
|
||||
|
||||
return completeActivity;
|
||||
}
|
||||
|
||||
public async remove<T extends Activity>(
|
||||
activity: Pick<T, 'key' | 'entityId'>,
|
||||
) {
|
||||
// Generate timestamp.
|
||||
const timestamp = BigInt(Date.now());
|
||||
|
||||
// Generate a UUID based on the timestamp
|
||||
const uuid = UUIDV7.fromTimestamp(timestamp).toString();
|
||||
|
||||
// If this doesn't reference a specific entity id, put it at root level (empty string: "")
|
||||
const id = activity.entityId ?? '';
|
||||
|
||||
const removedActivity: Activity = {
|
||||
key: activity.key,
|
||||
value: null,
|
||||
entityId: id,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
await this.store.set(uuid, removedActivity);
|
||||
|
||||
return removedActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific activity by type and id with type inference
|
||||
*/
|
||||
public get<T extends Activity>(type: T['name'], id: string): T | undefined {
|
||||
const activity = this.snapshot[type]?.[id];
|
||||
public get<T extends Activity>(
|
||||
type: T['key'],
|
||||
entityId: string = '',
|
||||
): T | undefined {
|
||||
const activity = this.snapshot[type]?.[entityId];
|
||||
return activity as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all activities of a specific type
|
||||
*/
|
||||
public getByType<T extends Activity>(type: T['name']): T[] {
|
||||
public getByType<T extends Activity>(type: T['key']): T[] {
|
||||
const activities = this.snapshot[type];
|
||||
if (!activities) {
|
||||
return [];
|
||||
@@ -157,20 +199,14 @@ export class Activities {
|
||||
if (!activity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure timestamp exists by extracting from UUID if needed
|
||||
if (!activity.timestamp) {
|
||||
activity.timestamp = UUIDV7.from(storeKey).getTimestamp();
|
||||
}
|
||||
|
||||
// Ensure ID exists
|
||||
if (!activity.entityId) {
|
||||
activity.entityId = storeKey;
|
||||
}
|
||||
|
||||
const snapshotKey = this.getSnapshotKey(activity);
|
||||
const existingActivity = this.getActivityFromSnapshot(
|
||||
activity.name,
|
||||
activity.key,
|
||||
activity.entityId,
|
||||
);
|
||||
|
||||
@@ -185,12 +221,15 @@ export class Activities {
|
||||
const oldStoreKey = this.storeKeyMap.get(snapshotKey);
|
||||
if (oldStoreKey && oldStoreKey !== storeKey) {
|
||||
// Delete the old activity from the store
|
||||
await this.store.set(oldStoreKey, null);
|
||||
// await this.store.set(oldStoreKey, null);
|
||||
}
|
||||
this.storeKeyMap.set(snapshotKey, storeKey);
|
||||
|
||||
// Emit the updated activity.
|
||||
this.emit(`updated:${activity.key}`, { activity });
|
||||
} else if (storeKey !== this.storeKeyMap.get(snapshotKey)) {
|
||||
// If we kept the existing activity, delete the new activity from the store
|
||||
await this.store.set(storeKey, null);
|
||||
// await this.store.set(storeKey, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,27 +269,27 @@ export class Activities {
|
||||
*/
|
||||
private getActivityFromSnapshot(
|
||||
type: string,
|
||||
id: string,
|
||||
entityId: string = '',
|
||||
): Activity | undefined {
|
||||
return this.snapshot[type]?.[id];
|
||||
return this.snapshot[type]?.[entityId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an activity in the snapshot, creating necessary object structure
|
||||
*/
|
||||
private setActivityInSnapshot(activity: Activity): void {
|
||||
if (!this.snapshot[activity.name]) {
|
||||
this.snapshot[activity.name] = {};
|
||||
if (!this.snapshot[activity.key]) {
|
||||
this.snapshot[activity.key] = {};
|
||||
}
|
||||
|
||||
this.snapshot[activity.name][activity.entityId] = activity;
|
||||
this.snapshot[activity.key][activity.entityId || ''] = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the snapshot key for an activity
|
||||
*/
|
||||
private getSnapshotKey(activity: Activity): string {
|
||||
return `${activity.name}:${activity.entityId}`;
|
||||
return `${activity.key}:${activity.entityId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,7 +299,7 @@ export class Activities {
|
||||
existingActivity: Activity | undefined,
|
||||
newActivity: Activity,
|
||||
): Activity {
|
||||
const policy = this.policies.get(newActivity.name);
|
||||
const policy = this.policies.get(newActivity.key);
|
||||
|
||||
if (!policy) {
|
||||
// Default policy: compare timestamps
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseStore } from './base-store.js';
|
||||
import { StoreInMemory } from './store-inmemory.js';
|
||||
|
||||
/**
|
||||
* A store that accepts multiple stores and syncs them by key.
|
||||
@@ -12,127 +13,128 @@ import { BaseStore } from './base-store.js';
|
||||
*/
|
||||
export class StoreComposite extends BaseStore {
|
||||
static async from(
|
||||
localStore: BaseStore,
|
||||
remoteStores: Array<BaseStore> = [],
|
||||
stores: Array<BaseStore> = [],
|
||||
primaryStore: BaseStore = StoreInMemory.from(),
|
||||
) {
|
||||
const compositeStore = new this(localStore, remoteStores);
|
||||
const compositeStore = new this(stores, primaryStore);
|
||||
await compositeStore.sync();
|
||||
return compositeStore;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private localStore: BaseStore,
|
||||
private remoteStores: Array<BaseStore> = [],
|
||||
private stores: Array<BaseStore> = [],
|
||||
private primaryStore: BaseStore,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async all<T>() {
|
||||
return await this.localStore.all<T>();
|
||||
return await this.primaryStore.all<T>();
|
||||
}
|
||||
|
||||
async keys() {
|
||||
return await this.localStore.keys();
|
||||
return await this.primaryStore.keys();
|
||||
}
|
||||
|
||||
async get<T>(key: string) {
|
||||
return await this.localStore.get<T>(key);
|
||||
return await this.primaryStore.get<T>(key);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T) {
|
||||
await this.localStore.set(key, value);
|
||||
await this.primaryStore.set(key, value);
|
||||
this.emit('set', { key, value });
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
await this.localStore.delete(key);
|
||||
await this.primaryStore.delete(key);
|
||||
this.emit('delete', { key });
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.localStore.clear();
|
||||
await this.primaryStore.clear();
|
||||
this.emit('clear', undefined);
|
||||
}
|
||||
|
||||
async has(key: string) {
|
||||
return await this.localStore.has(key);
|
||||
return await this.primaryStore.has(key);
|
||||
}
|
||||
|
||||
async close() {
|
||||
await Promise.all([
|
||||
this.localStore.close(),
|
||||
...this.remoteStores.map((store) => store.close()),
|
||||
this.primaryStore.close(),
|
||||
...this.stores.map((store) => store.close()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new remote store and set up synchronization
|
||||
*/
|
||||
public async addRemoteStore(remoteStore: BaseStore): Promise<void> {
|
||||
this.remoteStores.push(remoteStore);
|
||||
async addStore(remoteStore: BaseStore): Promise<void> {
|
||||
this.stores.push(remoteStore);
|
||||
|
||||
// Propagate existing local events to the new remote
|
||||
this.localStore.on('set', async (payload) => {
|
||||
this.primaryStore.on('set', async (payload) => {
|
||||
if (await remoteStore.has(payload.key)) return;
|
||||
await remoteStore.set(payload.key, payload.value);
|
||||
});
|
||||
|
||||
this.localStore.on('delete', async (payload) => {
|
||||
await remoteStore.delete(payload.key);
|
||||
this.primaryStore.on('delete', async (payload) => {
|
||||
if (await remoteStore.has(payload.key)) {
|
||||
await remoteStore.delete(payload.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.localStore.on('clear', async () => {
|
||||
this.primaryStore.on('clear', async () => {
|
||||
await remoteStore.clear();
|
||||
});
|
||||
|
||||
// Set up remote to local propagation
|
||||
remoteStore.on('set', async (payload) => {
|
||||
if (await this.localStore.has(payload.key)) return;
|
||||
await this.localStore.set(payload.key, payload.value);
|
||||
if (await this.primaryStore.has(payload.key)) return;
|
||||
await this.primaryStore.set(payload.key, payload.value);
|
||||
});
|
||||
|
||||
remoteStore.on('delete', async (payload) => {
|
||||
if (await this.localStore.has(payload.key)) {
|
||||
await this.localStore.delete(payload.key);
|
||||
if (await this.primaryStore.has(payload.key)) {
|
||||
await this.primaryStore.delete(payload.key);
|
||||
}
|
||||
});
|
||||
|
||||
remoteStore.on('clear', async () => {
|
||||
await this.localStore.clear();
|
||||
await this.primaryStore.clear();
|
||||
});
|
||||
|
||||
// Sync with the new remote
|
||||
await this.syncWithRemote(remoteStore);
|
||||
}
|
||||
|
||||
private async syncWithRemote(remoteStore: BaseStore): Promise<void> {
|
||||
const remoteKeys = await remoteStore.keys();
|
||||
const localKeys = await this.localStore.keys();
|
||||
|
||||
const missingLocalKeys = remoteKeys.filter(
|
||||
(key) => !localKeys.includes(key),
|
||||
);
|
||||
const missingRemoteKeys = localKeys.filter(
|
||||
(key) => !remoteKeys.includes(key),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
...missingLocalKeys.map(async (key) => {
|
||||
const value = await remoteStore.get(key);
|
||||
await this.localStore.set(key, value);
|
||||
}),
|
||||
...missingRemoteKeys.map(async (key) => {
|
||||
const value = await this.localStore.get(key);
|
||||
await remoteStore.set(key, value);
|
||||
}),
|
||||
]);
|
||||
await this.syncStore(remoteStore);
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
// Sync all remote stores in parallel
|
||||
await Promise.all(
|
||||
this.remoteStores.map(async (remoteStore) =>
|
||||
this.syncWithRemote(remoteStore),
|
||||
),
|
||||
this.stores.map(async (remoteStore) => this.syncStore(remoteStore)),
|
||||
);
|
||||
}
|
||||
|
||||
private async syncStore(store: BaseStore): Promise<void> {
|
||||
const storeKeys = await store.keys();
|
||||
const primaryStoreKeys = await this.primaryStore.keys();
|
||||
|
||||
const missingLocalKeys = storeKeys.filter(
|
||||
(key) => !primaryStoreKeys.includes(key),
|
||||
);
|
||||
const missingRemoteKeys = primaryStoreKeys.filter(
|
||||
(key) => !storeKeys.includes(key),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
...missingLocalKeys.map(async (key) => {
|
||||
const value = await store.get(key);
|
||||
await this.set(key, value);
|
||||
}),
|
||||
...missingRemoteKeys.map(async (key) => {
|
||||
const value = await this.primaryStore.get(key);
|
||||
await store.set(key, value);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,55 @@ import { StoreInMemorySynced } from './store-inmemory-synced.js';
|
||||
|
||||
import { Encrypter } from '@xocash/primitives';
|
||||
|
||||
// TODO: Only use pack/unpack on encrypted content.
|
||||
// Unsure why you were doing it on non-encrypted content (as it's not needed for IDB).
|
||||
// Did you bump into an object that couldn't be structuredClone'd?
|
||||
// Only use pack/unpack on encrypted content.
|
||||
// This ensures consistent handling of data whether encrypted or not.
|
||||
import { pack, unpack } from 'msgpackr';
|
||||
|
||||
import { openDB, deleteDB, IDBPDatabase } from 'idb';
|
||||
|
||||
export class StorageIndexedDB extends BaseStorage {
|
||||
private constructor(
|
||||
private db: IDBPDatabase,
|
||||
private dbName: string,
|
||||
) {
|
||||
private db: IDBPDatabase;
|
||||
private dbName: string;
|
||||
private versionChangePromise: Promise<void> | null = null;
|
||||
private activeStores: Set<StoreIndexedDB> = new Set(); // Track all active stores
|
||||
|
||||
private constructor(db: IDBPDatabase, dbName: string) {
|
||||
super();
|
||||
this.db = db;
|
||||
this.dbName = dbName;
|
||||
|
||||
// Listen for version change events
|
||||
this.db.addEventListener('versionchange', () => {
|
||||
this.db.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for any ongoing version changes to complete
|
||||
private async waitForVersionChange(): Promise<void> {
|
||||
if (this.versionChangePromise) {
|
||||
await this.versionChangePromise;
|
||||
}
|
||||
}
|
||||
|
||||
public static async createOrOpen(dbName: string) {
|
||||
try {
|
||||
// Check if database exists by attempting to open it at current version
|
||||
const existingDb = await openDB(dbName, undefined);
|
||||
const existingDb = await openDB(dbName, undefined, {
|
||||
blocked() {
|
||||
// Handle case where the database is blocked
|
||||
console.warn(`Database ${dbName} is blocked, waiting...`);
|
||||
},
|
||||
blocking() {
|
||||
// When this connection is blocking others
|
||||
console.warn(`This connection is blocking others for ${dbName}`);
|
||||
},
|
||||
terminated() {
|
||||
// When the database connection is terminated unexpectedly
|
||||
console.error(
|
||||
`Database connection to ${dbName} was terminated unexpectedly`,
|
||||
);
|
||||
},
|
||||
});
|
||||
return new StorageIndexedDB(existingDb, dbName);
|
||||
} catch (error) {
|
||||
// If database doesn't exist, create it at version 1
|
||||
@@ -35,33 +65,67 @@ export class StorageIndexedDB extends BaseStorage {
|
||||
}
|
||||
|
||||
async listStores() {
|
||||
await this.waitForVersionChange();
|
||||
return Array.from(this.db.objectStoreNames);
|
||||
}
|
||||
|
||||
async createStore(storeName: string, opts: StorageOpts = {}) {
|
||||
await this.waitForVersionChange();
|
||||
|
||||
if (!this.db.objectStoreNames.contains(storeName)) {
|
||||
const currentVersion = this.db.version;
|
||||
this.db.close();
|
||||
|
||||
const db = await openDB(this.dbName, currentVersion + 1, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore(storeName);
|
||||
},
|
||||
});
|
||||
this.db = db;
|
||||
// Set up the version change promise
|
||||
this.versionChangePromise = (async () => {
|
||||
try {
|
||||
const newDb = await openDB(this.dbName, currentVersion + 1, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore(storeName);
|
||||
},
|
||||
blocked() {
|
||||
console.warn(`Database upgrade is blocked, waiting...`);
|
||||
},
|
||||
blocking() {
|
||||
console.warn(`This connection is blocking upgrades`);
|
||||
},
|
||||
terminated() {
|
||||
console.error(
|
||||
`Database connection was terminated unexpectedly during upgrade`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Update the database reference
|
||||
this.db = newDb;
|
||||
|
||||
// Update db references in all active stores
|
||||
for (const store of this.activeStores) {
|
||||
store.updateDatabaseReference(newDb);
|
||||
}
|
||||
} finally {
|
||||
this.versionChangePromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
// Wait for the version change to complete
|
||||
await this.versionChangePromise;
|
||||
}
|
||||
|
||||
const store = await StoreIndexedDB.create(
|
||||
this.db,
|
||||
storeName,
|
||||
opts.encryption,
|
||||
this, // Pass a reference to the parent storage
|
||||
);
|
||||
|
||||
this.activeStores.add(store); // Track this store
|
||||
return opts.syncInMemory ? await StoreInMemorySynced.create(store) : store;
|
||||
}
|
||||
|
||||
async getStore(storeName: string, opts: StorageOpts = {}) {
|
||||
try {
|
||||
await this.waitForVersionChange();
|
||||
|
||||
if (!this.db.objectStoreNames.contains(storeName)) {
|
||||
return null;
|
||||
}
|
||||
@@ -70,9 +134,13 @@ export class StorageIndexedDB extends BaseStorage {
|
||||
this.db,
|
||||
storeName,
|
||||
opts.encryption,
|
||||
this, // Pass reference to parent
|
||||
);
|
||||
|
||||
return opts.syncInMemory ? StoreInMemorySynced.create(store) : store;
|
||||
this.activeStores.add(store); // Track this store
|
||||
return opts.syncInMemory
|
||||
? await StoreInMemorySynced.create(store)
|
||||
: store;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
@@ -87,23 +155,60 @@ export class StorageIndexedDB extends BaseStorage {
|
||||
}
|
||||
|
||||
async deleteStore(storeName: string) {
|
||||
await this.waitForVersionChange();
|
||||
|
||||
if (this.db.objectStoreNames.contains(storeName)) {
|
||||
const currentVersion = this.db.version;
|
||||
this.db.close();
|
||||
|
||||
const db = await openDB(this.dbName, currentVersion + 1, {
|
||||
upgrade(db) {
|
||||
db.deleteObjectStore(storeName);
|
||||
},
|
||||
});
|
||||
this.db = db;
|
||||
this.versionChangePromise = (async () => {
|
||||
try {
|
||||
const newDb = await openDB(this.dbName, currentVersion + 1, {
|
||||
upgrade(db) {
|
||||
db.deleteObjectStore(storeName);
|
||||
},
|
||||
blocked() {
|
||||
console.warn(`Database upgrade is blocked, waiting...`);
|
||||
},
|
||||
blocking() {
|
||||
console.warn(`This connection is blocking upgrades`);
|
||||
},
|
||||
terminated() {
|
||||
console.error(
|
||||
`Database connection was terminated unexpectedly during upgrade`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.db = newDb;
|
||||
|
||||
// Update db references in all active stores
|
||||
for (const store of this.activeStores) {
|
||||
store.updateDatabaseReference(newDb);
|
||||
}
|
||||
} finally {
|
||||
this.versionChangePromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
await this.versionChangePromise;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDatabase() {
|
||||
// Remove all active stores before closing
|
||||
this.activeStores.clear();
|
||||
|
||||
// Close current connection
|
||||
this.db.close();
|
||||
|
||||
// Delete the database
|
||||
await deleteDB(this.dbName);
|
||||
}
|
||||
|
||||
// Method to remove store from tracking when it's closed
|
||||
removeActiveStore(store: StoreIndexedDB) {
|
||||
this.activeStores.delete(store);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,26 +219,24 @@ export class StoreIndexedDB extends BaseStore {
|
||||
public db: IDBPDatabase,
|
||||
public storeName: string,
|
||||
public encryption?: Encrypter,
|
||||
private parentStorage?: StorageIndexedDB, // Reference to parent storage
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/*
|
||||
this.broadcastChannel = new BroadcastChannel(
|
||||
`StoreIndexedDB:${db.name}.${this.storeName}`,
|
||||
);
|
||||
|
||||
this.broadcastChannel.onmessage = (message) => {
|
||||
this.emit('set', message);
|
||||
};
|
||||
*/
|
||||
// Method to update the database reference
|
||||
updateDatabaseReference(newDb: IDBPDatabase) {
|
||||
this.db = newDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new instance of StoreIndexedDB.
|
||||
* Provides a more convenient way to instantiate the store when working with promises.
|
||||
* Provides a convenient way to instantiate the store when working with promises.
|
||||
*
|
||||
* @param db - An instance of an upgraded and opened IndexedDB database
|
||||
* @param storeName - The name of the object store within the database to use
|
||||
* @param encryption - Optional encrypter for encrypting/decrypting data
|
||||
* @param parentStorage - Reference to the parent StorageIndexedDB instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -144,8 +247,9 @@ export class StoreIndexedDB extends BaseStore {
|
||||
db: IDBPDatabase,
|
||||
storeName: string,
|
||||
encryption?: Encrypter,
|
||||
parentStorage?: StorageIndexedDB,
|
||||
) {
|
||||
return new this(db, storeName, encryption);
|
||||
return new this(db, storeName, encryption, parentStorage);
|
||||
}
|
||||
|
||||
async all<T>() {
|
||||
@@ -173,6 +277,10 @@ export class StoreIndexedDB extends BaseStore {
|
||||
async get<T>(key: string) {
|
||||
const encodedValue = await this.db.get(this.storeName, key);
|
||||
|
||||
if (!encodedValue) {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
const decryptedValue = this.encryption
|
||||
? this.encryption.decrypt(encodedValue)
|
||||
: encodedValue;
|
||||
@@ -221,7 +329,10 @@ export class StoreIndexedDB extends BaseStore {
|
||||
await Promise.all(
|
||||
Array.from(items).map(([key, value]) => {
|
||||
const encodedValue = pack(value);
|
||||
return store.put(encodedValue, key);
|
||||
const encryptedValue = this.encryption
|
||||
? this.encryption.encrypt(encodedValue)
|
||||
: encodedValue;
|
||||
return store.put(encryptedValue, key);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -245,6 +356,10 @@ export class StoreIndexedDB extends BaseStore {
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.db.close();
|
||||
// Remove this store from parent's tracking
|
||||
if (this.parentStorage) {
|
||||
this.parentStorage.removeActiveStore(this);
|
||||
}
|
||||
// Note: We don't close the DB here as it's managed by StorageIndexedDB
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import { BaseStorage, BaseStore, StorageOpts } from './base-store.js';
|
||||
import { StoreInMemorySynced } from './store-inmemory-synced.js';
|
||||
|
||||
import {
|
||||
Bytes,
|
||||
Encrypter,
|
||||
encodeExtendedJson,
|
||||
decodeExtendedJson,
|
||||
} from '@xocash/primitives';
|
||||
|
||||
export class StorageLocalStorage extends BaseStorage {
|
||||
private dbName: string;
|
||||
private activeStores: Set<StoreLocalStorage> = new Set(); // Track all active stores
|
||||
|
||||
private constructor(dbName: string) {
|
||||
super();
|
||||
this.dbName = dbName;
|
||||
}
|
||||
|
||||
public static async createOrOpen(dbName: string) {
|
||||
return new StorageLocalStorage(dbName);
|
||||
}
|
||||
|
||||
async listStores() {
|
||||
const prefix = `${this.dbName}.`;
|
||||
const stores = new Set<string>();
|
||||
|
||||
// Iterate through localStorage to find all stores with our dbName prefix
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(prefix)) {
|
||||
const parts = key.split('.');
|
||||
if (parts.length >= 2) {
|
||||
stores.add(parts[1]); // Extract store name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(stores);
|
||||
}
|
||||
|
||||
async createStore(storeName: string, opts: StorageOpts = {}) {
|
||||
// No need to explicitly create a store in localStorage, just return an instance
|
||||
const store = await StoreLocalStorage.create(
|
||||
this.dbName,
|
||||
storeName,
|
||||
opts.encryption,
|
||||
this, // Pass a reference to the parent storage
|
||||
);
|
||||
|
||||
this.activeStores.add(store); // Track this store
|
||||
return opts.syncInMemory ? await StoreInMemorySynced.create(store) : store;
|
||||
}
|
||||
|
||||
async getStore(storeName: string, opts: StorageOpts = {}) {
|
||||
try {
|
||||
// Check if store exists by looking for any keys with this store's prefix
|
||||
const storePrefix = `${this.dbName}.${storeName}.`;
|
||||
let storeExists = false;
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(storePrefix)) {
|
||||
storeExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!storeExists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = await StoreLocalStorage.create(
|
||||
this.dbName,
|
||||
storeName,
|
||||
opts.encryption,
|
||||
this, // Pass reference to parent
|
||||
);
|
||||
|
||||
this.activeStores.add(store); // Track this store
|
||||
return opts.syncInMemory
|
||||
? await StoreInMemorySynced.create(store)
|
||||
: store;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createOrGetStore(storeName: string, opts: StorageOpts = {}) {
|
||||
const existing = await this.getStore(storeName, opts);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return this.createStore(storeName, opts);
|
||||
}
|
||||
|
||||
async deleteStore(storeName: string) {
|
||||
const storePrefix = `${this.dbName}.${storeName}.`;
|
||||
|
||||
// Collect all keys to remove
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(storePrefix)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all keys for this store
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
}
|
||||
|
||||
async deleteDatabase() {
|
||||
// Remove all active stores before closing
|
||||
this.activeStores.clear();
|
||||
|
||||
// Remove all keys with this dbName prefix
|
||||
const prefix = `${this.dbName}.`;
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(prefix)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
}
|
||||
|
||||
// Method to remove store from tracking when it's closed
|
||||
removeActiveStore(store: StoreLocalStorage) {
|
||||
this.activeStores.delete(store);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of BaseStore using LocalStorage as the storage backend.
|
||||
*/
|
||||
export class StoreLocalStorage extends BaseStore {
|
||||
private keyPrefix: string;
|
||||
|
||||
constructor(
|
||||
public dbName: string,
|
||||
public storeName: string,
|
||||
public encryption?: Encrypter,
|
||||
private parentStorage?: StorageLocalStorage, // Reference to parent storage
|
||||
) {
|
||||
super();
|
||||
this.keyPrefix = `${dbName}.${storeName}.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new instance of StoreLocalStorage.
|
||||
* Provides a convenient way to instantiate the store when working with promises.
|
||||
*
|
||||
* @param dbName - The name of the database
|
||||
* @param storeName - The name of the store within the database
|
||||
* @param encryption - Optional encrypter for encrypting/decrypting data
|
||||
* @param parentStorage - Reference to the parent StorageLocalStorage instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const store = await StoreLocalStorage.create('myDB', 'myStore');
|
||||
* ```
|
||||
*/
|
||||
static async create(
|
||||
dbName: string,
|
||||
storeName: string,
|
||||
encryption?: Encrypter,
|
||||
parentStorage?: StorageLocalStorage,
|
||||
) {
|
||||
return new this(dbName, storeName, encryption, parentStorage);
|
||||
}
|
||||
|
||||
async all<T>() {
|
||||
const result: { [key: string]: T } = {};
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const fullKey = localStorage.key(i);
|
||||
if (fullKey && fullKey.startsWith(this.keyPrefix)) {
|
||||
const key = fullKey.substring(this.keyPrefix.length);
|
||||
const value = await this.get<T>(key);
|
||||
if (value !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async keys() {
|
||||
const keys: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const fullKey = localStorage.key(i);
|
||||
if (fullKey && fullKey.startsWith(this.keyPrefix)) {
|
||||
keys.push(fullKey.substring(this.keyPrefix.length));
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
async get<T>(key: string) {
|
||||
const fullKey = this.keyPrefix + key;
|
||||
const encodedValue = localStorage.getItem(fullKey);
|
||||
|
||||
if (!encodedValue) {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedValue = this.encryption
|
||||
? Bytes.from(await this.encryption.decrypt(encodedValue)).toUtf8()
|
||||
: encodedValue;
|
||||
|
||||
return decodeExtendedJson(decryptedValue) as T;
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving value for key ${key}:`, error);
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T) {
|
||||
const fullKey = this.keyPrefix + key;
|
||||
const encodedValue = encodeExtendedJson(value);
|
||||
|
||||
const encryptedValue = this.encryption
|
||||
? Bytes.from(await this.encryption.encrypt(encodedValue)).toHex()
|
||||
: encodedValue;
|
||||
|
||||
// LocalStorage can only store strings, so we convert the binary data
|
||||
localStorage.setItem(fullKey, encryptedValue);
|
||||
this.emit('set', { key, value });
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
const fullKey = this.keyPrefix + key;
|
||||
localStorage.removeItem(fullKey);
|
||||
this.emit('delete', { key });
|
||||
}
|
||||
|
||||
async batchGet<T>(keys: string[]): Promise<Map<string, T>> {
|
||||
const results = new Map<string, T>();
|
||||
|
||||
for (const key of keys) {
|
||||
const value = await this.get<T>(key);
|
||||
if (value !== undefined) {
|
||||
results.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async batchSet<T>(items: Map<string, T>) {
|
||||
for (const [key, value] of items) {
|
||||
await this.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const keys = await this.keys();
|
||||
for (const key of keys) {
|
||||
await this.delete(key);
|
||||
}
|
||||
this.emit('clear', undefined);
|
||||
}
|
||||
|
||||
async has(key: string) {
|
||||
const fullKey = this.keyPrefix + key;
|
||||
return localStorage.getItem(fullKey) !== null;
|
||||
}
|
||||
|
||||
async close() {
|
||||
// Remove this store from parent's tracking
|
||||
if (this.parentStorage) {
|
||||
this.parentStorage.removeActiveStore(this);
|
||||
}
|
||||
// No need to close anything with localStorage
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,41 @@
|
||||
import { type Activity } from '../storage/index.js';
|
||||
|
||||
export type WalletActivities =
|
||||
| WalletName
|
||||
| WalletDescription
|
||||
| WalletHDAddressGap
|
||||
| TransactionFirstSeen
|
||||
| TransactionMemo
|
||||
| XOWalletColor
|
||||
| XOTransactionCategory
|
||||
| XOFrozenCoin;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Standardized Activities
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export interface WalletName extends Activity {
|
||||
name: 'WalletName';
|
||||
key: 'WalletName';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface WalletDescription extends Activity {
|
||||
name: 'WalletDescription';
|
||||
key: 'WalletDescription';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface WalletHDAddressGap extends Activity {
|
||||
name: 'WalletHDAddressGap';
|
||||
key: 'WalletHDAddressGap';
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TransactionFirstSeen extends Activity {
|
||||
name: 'TransactionFirstSeen';
|
||||
key: 'TransactionFirstSeen';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TransactionMemo extends Activity {
|
||||
name: 'TransactionMemo';
|
||||
key: 'TransactionMemo';
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -34,16 +44,16 @@ export interface TransactionMemo extends Activity {
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export interface XOWalletColor extends Activity {
|
||||
name: 'XOWalletColor';
|
||||
key: 'XOWalletColor';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface XOTransactionCategory extends Activity {
|
||||
name: 'XOTransactionCategory';
|
||||
key: 'XOTransactionCategory';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface XOFrozenCoin extends Activity {
|
||||
name: 'XOFrozenCoin';
|
||||
key: 'XOFrozenCoin';
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import {
|
||||
WalletUTXOs,
|
||||
type InputTemplateBCH,
|
||||
type TransactionTemplate,
|
||||
type WalletAddresses,
|
||||
type WalletBlockchain,
|
||||
type WalletTransactions,
|
||||
} from './types.js';
|
||||
|
||||
import { type BlockchainUTXOs } from '../blockchain/index.js';
|
||||
import { type Activities } from '../storage/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';
|
||||
|
||||
export type WalletEvents = {
|
||||
shouldMonitorUpdated: boolean;
|
||||
addressesUpdated: WalletAddresses;
|
||||
transactionsUpdated: WalletTransactions;
|
||||
unspentsUpdated: BlockchainUTXOs;
|
||||
stateUpdated: string | null;
|
||||
@@ -30,6 +33,13 @@ export type WalletEvents = {
|
||||
export abstract class BaseWallet<
|
||||
WalletEventsType extends WalletEvents = WalletEvents,
|
||||
> extends EventEmitter<WalletEventsType> {
|
||||
static readonly deriveKey: (keyName: string) => Promise<Uint8Array>;
|
||||
|
||||
// Dependencies.
|
||||
abstract readonly activities: Activities;
|
||||
abstract readonly blockchain: WalletBlockchain;
|
||||
abstract readonly cache: BaseStore;
|
||||
|
||||
/**
|
||||
* Get the ID for this wallet.
|
||||
*/
|
||||
@@ -50,11 +60,6 @@ export abstract class BaseWallet<
|
||||
*/
|
||||
abstract getSigningKey(): Promise<Uint8Array>;
|
||||
|
||||
/**
|
||||
* Get the activities implementation
|
||||
*/
|
||||
abstract getActivities(): Promise<Activities>;
|
||||
|
||||
/**
|
||||
* Start monitoring this wallet for any transaction activity.
|
||||
*/
|
||||
@@ -109,7 +114,7 @@ export abstract class BaseWallet<
|
||||
* May as well just getUnspentDirectives() from the local state instead of fetch.
|
||||
* Likewise for balances.
|
||||
*/
|
||||
abstract getUnspentDirectives(): Promise<Array<InputTemplateBCH>>;
|
||||
abstract getUnspentDirectives(): Promise<WalletUTXOs>;
|
||||
|
||||
/**
|
||||
* Get the satoshi balance of this wallet.
|
||||
|
||||
@@ -45,6 +45,8 @@ export type WalletUTXO = {
|
||||
>;
|
||||
};
|
||||
|
||||
export type WalletUTXOs = ExtMap<WalletUTXO>;
|
||||
|
||||
export type TransactionTemplate = TransactionTemplateFixed<CompilerBCH>;
|
||||
|
||||
export type FilterAndSortCallback = () => Promise<Array<InputTemplateBCH>>;
|
||||
|
||||
@@ -59,22 +59,24 @@ export type WalletHDGenesisData = {
|
||||
export class WalletHD extends BaseWallet {
|
||||
static async from<T extends typeof WalletHD>(
|
||||
this: T,
|
||||
services: WalletDependencies,
|
||||
deps: 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,
|
||||
// Assign defaults if not otherwise provideed.
|
||||
const activities = deps.activities
|
||||
? deps.activities
|
||||
: await Activities.from();
|
||||
const cache = deps.cache ? deps.cache : StoreInMemory.from();
|
||||
|
||||
// Assign our final dependencies.
|
||||
const depsFinal = {
|
||||
activities,
|
||||
cache,
|
||||
blockchain: deps.blockchain,
|
||||
};
|
||||
|
||||
// Return a new instance of WalletHD.
|
||||
return new this(servicesFinal, genesisData) as InstanceType<T>;
|
||||
return new this(depsFinal, genesisData) as InstanceType<T>;
|
||||
}
|
||||
|
||||
// Dependencies.
|
||||
@@ -158,10 +160,6 @@ export class WalletHD extends BaseWallet {
|
||||
);
|
||||
}
|
||||
|
||||
async getActivities(): Promise<Activities> {
|
||||
return this.activities;
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Get the address gap.
|
||||
// TODO: This should come from activities.
|
||||
@@ -263,7 +261,7 @@ export class WalletHD extends BaseWallet {
|
||||
const unspentDirectives = await Promise.all(fetchPromises);
|
||||
|
||||
// Flatten them.
|
||||
const unspentsFlattened = unspentDirectives.flat();
|
||||
const unspentsFlattened = ExtMap.flatten(unspentDirectives);
|
||||
|
||||
// Return the unspents.
|
||||
return unspentsFlattened;
|
||||
@@ -358,7 +356,7 @@ export class WalletHD extends BaseWallet {
|
||||
// Build the transaction.
|
||||
const tx = TransactionBuilder.build(
|
||||
txTemplateFinal,
|
||||
unspents,
|
||||
unspents.toArray(),
|
||||
changeLockingBytecode,
|
||||
);
|
||||
|
||||
@@ -398,6 +396,7 @@ export class WalletHD extends BaseWallet {
|
||||
// Create a child P2PKH wallet for this Private Key.
|
||||
const wallet = await WalletP2PKH.from(
|
||||
{
|
||||
activities: this.activities,
|
||||
blockchain: this.blockchain,
|
||||
cache: this.cache,
|
||||
},
|
||||
|
||||
@@ -46,20 +46,27 @@ export type WalletP2PKHGenesisData = {
|
||||
export class WalletP2PKH extends BaseWallet {
|
||||
static async from<T extends typeof WalletP2PKH>(
|
||||
this: T,
|
||||
dependencies: WalletDependencies,
|
||||
deps: WalletDependencies,
|
||||
genesisData: WalletP2PKHGenesisData,
|
||||
) {
|
||||
// Assign in memory stores as defaults in case otherwise not provided.
|
||||
const dependenciesFinal = {
|
||||
...{
|
||||
activities: await Activities.fromStores(StoreInMemory.from()),
|
||||
cache: new StoreInMemory(),
|
||||
},
|
||||
...dependencies,
|
||||
// Assign defaults if not otherwise provideed.
|
||||
const activities = deps.activities
|
||||
? deps.activities
|
||||
: await Activities.from();
|
||||
const cache = deps.cache ? deps.cache : StoreInMemory.from();
|
||||
|
||||
// Assign our final dependencies.
|
||||
const depsFinal = {
|
||||
activities,
|
||||
cache,
|
||||
blockchain: deps.blockchain,
|
||||
};
|
||||
|
||||
// Return a new instance of Wallet P2PKH.
|
||||
return new this(dependenciesFinal, genesisData) as InstanceType<T>;
|
||||
const wallet = new this(depsFinal, genesisData) as InstanceType<T>;
|
||||
|
||||
// Return the wallet instance.
|
||||
return wallet;
|
||||
}
|
||||
|
||||
static fromBytes<T extends typeof WalletP2PKH>(
|
||||
@@ -188,10 +195,6 @@ export class WalletP2PKH extends BaseWallet {
|
||||
);
|
||||
}
|
||||
|
||||
async getActivities(): Promise<Activities> {
|
||||
return this.activities;
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Do nothing if already started.
|
||||
if (this.isStarted) {
|
||||
@@ -314,23 +317,21 @@ export class WalletP2PKH extends BaseWallet {
|
||||
const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd);
|
||||
|
||||
// Transform our inputs into unlocking directives.
|
||||
const inputDirectives = unspentOutputs
|
||||
.map((blockchainUTXO) => ({
|
||||
outpointIndex: blockchainUTXO.utxo.outpoint.outpointIndex,
|
||||
outpointTransactionHash:
|
||||
blockchainUTXO.utxo.outpoint.outpointTransactionHash,
|
||||
sequenceNumber: 0,
|
||||
unlockingBytecode: {
|
||||
compiler: compilerP2PKH,
|
||||
data: {
|
||||
keys: { privateKeys: { key: this.privateKey.toBytes() } },
|
||||
},
|
||||
script: 'unlock',
|
||||
token: blockchainUTXO.utxo.output.token,
|
||||
valueSatoshis: blockchainUTXO.utxo.output.valueSatoshis,
|
||||
const inputDirectives = unspentOutputs.map((blockchainUTXO) => ({
|
||||
outpointIndex: blockchainUTXO.utxo.outpoint.outpointIndex,
|
||||
outpointTransactionHash:
|
||||
blockchainUTXO.utxo.outpoint.outpointTransactionHash,
|
||||
sequenceNumber: 0,
|
||||
unlockingBytecode: {
|
||||
compiler: compilerP2PKH,
|
||||
data: {
|
||||
keys: { privateKeys: { key: this.privateKey.toBytes() } },
|
||||
},
|
||||
}))
|
||||
.toArray();
|
||||
script: 'unlock',
|
||||
token: blockchainUTXO.utxo.output.token,
|
||||
valueSatoshis: blockchainUTXO.utxo.output.valueSatoshis,
|
||||
},
|
||||
}));
|
||||
|
||||
// Return the directives.
|
||||
return inputDirectives;
|
||||
@@ -397,7 +398,7 @@ export class WalletP2PKH extends BaseWallet {
|
||||
// Build the transaction.
|
||||
const tx = TransactionBuilder.build(
|
||||
txTemplateFinal,
|
||||
unspents,
|
||||
unspents.toArray(),
|
||||
changeLockingBytecode,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,16 +2,15 @@ import { describe, test, expect, beforeEach } from 'vitest';
|
||||
import { Activities, StoreInMemory } from '../../src/storage';
|
||||
|
||||
describe('Activities', () => {
|
||||
let store: StoreInMemory;
|
||||
let activities: Activities;
|
||||
|
||||
beforeEach(async () => {
|
||||
activities = await Activities.fromStores(StoreInMemory.from());
|
||||
activities = await Activities.from();
|
||||
});
|
||||
|
||||
test('should add activity with auto-generated timestamp', async () => {
|
||||
const activity = {
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
@@ -28,7 +27,7 @@ describe('Activities', () => {
|
||||
const timestamp = BigInt(1234567890000);
|
||||
const activity = {
|
||||
timestamp,
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
@@ -45,14 +44,14 @@ describe('Activities', () => {
|
||||
|
||||
await activities.add({
|
||||
timestamp: oldTimestamp,
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Old Name',
|
||||
});
|
||||
|
||||
await activities.add({
|
||||
timestamp: newTimestamp,
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'New Name',
|
||||
});
|
||||
@@ -73,14 +72,14 @@ describe('Activities', () => {
|
||||
|
||||
await activities.add({
|
||||
timestamp: oldTimestamp,
|
||||
name: 'TransactionFirstSeen',
|
||||
key: 'TransactionFirstSeen',
|
||||
entityId: 'tx1',
|
||||
value: 'First Value',
|
||||
});
|
||||
|
||||
await activities.add({
|
||||
timestamp: newTimestamp,
|
||||
name: 'TransactionFirstSeen',
|
||||
key: 'TransactionFirstSeen',
|
||||
entityId: 'tx1',
|
||||
value: 'Second Value',
|
||||
});
|
||||
@@ -100,14 +99,14 @@ describe('Activities', () => {
|
||||
|
||||
await activities.add({
|
||||
timestamp: BigInt(1000),
|
||||
name: 'CustomType',
|
||||
key: 'CustomType',
|
||||
entityId: 'custom1',
|
||||
value: 50,
|
||||
});
|
||||
|
||||
await activities.add({
|
||||
timestamp: BigInt(2000),
|
||||
name: 'CustomType',
|
||||
key: 'CustomType',
|
||||
entityId: 'custom1',
|
||||
value: 30,
|
||||
});
|
||||
@@ -121,19 +120,19 @@ describe('Activities', () => {
|
||||
const testActivities = [
|
||||
{
|
||||
timestamp: BigInt(3000),
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Name 3',
|
||||
},
|
||||
{
|
||||
timestamp: BigInt(1000),
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Name 1',
|
||||
},
|
||||
{
|
||||
timestamp: BigInt(2000),
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Name 2',
|
||||
},
|
||||
@@ -151,14 +150,14 @@ describe('Activities', () => {
|
||||
test('should handle multiple activity types independently', async () => {
|
||||
await activities.add({
|
||||
timestamp: BigInt(1000),
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'My Wallet',
|
||||
});
|
||||
|
||||
await activities.add({
|
||||
timestamp: BigInt(2000),
|
||||
name: 'WalletDescription',
|
||||
key: 'WalletDescription',
|
||||
entityId: 'wallet1',
|
||||
value: 'My Description',
|
||||
});
|
||||
@@ -172,7 +171,7 @@ describe('Activities', () => {
|
||||
test('should update snapshot immediately when activity is added', async () => {
|
||||
const activity = {
|
||||
timestamp: BigInt(1000),
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
@@ -186,7 +185,7 @@ describe('Activities', () => {
|
||||
|
||||
test('should handle missing timestamp gracefully', async () => {
|
||||
const activity = {
|
||||
name: 'WalletName',
|
||||
key: 'WalletName',
|
||||
entityId: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user