Handle browser/node envs

Signed-off-by: James Zuccon <zuccon@gmail.com>
This commit is contained in:
James Zuccon
2024-09-13 07:54:24 +10:00
parent 2c9812200d
commit d9609e5b9f
14 changed files with 1211 additions and 307 deletions
+3
View File
@@ -13,3 +13,6 @@ e2e-tests.xml
docs/.vitepress/dist
docs/.vitepress/cache
docs/api
# Ignore any lerna debug output.
lerna-debug.log
+22 -1
View File
@@ -1,5 +1,10 @@
import { Transaction } from '@xo/primitives';
import { BlockchainLocal, WalletP2PKH } from '@xo/wallet';
import {
BlockchainLocal,
StorageMemory,
WalletP2PKH,
StorageSQLite,
} from '@xo/wallet';
const blockchainLocal = new BlockchainLocal();
await blockchainLocal.start();
@@ -52,3 +57,19 @@ console.log(
);
console.log(blockchainLocal.deriveUtxoSet());
const storageMemory = new StorageMemory();
await storageMemory.start();
await storageMemory.set('test', {
isItWorking: true,
});
console.log(storageMemory.storage);
const storageIDB = new StorageSQLite();
await storageIDB.start();
await storageIDB.set('test', {
thisShouldNotWork: true,
});
+998 -294
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -43,9 +43,9 @@
"@bitauth/libauth": "^3.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"eslint": "^8.10.0",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"eslint": "^9.10.0",
"eslint-config-prettier": "^8.1.0",
"lerna": "^8.1.8",
"prettier": "^2.5.1",
-1
View File
@@ -8,5 +8,4 @@ export * from './public-key.js';
export * from './satoshis.js';
export * from './script.js';
export * from './transaction.js';
export * from './transaction-graph.js';
export * from './transaction-hash.js';
+13 -1
View File
@@ -6,6 +6,16 @@
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"browser": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
},
"node": {
"types": "./dist/index.node.d.ts",
"default": "./dist/index.node.js"
}
},
"files": [
"dist"
],
@@ -18,6 +28,8 @@
"dependencies": {
"@electrum-cash/network": "^3.3.0",
"@xo/primitives": "^0.0.1",
"idb-keyval": "^6.2.1"
"idb-keyval": "^6.2.1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
}
}
@@ -11,6 +11,7 @@ import {
AddressListUnspent,
TransactionGet,
} from './blockchain-electrum-types.js';
import { EventEmitter } from '../utils/index.js';
import { Address, Transaction } from '@xo/primitives';
import { ElectrumClient } from '@electrum-cash/network';
@@ -21,10 +22,17 @@ export interface BroadcastOpts {
waitUntilSeen: boolean;
}
export type BlockchainElectrumEvents = {
isConnectedUpdated: boolean;
};
export class BlockchainElectrum implements IBlockchain {
// Composables.
events: EventEmitter<BlockchainElectrumEvents> = new EventEmitter();
electrumClient!: ElectrumClient;
// Address subscriptions.
// State.
isConnected = false;
addressSubscriptions: { [address: string]: AddressCallback } = {};
transactionSubscriptions: { [txHash: string]: TransactionCallback } = {};
@@ -32,7 +40,10 @@ export class BlockchainElectrum implements IBlockchain {
public readonly servers: string[],
public readonly application = 'BCHApp',
public readonly version = '1.5'
) {}
) {
// Monitor the following properties.
this.events.monitorProperty(this, 'isConnected', 'isConnectedUpdated');
}
async start(): Promise<void> {
// TODO: We want to use a Cluster instead of a Client, but as of 2022-07-07 there are issues with Subscriptions on Cluster.
@@ -164,8 +164,7 @@ export class BlockchainLocal implements IBlockchain {
utxoState[`${input.outpointTransactionHash};${input.outpointIndex}`];
// If there is no corresponding source output, skip this notification.
// NOTE: This can occur if skipValidation is set to true in broadcast.
// It is generally useful to skipValidation if you are emulating a coinbase tx.
// NOTE: This can occur if skipValidation is set to true in broadcast (which would be used for "coinbase" transactions).
if (!sourceOutput) {
return;
}
+79 -1
View File
@@ -1,3 +1,9 @@
import { IBlockchain, AddressCallback, TransactionCallback } from './interface';
import { BlockchainElectrum } from './blockchain-electrum.js';
import { BlockchainLocal } from './blockchain-local.js';
import { Transaction } from '@xo/primitives';
/**
* The idea here is that this will implement the IBlockchain interface.
* But, it will allow the dev to specify multiple child IBlockchain interfaces for configuration.
@@ -12,4 +18,76 @@
* ])
* ```
*/
export class Blockchain {}
export class Blockchain implements IBlockchain {
blockchainLocal: BlockchainLocal;
blockchainElectrum: BlockchainElectrum;
constructor() {
this.blockchainLocal = new BlockchainLocal();
this.blockchainElectrum = new BlockchainElectrum(
['bch.imaginary.cash', 'cashnode.bch.ninja', 'electrum.imaginary.cash'],
'BCHApp',
'1.5'
);
}
async start() {
await Promise.all([
this.blockchainLocal.start(),
this.blockchainElectrum.start(),
]);
}
async stop() {
await Promise.all([
this.blockchainLocal.stop(),
this.blockchainElectrum.stop(),
]);
}
async fetchUnspents(address: string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.fetchUnspents(address);
} else {
return await this.blockchainLocal.fetchUnspents(address);
}
}
async fetchTransactions(address: string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.fetchTransactions(address);
} else {
return await this.blockchainLocal.fetchTransactions(address);
}
}
async broadcastTransaction(tx: Uint8Array | Transaction | string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.broadcastTransaction(tx);
} else {
return await this.blockchainLocal.broadcastTransaction(tx);
}
}
async subscribeAddress(address: string, callback: AddressCallback) {
throw new Error('Not yet implemented');
}
async unsubscribeAddress(address: string, callback: AddressCallback) {
throw new Error('Not yet implemented');
}
async subscribeTransaction(
transactionHash: string,
callback: TransactionCallback
) {
throw new Error('Not yet implemented');
}
async unsubscribeTransaction(
transactionHash: string,
callback: TransactionCallback
) {
throw new Error('Not yet implemented');
}
}
+5
View File
@@ -0,0 +1,5 @@
// Export modules that work across all platforms.
export * from './index.js';
// These exports will only work in a browser environment.
export * from './storage/storage-idb.js';
+5
View File
@@ -0,0 +1,5 @@
// Export modules that work across all platforms.
export * from './index.js';
// These exports will only work in a browser environment.
export * from './storage/storage-sqlite.js';
-1
View File
@@ -1,4 +1,3 @@
export * from './interface.js';
export * from './storage.js';
export * from './storage-idb.js';
export * from './storage-memory.js';
+1 -1
View File
@@ -2,7 +2,7 @@ export interface IStorage {
start: () => Promise<void>;
stop: () => Promise<void>;
get: <T>(key: string) => Promise<T>;
get: <T>(key: string) => Promise<T | undefined>;
set: <T>(key: string, value: T) => Promise<void>;
delete: (key: string) => Promise<void>;
clear: () => Promise<void>;
@@ -0,0 +1,68 @@
import { type IStorage } from './interface';
import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';
export class StorageSQLite implements IStorage {
private db: Database | null = null;
constructor(public filename = './storage.db') {}
async start() {
// Open the database.
this.db = await open({
filename: this.filename,
driver: sqlite3.Database,
});
// Enable WAL mode to prevent corruption.
await this.db.run('PRAGMA journal_mode = WAL;');
// Create the key->value store.
await this.db.exec(`
CREATE TABLE IF NOT EXISTS key_value_store (
key TEXT PRIMARY KEY,
value JSON
)
`);
}
async stop() {
if (this.db) {
await this.db.close();
this.db = null;
}
}
async get<T>(key: string): Promise<T | undefined> {
if (!this.db) throw new Error('Database not initialized');
const row = await this.db.get(
'SELECT value FROM key_value_store WHERE key = ?',
key
);
return row ? JSON.parse(row.value) : undefined;
}
async set<T>(key: string, value: T): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
const jsonValue = JSON.stringify(value);
await this.db.run(
'INSERT OR REPLACE INTO key_value_store (key, value) VALUES (?, json(?))',
key,
jsonValue
);
}
async delete(key: string): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
await this.db.run('DELETE FROM key_value_store WHERE key = ?', key);
}
async clear(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
await this.db.run('DELETE FROM key_value_store');
}
}