Handle browser/node envs
Signed-off-by: James Zuccon <zuccon@gmail.com>
This commit is contained in:
@@ -13,3 +13,6 @@ e2e-tests.xml
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/cache
|
||||
docs/api
|
||||
|
||||
# Ignore any lerna debug output.
|
||||
lerna-debug.log
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Generated
+998
-294
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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,4 +1,3 @@
|
||||
export * from './interface.js';
|
||||
export * from './storage.js';
|
||||
export * from './storage-idb.js';
|
||||
export * from './storage-memory.js';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user