WIP (BlockchainLocal working)
Signed-off-by: James Zuccon <zuccon@gmail.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import { Transaction } from '@xo/primitives';
|
||||||
|
import { BlockchainLocal, WalletP2PKH } from '@xo/wallet';
|
||||||
|
|
||||||
|
const blockchainLocal = new BlockchainLocal();
|
||||||
|
await blockchainLocal.start();
|
||||||
|
|
||||||
|
const walletP2PKH = WalletP2PKH.generateRandom(blockchainLocal);
|
||||||
|
const walletP2PKHReceive = WalletP2PKH.generateRandom(blockchainLocal);
|
||||||
|
|
||||||
|
const coinbaseTransaction = new Transaction({
|
||||||
|
version: 2,
|
||||||
|
locktime: 0,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
outpointIndex: 0,
|
||||||
|
outpointTransactionHash: new Uint8Array(),
|
||||||
|
unlockingBytecode: new Uint8Array(),
|
||||||
|
sequenceNumber: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
lockingBytecode: walletP2PKH.getAddress().toLockscriptBytes(),
|
||||||
|
valueSatoshis: 100_000n,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lockingBytecode: walletP2PKH.getAddress().toLockscriptBytes(),
|
||||||
|
valueSatoshis: 150_000n,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await blockchainLocal.broadcastTransaction(coinbaseTransaction, {
|
||||||
|
skipValidation: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('walletP2PKH unspents', await walletP2PKH.fetchUnspentDirectives());
|
||||||
|
console.log(
|
||||||
|
'walletP2PKHReceive unspents',
|
||||||
|
await walletP2PKHReceive.fetchUnspentDirectives()
|
||||||
|
);
|
||||||
|
|
||||||
|
const sweepTx = await walletP2PKH.sweep(
|
||||||
|
walletP2PKHReceive.getAddress().toLockscriptBytes()
|
||||||
|
);
|
||||||
|
await blockchainLocal.broadcastTransaction(sweepTx);
|
||||||
|
|
||||||
|
console.log('walletP2PKH unspents', await walletP2PKH.fetchUnspentDirectives());
|
||||||
|
console.log(
|
||||||
|
'walletP2PKHReceive unspents',
|
||||||
|
await walletP2PKHReceive.fetchUnspentDirectives()
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(blockchainLocal.deriveUtxoSet());
|
||||||
@@ -19,7 +19,7 @@ await wallet.scan();
|
|||||||
const transactions = wallet.getTransactions();
|
const transactions = wallet.getTransactions();
|
||||||
|
|
||||||
transactions.forEach((tx) => {
|
transactions.forEach((tx) => {
|
||||||
blockchain.broadcastTransaction(tx);
|
blockchain.commitTransaction(tx);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(blockchain.pendingTransactions.deriveUtxoState());
|
console.log(blockchain.pendingTransactions.deriveUtxoState());
|
||||||
|
|||||||
Generated
+8
-1
@@ -6007,6 +6007,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/idb-keyval": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -10940,7 +10946,8 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electrum-cash/network": "^3.3.0",
|
"@electrum-cash/network": "^3.3.0",
|
||||||
"@xo/primitives": "^0.0.1"
|
"@xo/primitives": "^0.0.1",
|
||||||
|
"idb-keyval": "^6.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class Address {
|
|||||||
/**
|
/**
|
||||||
* Decode a CashAddr into an Address Entity.
|
* Decode a CashAddr into an Address Entity.
|
||||||
*
|
*
|
||||||
* @param address {string} The CashAddr to decode.
|
* @param cashAddr The CashAddr to decode.
|
||||||
*
|
*
|
||||||
* @throws {Error} If address cannot be decoded.
|
* @throws {Error} If address cannot be decoded.
|
||||||
*
|
*
|
||||||
@@ -71,7 +71,7 @@ export class Address {
|
|||||||
/**
|
/**
|
||||||
* Decode a Legacy Address into an Address Entity.
|
* Decode a Legacy Address into an Address Entity.
|
||||||
*
|
*
|
||||||
* @param address {string} The Legacy (Base58) Address to decode.
|
* @param legacyAddress The Legacy (Base58) Address to decode.
|
||||||
*
|
*
|
||||||
* @throws {Error} If address cannot be decoded.
|
* @throws {Error} If address cannot be decoded.
|
||||||
*
|
*
|
||||||
@@ -128,7 +128,7 @@ export class Address {
|
|||||||
/**
|
/**
|
||||||
* Instantiate an address from Contract Bytecode.
|
* Instantiate an address from Contract Bytecode.
|
||||||
*
|
*
|
||||||
* @param contractBytes {Uint8Array} The contract bytecode
|
* @param contractBytes The contract bytecode
|
||||||
*
|
*
|
||||||
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
||||||
*
|
*
|
||||||
@@ -156,7 +156,7 @@ export class Address {
|
|||||||
/**
|
/**
|
||||||
* Instantiate an address from Contract Bytecode as a hex string.
|
* Instantiate an address from Contract Bytecode as a hex string.
|
||||||
*
|
*
|
||||||
* @param contractBytes {Uint8Array} The contract bytecode
|
* @param hex The contract bytecode as hex
|
||||||
*
|
*
|
||||||
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class Bytes {
|
|||||||
/**
|
/**
|
||||||
* Instantiates bytes from a Base64 string.
|
* Instantiates bytes from a Base64 string.
|
||||||
*
|
*
|
||||||
* @param utf8 {string} The Base64 string.
|
* @param base64 The Base64 string.
|
||||||
*
|
*
|
||||||
* @throws {Error} If Base64 is invalid.
|
* @throws {Error} If Base64 is invalid.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
deriveHdPath,
|
deriveHdPath,
|
||||||
deriveHdPublicNode,
|
deriveHdPublicNode,
|
||||||
deriveHdPrivateNodeFromSeed,
|
deriveHdPrivateNodeFromSeed,
|
||||||
|
deriveSeedFromBip39Mnemonic,
|
||||||
generateRandomBytes,
|
generateRandomBytes,
|
||||||
} from '@bitauth/libauth';
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
@@ -26,38 +27,16 @@ export class HDPrivateNode {
|
|||||||
/**
|
/**
|
||||||
* Construct a new HD Private Node.
|
* Construct a new HD Private Node.
|
||||||
*
|
*
|
||||||
* @param node {HdPrivateNodeValid} The HD Private Node.
|
* @param node The HD Private Node.
|
||||||
*/
|
*/
|
||||||
constructor(node: HdPrivateNodeValid) {
|
constructor(node: HdPrivateNodeValid) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a HD Private Node from the given XPriv Key.
|
|
||||||
*
|
|
||||||
* @param xpriv {string} The XPriv Key.
|
|
||||||
*
|
|
||||||
* @throws {Error} If HD Private Node cannot be created.
|
|
||||||
*
|
|
||||||
* @returns {HDPrivateNode} The created HD Private Node.
|
|
||||||
*/
|
|
||||||
public static fromXPriv(xpriv: string): HDPrivateNode {
|
|
||||||
// Attempt to decode the XPriv Key.
|
|
||||||
const decodeResult = decodeHdPrivateKey(xpriv);
|
|
||||||
|
|
||||||
// If a string is returned, this indicates an error...
|
|
||||||
if (typeof decodeResult === 'string') {
|
|
||||||
throw new Error(decodeResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a new HD Private Node from the given XPriv.
|
|
||||||
return new HDPrivateNode(decodeResult.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a HD Private Node from a raw seed.
|
* Creates a HD Private Node from a raw seed.
|
||||||
*
|
*
|
||||||
* @param seed {Uint8Array} The raw seed bytes.
|
* @param seed The raw seed bytes.
|
||||||
*
|
*
|
||||||
* @throws {Error} If HD Private Node cannot be created.
|
* @throws {Error} If HD Private Node cannot be created.
|
||||||
*
|
*
|
||||||
@@ -83,6 +62,45 @@ export class HDPrivateNode {
|
|||||||
return new HDPrivateNode(deriveResult);
|
return new HDPrivateNode(deriveResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HD Private Node from the given mnemonic.
|
||||||
|
*
|
||||||
|
* @param mnemonic The mnemonic.
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Private Node cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The created HD Private Node.
|
||||||
|
*/
|
||||||
|
public static fromMnemonic(mnemonic: string): HDPrivateNode {
|
||||||
|
// Derive the seed from the mnemonic.
|
||||||
|
const seed = deriveSeedFromBip39Mnemonic(mnemonic);
|
||||||
|
|
||||||
|
// Return a new HDPrivateNode from the given seed.
|
||||||
|
return HDPrivateNode.fromSeed(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HD Private Node from the given XPriv Key.
|
||||||
|
*
|
||||||
|
* @param xpriv The XPriv Key.
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Private Node cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The created HD Private Node.
|
||||||
|
*/
|
||||||
|
public static fromXPriv(xpriv: string): HDPrivateNode {
|
||||||
|
// Attempt to decode the XPriv Key.
|
||||||
|
const decodeResult = decodeHdPrivateKey(xpriv);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new HD Private Node from the given XPriv.
|
||||||
|
return new HDPrivateNode(decodeResult.node);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a random HD Private Node.
|
* Generates a random HD Private Node.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ export class PrivateKey {
|
|||||||
this.bytes = bytes;
|
this.bytes = bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Private Key Entity from the given bytes.
|
||||||
|
*
|
||||||
|
* @param privateKeyBytes {string} Raw Private Key bytes.
|
||||||
|
*
|
||||||
|
* @throws {Error} If Private Key could not be created.
|
||||||
|
*
|
||||||
|
* @returns {PrivateKey} The created Private Key Entity.
|
||||||
|
*/
|
||||||
|
public static fromBytes(privateKeyBytes: Uint8Array): PrivateKey {
|
||||||
|
return new PrivateKey(privateKeyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Private Key Entity from the given bytes (as hexadecimal string).
|
* Create a Private Key Entity from the given bytes (as hexadecimal string).
|
||||||
*
|
*
|
||||||
@@ -232,7 +245,7 @@ export class PrivateKey {
|
|||||||
*
|
*
|
||||||
* NOTE: Electron can verify this message successfully, but generates them differently internally.
|
* NOTE: Electron can verify this message successfully, but generates them differently internally.
|
||||||
*
|
*
|
||||||
* @param messageHash {Uint8Array} The hash of the message.
|
* @param message The message.
|
||||||
*
|
*
|
||||||
* @throws {Error} If message hash cannot be signed.
|
* @throws {Error} If message hash cannot be signed.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -106,4 +106,25 @@ export class Transaction {
|
|||||||
public getLocktime() {
|
public getLocktime() {
|
||||||
return this.transaction.locktime;
|
return this.transaction.locktime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isCoinbase(): boolean {
|
||||||
|
// TODO: This definitely needs checking.
|
||||||
|
|
||||||
|
// A coinbase transaction MUST only have one input.
|
||||||
|
if (this.getInputs().length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the input's unlocking bytecode.
|
||||||
|
const unlockingBytecode = this.getInputs()[0].unlockingBytecode;
|
||||||
|
|
||||||
|
// Get the firt byte.
|
||||||
|
const firstByte = unlockingBytecode[0];
|
||||||
|
|
||||||
|
if (firstByte >= 0x01 && firstByte <= 0x4b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Blockchain, WalletHD } from '@xo/wallet';
|
import { HDPrivateNode } from '@xo/primitives';
|
||||||
|
import { IBlockchain, WalletHD } from '@xo/wallet';
|
||||||
import { WalletP2PKHVue, useWalletP2PKHVue } from './wallet-p2pkh-vue.js';
|
import { WalletP2PKHVue, useWalletP2PKHVue } from './wallet-p2pkh-vue.js';
|
||||||
|
|
||||||
import { computed, ref, shallowRef } from 'vue';
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
|
|
||||||
import { HdPrivateNodeValid } from '@bitauth/libauth';
|
|
||||||
|
|
||||||
export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
|
export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
|
||||||
// Reactive State.
|
// Reactive State.
|
||||||
public rShouldMonitor = ref(false);
|
public rShouldMonitor = ref(false);
|
||||||
@@ -14,13 +13,13 @@ export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
|
|||||||
public rBalance = computed(() => {
|
public rBalance = computed(() => {
|
||||||
return this.rWallets.value.reduce(
|
return this.rWallets.value.reduce(
|
||||||
(total, node) => total + node.rBalance.value,
|
(total, node) => total + node.rBalance.value,
|
||||||
0
|
0n
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
node: HdPrivateNodeValid,
|
node: HDPrivateNode,
|
||||||
electrum: Blockchain,
|
electrum: IBlockchain,
|
||||||
walletFactory = useWalletP2PKHVue
|
walletFactory = useWalletP2PKHVue
|
||||||
) {
|
) {
|
||||||
super(node, electrum, walletFactory);
|
super(node, electrum, walletFactory);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Transaction } from '@xo/primitives';
|
import { PrivateKey, Transaction } from '@xo/primitives';
|
||||||
import { type AddressListUnspent, Blockchain, WalletP2PKH } from '@xo/wallet';
|
import { type IBlockchain, type UTXO, WalletP2PKH } from '@xo/wallet';
|
||||||
|
|
||||||
import { computed, shallowRef } from 'vue';
|
import { computed, shallowRef } from 'vue';
|
||||||
/**
|
/**
|
||||||
@@ -15,19 +15,19 @@ import { computed, shallowRef } from 'vue';
|
|||||||
*/
|
*/
|
||||||
export class WalletP2PKHVue extends WalletP2PKH {
|
export class WalletP2PKHVue extends WalletP2PKH {
|
||||||
// Reactive State.
|
// Reactive State.
|
||||||
public rUnspents = shallowRef<AddressListUnspent['response']>([]);
|
public rUnspents = shallowRef<Array<UTXO>>([]);
|
||||||
public rTransactions = shallowRef<Array<Transaction>>([]);
|
public rTransactions = shallowRef<Array<Transaction>>([]);
|
||||||
|
|
||||||
// Computeds.
|
// Computeds.
|
||||||
public rBalance = computed(() => {
|
public rBalance = computed(() => {
|
||||||
return this.rUnspents.value.reduce(
|
return this.rUnspents.value.reduce(
|
||||||
(total, unspent) => total + unspent.value,
|
(total, unspent) => total + unspent.valueSatoshis,
|
||||||
0
|
0n
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
|
constructor(privateKey: PrivateKey, electrum: IBlockchain) {
|
||||||
super(privateKeyBytes, electrum);
|
super(privateKey, electrum);
|
||||||
|
|
||||||
this.events.on('unspentsUpdated', (unspents) => {
|
this.events.on('unspentsUpdated', (unspents) => {
|
||||||
this.rUnspents.value = unspents;
|
this.rUnspents.value = unspents;
|
||||||
@@ -40,8 +40,8 @@ export class WalletP2PKHVue extends WalletP2PKH {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useWalletP2PKHVue(
|
export function useWalletP2PKHVue(
|
||||||
privateKeyBytes: Uint8Array,
|
privateKey: PrivateKey,
|
||||||
electrum: Blockchain
|
electrum: IBlockchain
|
||||||
): WalletP2PKHVue {
|
): WalletP2PKHVue {
|
||||||
return new WalletP2PKHVue(privateKeyBytes, electrum);
|
return new WalletP2PKHVue(privateKey, electrum);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electrum-cash/network": "^3.3.0",
|
"@electrum-cash/network": "^3.3.0",
|
||||||
"@xo/primitives": "^0.0.1"
|
"@xo/primitives": "^0.0.1",
|
||||||
|
"idb-keyval": "^6.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -129,3 +129,14 @@ export interface AddressNotification {
|
|||||||
// [Address, Status]
|
// [Address, Status]
|
||||||
params: [string, string | null];
|
params: [string, string | null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransactionNotification {
|
||||||
|
jsonrpc: '2.0';
|
||||||
|
method: 'blockchain.transaction.subscribe';
|
||||||
|
// [TransactionHash, Blockheight]
|
||||||
|
params: [txHash: string, blockheight: number | null];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ElectrumNotification =
|
||||||
|
| AddressNotification
|
||||||
|
| TransactionNotification;
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import type {
|
||||||
|
IBlockchain,
|
||||||
|
AddressCallback,
|
||||||
|
TransactionCallback,
|
||||||
|
UTXO,
|
||||||
|
} from './interface.js';
|
||||||
|
import {
|
||||||
|
ElectrumNotification,
|
||||||
|
ElectrumRequest,
|
||||||
|
AddressGetHistory,
|
||||||
|
AddressListUnspent,
|
||||||
|
TransactionGet,
|
||||||
|
} from './blockchain-electrum-types.js';
|
||||||
|
|
||||||
|
import { Address, Transaction } from '@xo/primitives';
|
||||||
|
import { ElectrumClient } from '@electrum-cash/network';
|
||||||
|
import { hexToBin } from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export interface BroadcastOpts {
|
||||||
|
timeout: number;
|
||||||
|
waitUntilSeen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlockchainElectrum implements IBlockchain {
|
||||||
|
electrumClient!: ElectrumClient;
|
||||||
|
|
||||||
|
// Address subscriptions.
|
||||||
|
addressSubscriptions: { [address: string]: AddressCallback } = {};
|
||||||
|
transactionSubscriptions: { [txHash: string]: TransactionCallback } = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly servers: string[],
|
||||||
|
public readonly application = 'BCHApp',
|
||||||
|
public readonly version = '1.5'
|
||||||
|
) {}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// See: https://gitlab.com/GeneralProtocols/anyhedge/whitelabel/frontend/-/issues/76
|
||||||
|
|
||||||
|
// Iterate over each server in our list of servers.
|
||||||
|
// NOTE: We are still using an Electrum Client due to the above bug.
|
||||||
|
// But, if we cannot connect to the first server, we will try the second (and third if given, etc) as a fallback.
|
||||||
|
for (const electrumServer of this.servers) {
|
||||||
|
// Initialize an Electrum client using the given server.
|
||||||
|
const electrum = new ElectrumClient(
|
||||||
|
this.application,
|
||||||
|
this.version,
|
||||||
|
electrumServer,
|
||||||
|
50004,
|
||||||
|
'wss'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attempt to connect to this Electrum Server.
|
||||||
|
try {
|
||||||
|
await electrum.connect();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
|
||||||
|
// If it fails to connect, try the next server in our list.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the client.
|
||||||
|
this.electrumClient = electrum;
|
||||||
|
|
||||||
|
// Setup notification handler.
|
||||||
|
this.electrumClient.on('notification', this.onNotification.bind(this));
|
||||||
|
|
||||||
|
// Return to prevent further execution.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw an error if we failed to connect to any of our Electrum Servers.
|
||||||
|
throw new Error(
|
||||||
|
'Failed to connect to Electrum. Please try restarting your browser and ensure you are currently connected to the internet.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
// Disconnect from Electrum.
|
||||||
|
this.electrumClient.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUnspents(address: string): Promise<UTXO[]> {
|
||||||
|
// Fetch unspents for the given address from our Electrum server(s).
|
||||||
|
const unspentOutputs = await this.request<AddressListUnspent>(
|
||||||
|
'blockchain.address.listunspent',
|
||||||
|
address,
|
||||||
|
'include_tokens'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Work out the locking byecode from the address.
|
||||||
|
const lockingBytecode =
|
||||||
|
Address.fromCashAddrOrLegacy(address).toLockscriptBytes();
|
||||||
|
|
||||||
|
// Transform the unspents into our UTXO format.
|
||||||
|
const utxos = unspentOutputs.map((unspent) => ({
|
||||||
|
outpointTransactionHash: hexToBin(unspent.tx_hash),
|
||||||
|
outpointIndex: unspent.tx_pos,
|
||||||
|
valueSatoshis: BigInt(unspent.value),
|
||||||
|
token: unspent.token_data
|
||||||
|
? {
|
||||||
|
amount: BigInt(unspent.token_data.amount),
|
||||||
|
category: hexToBin(unspent.token_data.category),
|
||||||
|
nft: unspent.token_data.nft
|
||||||
|
? {
|
||||||
|
capability: unspent.token_data.nft.capability,
|
||||||
|
commitment: hexToBin(unspent.token_data.nft.commitment),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
lockingBytecode,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Return the unspents.
|
||||||
|
return utxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTransactions(address: string): Promise<Transaction[]> {
|
||||||
|
// Get the history of the address from our Electrum server(s).
|
||||||
|
const history = await this.request<AddressGetHistory>(
|
||||||
|
'blockchain.address.get_history',
|
||||||
|
address,
|
||||||
|
0,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the transactions for each history item.
|
||||||
|
const transactions = await Promise.all(
|
||||||
|
history.map((item) =>
|
||||||
|
this.request<TransactionGet>(
|
||||||
|
'blockchain.transaction.get',
|
||||||
|
item.tx_hash,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decode the transactions.
|
||||||
|
const decodedTransactions = transactions.map((tx) =>
|
||||||
|
Transaction.fromHex(tx)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the transactions.
|
||||||
|
return decodedTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: async broadcast() method
|
||||||
|
// This should accept options: { waitForSeen: boolean }
|
||||||
|
async broadcastTransaction(
|
||||||
|
tx: Uint8Array | Transaction | string,
|
||||||
|
opts: BroadcastOpts = { timeout: 10_000, waitUntilSeen: true }
|
||||||
|
) {
|
||||||
|
// Cast the transaction.
|
||||||
|
if (!(tx instanceof Transaction)) {
|
||||||
|
tx = Transaction.fromAny(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to broadcast the transaction.
|
||||||
|
await new Promise<void>(async (resolve, reject) => {
|
||||||
|
// Set a timeout.
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error(`Failed to broadcast ${tx.toHashHex()}`));
|
||||||
|
}, opts.timeout);
|
||||||
|
|
||||||
|
// Create a subscribe function so that we get notified when transaction is seen by network
|
||||||
|
const subscribeCallback = (_height: number | null) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.unsubscribeTransaction(tx.toHashHex(), subscribeCallback);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to the transaction.
|
||||||
|
await this.subscribeTransaction(tx.toHashHex(), subscribeCallback);
|
||||||
|
|
||||||
|
// Broadcast the transaction to our Electrum node.
|
||||||
|
await this.request('blockchain.transaction.broadcast', tx.toHex());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeAddress(address: string, callback: AddressCallback) {
|
||||||
|
await this.electrumClient.subscribe(
|
||||||
|
'blockchain.address.subscribe',
|
||||||
|
address
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addressSubscriptions[address] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribeAddress(address: string, _callback: AddressCallback) {
|
||||||
|
if (!this.addressSubscriptions[address]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.addressSubscriptions[address];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.electrumClient.unsubscribe(
|
||||||
|
'blockchain.address.unsubscribe',
|
||||||
|
address
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeTransaction(
|
||||||
|
transactionHash: string,
|
||||||
|
callback: TransactionCallback
|
||||||
|
) {
|
||||||
|
await this.electrumClient.subscribe(
|
||||||
|
'blockchain.transaction.subscribe',
|
||||||
|
transactionHash
|
||||||
|
);
|
||||||
|
|
||||||
|
this.transactionSubscriptions[transactionHash] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribeTransaction(
|
||||||
|
transactionHash: string,
|
||||||
|
_callback: TransactionCallback
|
||||||
|
) {
|
||||||
|
if (!this.transactionSubscriptions[transactionHash]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.transactionSubscriptions[transactionHash];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.electrumClient.unsubscribe(
|
||||||
|
'blockchain.transaction.unsubscribe',
|
||||||
|
transactionHash
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async request<T extends ElectrumRequest>(
|
||||||
|
endpoint: T['method'],
|
||||||
|
...params: T['params']
|
||||||
|
): Promise<T['response']> {
|
||||||
|
const response = await this.electrumClient.request(endpoint, ...params);
|
||||||
|
|
||||||
|
if (response instanceof Error) {
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onNotification(data: ElectrumNotification) {
|
||||||
|
// Handle Address notification.
|
||||||
|
if (data.method === 'blockchain.address.subscribe') {
|
||||||
|
const address = data.params[0];
|
||||||
|
const status = data.params[1];
|
||||||
|
const subscription = this.addressSubscriptions[address];
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
console.warn(
|
||||||
|
`Notification for address ${address} subscribed to, but has no handler`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the subscription's callback.
|
||||||
|
subscription(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Transaction Notification.
|
||||||
|
if (data.method === 'blockchain.transaction.subscribe') {
|
||||||
|
const txHash = data.params[0];
|
||||||
|
const height = data.params[1];
|
||||||
|
const subscription = this.transactionSubscriptions[txHash];
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
console.warn(
|
||||||
|
`Notification for tx ${txHash} subscribed to, but has no handler`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the subscription's callback.
|
||||||
|
subscription(height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import type {
|
||||||
|
AddressCallback,
|
||||||
|
IBlockchain,
|
||||||
|
TransactionCallback,
|
||||||
|
UTXO,
|
||||||
|
} from './interface.js';
|
||||||
|
|
||||||
|
import { Address, Transaction } from '@xo/primitives';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type Output,
|
||||||
|
binToHex,
|
||||||
|
createVirtualMachineBCH,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export type UTXOSet = { [outpointHashAndIndex: string]: UTXO };
|
||||||
|
|
||||||
|
// Local Blockchain Adapter.
|
||||||
|
// This simulates the BCH Blockchain.
|
||||||
|
export class BlockchainLocal implements IBlockchain {
|
||||||
|
private transactions: Map<string, Transaction>;
|
||||||
|
private addressSubscriptions: Map<string, Set<AddressCallback>>;
|
||||||
|
private transactionSubscriptions: Map<string, Set<TransactionCallback>>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.transactions = new Map();
|
||||||
|
this.addressSubscriptions = new Map();
|
||||||
|
this.transactionSubscriptions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
console.log('BlockchainLocal: Started');
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
console.log('BlockchainLocal: Stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUnspents(address: string): Promise<UTXO[]> {
|
||||||
|
// Derive the current UTXO set.
|
||||||
|
const utxoSet = this.deriveUtxoSet();
|
||||||
|
|
||||||
|
// Filter for only UTXOs matching the given address.
|
||||||
|
return Object.values(utxoSet).filter((utxo) => {
|
||||||
|
// Decode the address and locking bytecode.
|
||||||
|
const utxoAddress = Address.fromLockscriptBytes(utxo.lockingBytecode);
|
||||||
|
const needleAddress = Address.fromCashAddrOrLegacy(address);
|
||||||
|
|
||||||
|
// Compare the lockscript hex.
|
||||||
|
return utxoAddress.toLockscriptHex() === needleAddress.toLockscriptHex();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTransactions(address: string): Promise<Transaction[]> {
|
||||||
|
return this.getAllTransactions().filter((tx) => {
|
||||||
|
// Check if any input or output involves the address
|
||||||
|
// This is a simplified check and might need to be more sophisticated in a real implementation
|
||||||
|
return (
|
||||||
|
tx
|
||||||
|
.getInputs()
|
||||||
|
.some((input) =>
|
||||||
|
binToHex(input.unlockingBytecode).includes(address)
|
||||||
|
) ||
|
||||||
|
tx
|
||||||
|
.getOutputs()
|
||||||
|
.some((output) => binToHex(output.lockingBytecode).includes(address))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async broadcastTransaction(
|
||||||
|
tx: Transaction | Uint8Array | string,
|
||||||
|
opts: { skipValidation: boolean } = { skipValidation: false }
|
||||||
|
) {
|
||||||
|
// Cast to transaction object.
|
||||||
|
if (!(tx instanceof Transaction)) {
|
||||||
|
tx = Transaction.fromAny(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive the UTXO set so that we can find source outputs.
|
||||||
|
const utxoSet = this.deriveUtxoSet();
|
||||||
|
|
||||||
|
if (!opts.skipValidation) {
|
||||||
|
console.log('utxoSet', utxoSet);
|
||||||
|
|
||||||
|
// Find the source outputs from our UTXO set so that we can make sure the transaction is valid.
|
||||||
|
const sourceOutputs = tx.getInputs().map((input) => {
|
||||||
|
const utxoId = `${binToHex(input.outpointTransactionHash)}:${
|
||||||
|
input.outpointIndex
|
||||||
|
}`;
|
||||||
|
|
||||||
|
console.log(utxoId);
|
||||||
|
|
||||||
|
return utxoSet[utxoId];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('sourceOutputs', sourceOutputs);
|
||||||
|
|
||||||
|
// Validate the transaction.
|
||||||
|
BlockchainLocal.validateTransaction(tx, sourceOutputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the transaction.
|
||||||
|
this.addTransaction(tx);
|
||||||
|
|
||||||
|
// Notify our address and transaction subscribers.
|
||||||
|
this.notifyAddressSubscribers(tx);
|
||||||
|
this.notifyTransactionSubscribers(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeAddress(address: string, callback: AddressCallback) {
|
||||||
|
// Convert the address to it's lockscript hex so that it's consistently represented.
|
||||||
|
const lockscriptHex =
|
||||||
|
Address.fromCashAddrOrLegacy(address).toLockscriptHex();
|
||||||
|
|
||||||
|
// If no subscriptions exist yet on this lockscript, create a new Set.
|
||||||
|
if (!this.addressSubscriptions.has(lockscriptHex)) {
|
||||||
|
this.addressSubscriptions.set(lockscriptHex, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the address notification callback.
|
||||||
|
this.addressSubscriptions.get(address)!.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribeAddress(address: string, callback: AddressCallback) {
|
||||||
|
// Convert the address to it's lockscript hex so that it's consistently represented.
|
||||||
|
const lockscriptHex =
|
||||||
|
Address.fromCashAddrOrLegacy(address).toLockscriptHex();
|
||||||
|
|
||||||
|
// Find all callbacks this lockscript is subscribed to.
|
||||||
|
const callbacks = this.addressSubscriptions.get(lockscriptHex);
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
this.addressSubscriptions.delete(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeTransaction(
|
||||||
|
transactionHash: string,
|
||||||
|
callback: TransactionCallback
|
||||||
|
) {
|
||||||
|
if (!this.transactionSubscriptions.has(transactionHash)) {
|
||||||
|
this.transactionSubscriptions.set(transactionHash, new Set());
|
||||||
|
}
|
||||||
|
this.transactionSubscriptions.get(transactionHash)!.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribeTransaction(
|
||||||
|
transactionHash: string,
|
||||||
|
callback: TransactionCallback
|
||||||
|
) {
|
||||||
|
const callbacks = this.transactionSubscriptions.get(transactionHash);
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
this.transactionSubscriptions.delete(transactionHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyAddressSubscribers(tx: Transaction) {
|
||||||
|
// Derive the UTXO state so that we can lookup source outputs.
|
||||||
|
const utxoState = this.deriveUtxoSet();
|
||||||
|
|
||||||
|
// First, notify all source output listeners.
|
||||||
|
tx.getInputs().forEach((input) => {
|
||||||
|
const sourceOutput =
|
||||||
|
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.
|
||||||
|
if (!sourceOutput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = Address.fromLockscriptBytes(
|
||||||
|
sourceOutput.lockingBytecode
|
||||||
|
).toLockscriptHex();
|
||||||
|
|
||||||
|
const callbacks = this.addressSubscriptions.get(address);
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach((callback) => callback('updated'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then notify all output listeners.
|
||||||
|
tx.getOutputs().forEach((output) => {
|
||||||
|
const address = Address.fromLockscriptBytes(
|
||||||
|
output.lockingBytecode
|
||||||
|
).toLockscriptHex();
|
||||||
|
|
||||||
|
const callbacks = this.addressSubscriptions.get(address);
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach((callback) => callback('updated'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyTransactionSubscribers(tx: Transaction) {
|
||||||
|
const txHash = tx.toHashHex();
|
||||||
|
const callbacks = this.transactionSubscriptions.get(txHash);
|
||||||
|
if (callbacks) {
|
||||||
|
// In this local simulation, we're not tracking block height, so we'll use null
|
||||||
|
callbacks.forEach((callback) => callback(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTransaction(tx: Transaction) {
|
||||||
|
this.transactions.set(tx.toHashHex(), tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTransaction(txHash: string) {
|
||||||
|
this.transactions.delete(txHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDependentTransactions(txHash: string): string[] {
|
||||||
|
const removedTxids: string[] = [];
|
||||||
|
const toRemove: Set<string> = new Set([txHash]);
|
||||||
|
|
||||||
|
const findDependents = (id: string) => {
|
||||||
|
for (const [otherTxid, tx] of this.transactions) {
|
||||||
|
if (
|
||||||
|
tx
|
||||||
|
.getInputs()
|
||||||
|
.some((input) => binToHex(input.outpointTransactionHash) === id)
|
||||||
|
) {
|
||||||
|
toRemove.add(otherTxid);
|
||||||
|
findDependents(otherTxid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findDependents(txHash);
|
||||||
|
|
||||||
|
for (const id of toRemove) {
|
||||||
|
if (this.transactions.has(id)) {
|
||||||
|
this.transactions.delete(id);
|
||||||
|
removedTxids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removedTxids;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTransaction(txHash: string): Transaction | undefined {
|
||||||
|
return this.transactions.get(txHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllTransactions(): Transaction[] {
|
||||||
|
return Array.from(this.transactions.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveUtxoSet(): UTXOSet {
|
||||||
|
const utxoMap: UTXOSet = {};
|
||||||
|
|
||||||
|
// First, add all outputs as potential UTXOs
|
||||||
|
for (const tx of this.transactions.values()) {
|
||||||
|
const txHash = tx.toHash();
|
||||||
|
|
||||||
|
tx.getOutputs().forEach((output, outpointIndex) => {
|
||||||
|
const utxo: UTXO = {
|
||||||
|
outpointTransactionHash: txHash.toBytes(),
|
||||||
|
outpointIndex,
|
||||||
|
valueSatoshis: output.valueSatoshis,
|
||||||
|
lockingBytecode: output.lockingBytecode,
|
||||||
|
};
|
||||||
|
utxoMap[`${txHash.toHex()}:${outpointIndex}`] = utxo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, remove all spent outputs
|
||||||
|
for (const tx of this.transactions.values()) {
|
||||||
|
tx.getInputs().forEach((input) => {
|
||||||
|
const outpointTransactionHashHex = binToHex(
|
||||||
|
input.outpointTransactionHash
|
||||||
|
);
|
||||||
|
|
||||||
|
delete utxoMap[`${outpointTransactionHashHex}:${input.outpointIndex}`];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return utxoMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDependentTransactions(txHash: string): Transaction[] {
|
||||||
|
return this.getAllTransactions().filter((tx) => {
|
||||||
|
tx.getInputs().some((input) => {
|
||||||
|
const outpointTransactionHashHex = binToHex(
|
||||||
|
input.outpointTransactionHash
|
||||||
|
);
|
||||||
|
return outpointTransactionHashHex === txHash;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBroadcastOrder(): Transaction[] {
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const order: Transaction[] = [];
|
||||||
|
|
||||||
|
const visit = (txid: string) => {
|
||||||
|
if (visited.has(txid)) return;
|
||||||
|
|
||||||
|
const tx = this.transactions.get(txid);
|
||||||
|
if (!tx) return;
|
||||||
|
|
||||||
|
visited.add(txid);
|
||||||
|
|
||||||
|
// First, visit all transactions that this transaction depends on
|
||||||
|
for (const input of tx.getInputs()) {
|
||||||
|
const outpointTransactionHashHex = binToHex(
|
||||||
|
input.outpointTransactionHash
|
||||||
|
);
|
||||||
|
visit(outpointTransactionHashHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
order.push(tx);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the topological sort from each transaction
|
||||||
|
for (const tx of this.transactions.values()) {
|
||||||
|
visit(tx.toHashHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Consider moving this to Transaction Primitive. Trade-off is that it's probably big. Maybe standalone util?
|
||||||
|
*/
|
||||||
|
static validateTransaction(tx: Transaction, sourceOutputs: Array<Output>) {
|
||||||
|
console.log(tx);
|
||||||
|
console.log(sourceOutputs);
|
||||||
|
|
||||||
|
const virtualMachine = createVirtualMachineBCH();
|
||||||
|
const virtualMachineResult = virtualMachine.verify({
|
||||||
|
transaction: {
|
||||||
|
version: tx.getVersion(),
|
||||||
|
locktime: tx.getLocktime(),
|
||||||
|
inputs: tx.getInputs(),
|
||||||
|
outputs: tx.getOutputs(),
|
||||||
|
},
|
||||||
|
sourceOutputs,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If simulation of the transaction fails...
|
||||||
|
if (typeof virtualMachineResult === 'string') {
|
||||||
|
throw new Error(virtualMachineResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* So, if the network is not available, it can fallback to the local and sync later.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Actual interface will be more complex, but this is the rough idea.
|
||||||
|
* const blockchain = new Blockchain([
|
||||||
|
* new BlockchainLocal(...),
|
||||||
|
* new BlockchainElectrum(...)
|
||||||
|
* ])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Blockchain {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './blockchain.js';
|
||||||
|
export * from './blockchain-electrum.js';
|
||||||
|
export * from './blockchain-electrum-types.js';
|
||||||
|
export * from './blockchain-local.js';
|
||||||
|
export * from './interface.js';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Transaction } from '@xo/primitives';
|
||||||
|
|
||||||
|
import type { Output } from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export type AddressCallback = (status: string | null) => void;
|
||||||
|
export type TransactionCallback = (height: number | null) => void;
|
||||||
|
|
||||||
|
export interface UTXO extends Output {
|
||||||
|
outpointTransactionHash: Uint8Array;
|
||||||
|
outpointIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBlockchain {
|
||||||
|
start: () => Promise<void>;
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
|
||||||
|
fetchUnspents: (address: string) => Promise<UTXO[]>;
|
||||||
|
fetchTransactions: (address: string) => Promise<Array<Transaction>>;
|
||||||
|
broadcastTransaction: (transaction: Uint8Array) => Promise<void>;
|
||||||
|
|
||||||
|
subscribeAddress: (
|
||||||
|
address: string,
|
||||||
|
callback: AddressCallback
|
||||||
|
) => Promise<void>;
|
||||||
|
unsubscribeAddress: (
|
||||||
|
address: string,
|
||||||
|
callback: AddressCallback
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
subscribeTransaction: (
|
||||||
|
transactionHash: string,
|
||||||
|
callback: TransactionCallback
|
||||||
|
) => Promise<void>;
|
||||||
|
unsubscribeTransaction: (
|
||||||
|
transactionHash: string,
|
||||||
|
callback: TransactionCallback
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './blockchain/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';
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './interface.js';
|
||||||
|
export * from './storage.js';
|
||||||
|
export * from './storage-idb.js';
|
||||||
|
export * from './storage-memory.js';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface IStorage {
|
||||||
|
start: () => Promise<void>;
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
|
||||||
|
get: <T>(key: string) => Promise<T>;
|
||||||
|
set: <T>(key: string, value: T) => Promise<void>;
|
||||||
|
delete: (key: string) => Promise<void>;
|
||||||
|
clear: () => Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { type IStorage } from './interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
get as idbGet,
|
||||||
|
set as idbSet,
|
||||||
|
del as idbDel,
|
||||||
|
clear as idbClear,
|
||||||
|
} from 'idb-keyval';
|
||||||
|
|
||||||
|
export class StorageIDB implements IStorage {
|
||||||
|
async start() {}
|
||||||
|
|
||||||
|
async stop() {}
|
||||||
|
|
||||||
|
async get(key: string) {
|
||||||
|
return await idbGet(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T) {
|
||||||
|
await idbSet(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
idbDel(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
idbClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { type IStorage } from './interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exists for testing. It is not intended for actual use and will likely be removed.
|
||||||
|
*/
|
||||||
|
export class StorageMemory implements IStorage {
|
||||||
|
storage: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
async start() {}
|
||||||
|
|
||||||
|
async stop() {}
|
||||||
|
|
||||||
|
async get(key: string) {
|
||||||
|
return this.storage[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T) {
|
||||||
|
this.storage[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
delete this.storage[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
this.storage = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* The idea here is that this will implement the IStorage interface.
|
||||||
|
* But, it will allow the dev to specify multiple child IStorage interfaces for configuration.
|
||||||
|
* So, if the cloud storage is not available, the dev can specify behaviour (e.g. use quorum or just fail whatever operation was happening).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Actual interface will be more complex, but this is the rough idea.
|
||||||
|
* const storage = new Storage([
|
||||||
|
* new StorageIDB(...),
|
||||||
|
* new StorageSomeCloud(...)
|
||||||
|
* ])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Storage {}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export class CashRPCError extends Error {
|
||||||
|
constructor(message: string, public readonly details?: string) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
// Capture the stack trace (V8 engines only)
|
||||||
|
// This step ensures the stack trace is captured correctly
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, CashRPCError);
|
||||||
|
} else {
|
||||||
|
this.stack = new Error().stack;
|
||||||
|
console.error(this.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇️ because we are extending a built-in class
|
||||||
|
Object.setPrototypeOf(this, CashRPCError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import { Transaction, TransactionGraph } from '@xo/primitives';
|
|
||||||
import { AddressNotification, ElectrumRequest } from './blockchain-types.js';
|
|
||||||
|
|
||||||
import { ElectrumClient } from '@electrum-cash/network';
|
|
||||||
|
|
||||||
export type AddressCallback = (status: string | null) => void;
|
|
||||||
|
|
||||||
// TODO: Rename this to Blockcain Service and refactor such that it can store local state.
|
|
||||||
// For local state, we'll want to track a tree of dependencies locally.
|
|
||||||
// Might be able to copy whatever structure a proper node uses (DAG?).
|
|
||||||
export class Blockchain {
|
|
||||||
pendingTransactions: TransactionGraph = new TransactionGraph();
|
|
||||||
|
|
||||||
electrumClient!: ElectrumClient;
|
|
||||||
|
|
||||||
// Address subscriptions.
|
|
||||||
addressSubscriptions: { [address: string]: AddressCallback } = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public readonly servers: string[],
|
|
||||||
public readonly application = 'BCHApp',
|
|
||||||
public readonly version = '1.5'
|
|
||||||
) {}
|
|
||||||
|
|
||||||
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.
|
|
||||||
// See: https://gitlab.com/GeneralProtocols/anyhedge/whitelabel/frontend/-/issues/76
|
|
||||||
|
|
||||||
// Iterate over each server in our list of servers.
|
|
||||||
// NOTE: We are still using an Electrum Client due to the above bug.
|
|
||||||
// But, if we cannot connect to the first server, we will try the second (and third if given, etc) as a fallback.
|
|
||||||
for (const electrumServer of this.servers) {
|
|
||||||
// Initialize an Electrum client using the given server.
|
|
||||||
const electrum = new ElectrumClient(
|
|
||||||
this.application,
|
|
||||||
this.version,
|
|
||||||
electrumServer,
|
|
||||||
50004,
|
|
||||||
'wss'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attempt to connect to this Electrum Server.
|
|
||||||
try {
|
|
||||||
await electrum.connect();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error);
|
|
||||||
|
|
||||||
// If it fails to connect, try the next server in our list.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the client.
|
|
||||||
this.electrumClient = electrum;
|
|
||||||
|
|
||||||
// Setup notification handler.
|
|
||||||
this.electrumClient.on('notification', this.onNotification.bind(this));
|
|
||||||
|
|
||||||
// Return to prevent further execution.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throw an error if we failed to connect to any of our Electrum Servers.
|
|
||||||
throw new Error(
|
|
||||||
'Failed to connect to Electrum. Please try restarting your browser and ensure you are currently connected to the internet.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async request<T extends ElectrumRequest>(
|
|
||||||
endpoint: T['method'],
|
|
||||||
...params: T['params']
|
|
||||||
): Promise<T['response']> {
|
|
||||||
const response = await this.electrumClient.request(endpoint, ...params);
|
|
||||||
|
|
||||||
if (response instanceof Error) {
|
|
||||||
throw response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This won't actually broadcast, this will just commit the transaction to our graph.
|
|
||||||
* The reason we want this is because, for protos like BIP70, we want service to verify the tx and broadcast it.
|
|
||||||
* And we just want to be able to monitor whether service did the needful (broadcasted it).
|
|
||||||
* This should accept options: { waitForSeen: boolean }
|
|
||||||
*/
|
|
||||||
async commitTransaction(
|
|
||||||
transaction: Uint8Array | Transaction | string,
|
|
||||||
opts: { waitUntilSeen: boolean } = { waitUntilSeen: true }
|
|
||||||
) {
|
|
||||||
if (!(transaction instanceof Transaction)) {
|
|
||||||
transaction = Transaction.fromAny(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingTransactions.addTransaction(transaction);
|
|
||||||
|
|
||||||
// TODO: Wait for transaction to be seen on network.
|
|
||||||
console.log(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: async broadcast() method
|
|
||||||
// This should accept options: { waitForSeen: boolean }
|
|
||||||
async broadcastTransaction(
|
|
||||||
transaction: Uint8Array | Transaction | string,
|
|
||||||
opts: { waitUntilSeen: boolean } = { waitUntilSeen: true }
|
|
||||||
) {
|
|
||||||
if (!(transaction instanceof Transaction)) {
|
|
||||||
transaction = Transaction.fromAny(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingTransactions.addTransaction(transaction);
|
|
||||||
|
|
||||||
// Broadcast any pending transaction.
|
|
||||||
// TODO: Use while loop instead.
|
|
||||||
for (let tx of this.pendingTransactions.getBroadcastOrder()) {
|
|
||||||
try {
|
|
||||||
console.log('broadcasting', tx);
|
|
||||||
|
|
||||||
this.pendingTransactions.removeTransaction(tx.toHashHex());
|
|
||||||
} catch (error) {
|
|
||||||
// TODO: Set these as a variable and then throw an ExtError.
|
|
||||||
this.pendingTransactions.removeDependentTransactions(tx.toHashHex());
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Broadcast failed for ${transaction.toHashHex()}. Removing dependent transactions.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Wait for transaction to be seen on network.
|
|
||||||
console.log(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
async subscribeAddress(address: string, callback: AddressCallback) {
|
|
||||||
await this.electrumClient.subscribe(
|
|
||||||
'blockchain.address.subscribe',
|
|
||||||
address
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addressSubscriptions[address] = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
async unsubscribeAddress(address: string, _callback: AddressCallback) {
|
|
||||||
if (!this.addressSubscriptions[address]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.addressSubscriptions[address];
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.electrumClient.unsubscribe(
|
|
||||||
'blockchain.address.unsubscribe',
|
|
||||||
address
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onNotification(data: AddressNotification) {
|
|
||||||
// Handle Address notification.
|
|
||||||
if (data.method === 'blockchain.address.subscribe') {
|
|
||||||
const address = data.params[0];
|
|
||||||
const status = data.params[1];
|
|
||||||
const subscription = this.addressSubscriptions[address];
|
|
||||||
|
|
||||||
if (!subscription) {
|
|
||||||
console.warn(
|
|
||||||
`Notification for address ${address} subscribed to, but has no handler`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger the subscription's callback.
|
|
||||||
subscription(status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Blockchain } from './blockchain.js';
|
|
||||||
|
|
||||||
export type ServiceConfig<BlockchainType, WalletType> = {
|
|
||||||
createBlockchain: () => BlockchainType;
|
|
||||||
createWallet: (
|
|
||||||
privateKeyBytes: Uint8Array,
|
|
||||||
electrum: Blockchain
|
|
||||||
) => WalletType;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
export * from './blockchain.js';
|
|
||||||
export * from './blockchain-types.js';
|
|
||||||
export * from './storage.js';
|
|
||||||
export * from './wallet-hd.js';
|
export * from './wallet-hd.js';
|
||||||
export * from './wallet-p2pkh.js';
|
export * from './wallet-p2pkh.js';
|
||||||
export * from './wallet-p2sh20.js';
|
export * from './wallet-p2sh20.js';
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Transaction } from '@xo/primitives';
|
||||||
|
import { type UTXO } from '../blockchain/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type InputTemplate,
|
||||||
|
CompilerBCH,
|
||||||
|
CompilationData,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export type InputTemplateBCH = InputTemplate<
|
||||||
|
CompilerBCH,
|
||||||
|
false,
|
||||||
|
CompilationData
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface IWallet {
|
||||||
|
/**
|
||||||
|
* Start monitoring this wallet for any transaction activity.
|
||||||
|
*/
|
||||||
|
start: () => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop monitoring this wallet for any transaction activity.
|
||||||
|
*/
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any event listeners from this wallet.
|
||||||
|
*/
|
||||||
|
destroy: () => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh this wallet's state.
|
||||||
|
*/
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all transactions from the blockchain involving this wallet.
|
||||||
|
*/
|
||||||
|
fetchTransactions: () => Promise<Array<Transaction>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all unspents from the blockchain belonging to this wallet.
|
||||||
|
*/
|
||||||
|
fetchUnspents: () => Promise<Array<UTXO>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all unspents from the blockchain belonging to this wallet.
|
||||||
|
*
|
||||||
|
* @todo Probably remove me and just store unspents as this?
|
||||||
|
*/
|
||||||
|
fetchUnspentDirectives: () => Promise<Array<InputTemplateBCH>>;
|
||||||
|
|
||||||
|
// Individual wallets should be able to send shekels too.
|
||||||
|
// send: () => Promise<void>
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* TODO
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* Storage should support Adapters.
|
|
||||||
* We don't want to extend this - we want to compose it.
|
|
||||||
* Reason being that we want to support multiple storage adapters.
|
|
||||||
* And we should allow the possibility of having non-critical storage adapters fail (e.g. remote vs local).
|
|
||||||
*
|
|
||||||
* Additionally, consider how we'll store data for WalletSets.
|
|
||||||
* There will be child wallets. These will have their own data.
|
|
||||||
* And each child may then have its own wallet. And on and on.
|
|
||||||
* So, it's a tree hierarchy.
|
|
||||||
* We can probably emit a "storageUpdated" event that is registered by each parent wallet.
|
|
||||||
* The parent then "wraps" its own state into this and propagates it up to its parent.
|
|
||||||
* Then, when it reaches root, the storage adapter stashes all of the propagated data into actual storage.
|
|
||||||
*/
|
|
||||||
export class Storage {
|
|
||||||
constructor() {
|
|
||||||
console.log('TODO');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { type AddressListUnspent } from './blockchain-types.js';
|
import {
|
||||||
import { Blockchain } from './blockchain.js';
|
type IBlockchain,
|
||||||
|
type AddressListUnspent,
|
||||||
|
} from '../blockchain/index.js';
|
||||||
|
|
||||||
import { PrivateKey, Transaction } from '@xo/primitives';
|
import { PrivateKey, Transaction } from '@xo/primitives';
|
||||||
|
|
||||||
@@ -17,11 +19,11 @@ export type InputTemplateBCH = InputTemplate<
|
|||||||
|
|
||||||
export abstract class WalletBase extends PrivateKey {
|
export abstract class WalletBase extends PrivateKey {
|
||||||
// Dependencies/Services
|
// Dependencies/Services
|
||||||
public electrum: Blockchain;
|
public blockchain: IBlockchain;
|
||||||
|
|
||||||
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
|
constructor(privateKeyBytes: Uint8Array, blockchain: IBlockchain) {
|
||||||
super(privateKeyBytes);
|
super(privateKeyBytes);
|
||||||
this.electrum = electrum;
|
this.blockchain = blockchain;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract startMonitoring(): Promise<void>;
|
abstract startMonitoring(): Promise<void>;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
// Import the Stamp class to derive the private keys
|
// Import the Stamp class to derive the private keys
|
||||||
|
import { type IWallet } from './interface.js';
|
||||||
|
|
||||||
import { HDPrivateNode, Transaction } from '@xo/primitives';
|
import { HDPrivateNode, Transaction } from '@xo/primitives';
|
||||||
|
import { IBlockchain } from '../blockchain/index.js';
|
||||||
import { EventEmitter } from '../utils/index.js';
|
import { EventEmitter } from '../utils/index.js';
|
||||||
import { Blockchain } from './blockchain.js';
|
|
||||||
import {
|
import {
|
||||||
WalletP2PKH,
|
WalletP2PKH,
|
||||||
WalletP2PKHFactory,
|
WalletP2PKHFactory,
|
||||||
useWalletP2PKH,
|
useWalletP2PKH,
|
||||||
} from './wallet-p2pkh.js';
|
} from './wallet-p2pkh.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type HdPrivateNodeValid,
|
|
||||||
deriveSeedFromBip39Mnemonic,
|
deriveSeedFromBip39Mnemonic,
|
||||||
encodeTransaction,
|
encodeTransaction,
|
||||||
generateTransaction,
|
generateTransaction,
|
||||||
@@ -24,9 +24,9 @@ export type WalletHDEvents<WalletType> = {
|
|||||||
walletsUpdated: Array<WalletType>;
|
walletsUpdated: Array<WalletType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class WalletHD<
|
export class WalletHD<WalletType extends WalletP2PKH = WalletP2PKH>
|
||||||
WalletType extends WalletP2PKH = WalletP2PKH
|
implements IWallet
|
||||||
> extends HDPrivateNode {
|
{
|
||||||
// Events.
|
// Events.
|
||||||
public events: EventEmitter<WalletHDEvents<WalletType>> = new EventEmitter();
|
public events: EventEmitter<WalletHDEvents<WalletType>> = new EventEmitter();
|
||||||
|
|
||||||
@@ -35,22 +35,32 @@ export class WalletHD<
|
|||||||
public wallets: Array<WalletType> = [];
|
public wallets: Array<WalletType> = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public node: HdPrivateNodeValid,
|
public node: HDPrivateNode,
|
||||||
public electrum: Blockchain,
|
public blockchain: IBlockchain,
|
||||||
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH
|
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH
|
||||||
) {
|
) {
|
||||||
// Instantiate parent class.
|
|
||||||
super(node);
|
|
||||||
|
|
||||||
// Monitor the following properties and emit an event when they change.
|
// Monitor the following properties and emit an event when they change.
|
||||||
this.events.monitorProperty(this, 'shouldMonitor', 'shouldMonitorUpdated');
|
this.events.monitorProperty(this, 'shouldMonitor', 'shouldMonitorUpdated');
|
||||||
this.events.monitorProperty(this, 'wallets', 'walletsUpdated');
|
this.events.monitorProperty(this, 'wallets', 'walletsUpdated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromSeed<T extends WalletHD<any>>(
|
||||||
|
this: WalletHDConstructor<T>,
|
||||||
|
seed: Uint8Array,
|
||||||
|
blockchain: IBlockchain,
|
||||||
|
walletFactory?: WalletP2PKHFactory<any>
|
||||||
|
): T {
|
||||||
|
// Derive node from mnemonic.
|
||||||
|
const hdPrivateNode = HDPrivateNode.fromSeed(seed);
|
||||||
|
|
||||||
|
// Return a new instance of this class.
|
||||||
|
return new this(hdPrivateNode, blockchain, walletFactory);
|
||||||
|
}
|
||||||
|
|
||||||
static fromMnemonic<T extends WalletHD<any>>(
|
static fromMnemonic<T extends WalletHD<any>>(
|
||||||
this: WalletHDConstructor<T>,
|
this: WalletHDConstructor<T>,
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
electrum: Blockchain,
|
blockchain: IBlockchain,
|
||||||
walletFactory?: WalletP2PKHFactory<any>
|
walletFactory?: WalletP2PKHFactory<any>
|
||||||
): T {
|
): T {
|
||||||
// Derive the seed from the mnemonic.
|
// Derive the seed from the mnemonic.
|
||||||
@@ -60,46 +70,93 @@ export class WalletHD<
|
|||||||
const hdPrivateNode = HDPrivateNode.fromSeed(seed);
|
const hdPrivateNode = HDPrivateNode.fromSeed(seed);
|
||||||
|
|
||||||
// Return a new instance of this class.
|
// Return a new instance of this class.
|
||||||
return new this(hdPrivateNode.node, electrum, walletFactory);
|
return new this(hdPrivateNode, blockchain, walletFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromXPriv<T extends WalletHD<any>>(
|
||||||
|
this: WalletHDConstructor<T>,
|
||||||
|
xpriv: string,
|
||||||
|
blockchain: IBlockchain,
|
||||||
|
walletFactory?: WalletP2PKHFactory<any>
|
||||||
|
): T {
|
||||||
|
// Derive node from mnemonic.
|
||||||
|
const hdPrivateNode = HDPrivateNode.fromXPriv(xpriv);
|
||||||
|
|
||||||
|
// Return a new instance of this class.
|
||||||
|
return new this(hdPrivateNode, blockchain, walletFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
this.shouldMonitor = true;
|
||||||
|
|
||||||
|
await Promise.all(this.wallets.map((wallet) => wallet.start()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
this.shouldMonitor = false;
|
||||||
|
|
||||||
|
await Promise.all(this.wallets.map((wallet) => wallet.stop()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchTransactions() {
|
||||||
|
const transactions = await Promise.all(
|
||||||
|
this.wallets.map((wallet) => wallet.fetchTransactions())
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Remove duplicates.
|
||||||
|
|
||||||
|
return transactions.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUnspents() {
|
||||||
|
const unspents = await Promise.all(
|
||||||
|
this.wallets.map((wallet) => wallet.fetchUnspents())
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Remove duplicates.
|
||||||
|
|
||||||
|
return unspents.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUnspentDirectives() {
|
||||||
|
const unspentDirectives = await Promise.all(
|
||||||
|
this.wallets.map((wallet) => wallet.fetchUnspentDirectives())
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Remove duplicates.
|
||||||
|
|
||||||
|
return unspentDirectives.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
await Promise.all(this.wallets.map((node) => node.refresh()));
|
||||||
|
}
|
||||||
|
|
||||||
async setQuantity(quantity: number) {
|
async setQuantity(quantity: number) {
|
||||||
// Clear existing nodes.
|
// Clear existing nodes.
|
||||||
this.wallets = this.deriveWallets(quantity);
|
this.wallets = this.deriveWallets(quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
async startMonitoring() {
|
|
||||||
this.shouldMonitor = true;
|
|
||||||
|
|
||||||
await Promise.all(this.wallets.map((wallet) => wallet.startMonitoring()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopMonitoring() {
|
|
||||||
this.shouldMonitor = false;
|
|
||||||
|
|
||||||
await Promise.all(this.wallets.map((wallet) => wallet.stopMonitoring()));
|
|
||||||
}
|
|
||||||
|
|
||||||
deriveWallets(count: number, startIndex = 0) {
|
deriveWallets(count: number, startIndex = 0) {
|
||||||
// Create an array to store our wallets.
|
// Create an array to store our wallets.
|
||||||
const wallets: Array<WalletType> = [];
|
const wallets: Array<WalletType> = [];
|
||||||
|
|
||||||
// Create the given count of wallets.
|
// Create the given count of wallets.
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const childNode = this.derivePath(
|
const childNode = this.node.derivePath(
|
||||||
`${DERIVATION_PATH}/0/${startIndex + i}`
|
`${DERIVATION_PATH}/0/${startIndex + i}`
|
||||||
);
|
);
|
||||||
const wallet = this.walletFactory(
|
const wallet = this.walletFactory(
|
||||||
childNode.privateKey().toBytes(),
|
childNode.privateKey(),
|
||||||
this.electrum
|
this.blockchain
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.shouldMonitor) {
|
if (this.shouldMonitor) {
|
||||||
wallet.startMonitoring();
|
wallet.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
wallets.push(wallet);
|
wallets.push(wallet);
|
||||||
@@ -128,7 +185,7 @@ export class WalletHD<
|
|||||||
|
|
||||||
// Get the transaction history of each wallet in the batch
|
// Get the transaction history of each wallet in the batch
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
batch.map((wallet) => wallet.fetchHistory())
|
batch.map((wallet) => wallet.fetchTransactions())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process the results
|
// Process the results
|
||||||
@@ -156,14 +213,10 @@ export class WalletHD<
|
|||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshChildNodes() {
|
|
||||||
await Promise.all(this.wallets.map((node) => node.refresh()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async sweep(payoutBytecode: Uint8Array) {
|
async sweep(payoutBytecode: Uint8Array) {
|
||||||
// Get a list of inputs belonging to each wallet.
|
// Get a list of inputs belonging to each wallet.
|
||||||
const inputs = await Promise.all(
|
const inputs = await Promise.all(
|
||||||
this.wallets.map((wallet) => wallet.getUnspentDirectives())
|
this.wallets.map((wallet) => wallet.fetchUnspentDirectives())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Flatten the inputs.
|
// Flatten the inputs.
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import { Transaction } from '@xo/primitives';
|
import { type IWallet } from './interface.js';
|
||||||
|
|
||||||
|
import { PrivateKey, Transaction } from '@xo/primitives';
|
||||||
|
import { type IBlockchain, type UTXO } from '../blockchain/index.js';
|
||||||
import { EventEmitter } from '../utils/index.js';
|
import { EventEmitter } from '../utils/index.js';
|
||||||
import {
|
|
||||||
type AddressListUnspent,
|
|
||||||
type AddressGetHistory,
|
|
||||||
type TransactionGet,
|
|
||||||
} from './blockchain-types.js';
|
|
||||||
import { Blockchain } from './blockchain.js';
|
|
||||||
|
|
||||||
import { WalletBase } from './wallet-base.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hexToBin,
|
encodeTransaction,
|
||||||
|
generateTransaction,
|
||||||
|
getMinimumFee,
|
||||||
walletTemplateP2pkhNonHd,
|
walletTemplateP2pkhNonHd,
|
||||||
walletTemplateToCompilerBCH,
|
walletTemplateToCompilerBCH,
|
||||||
} from '@bitauth/libauth';
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
export type WalletP2PKHEvents = {
|
export type WalletP2PKHEvents = {
|
||||||
transactionsUpdated: Array<Transaction>;
|
transactionsUpdated: Array<Transaction>;
|
||||||
unspentsUpdated: AddressListUnspent['response'];
|
unspentsUpdated: Array<UTXO>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,124 +27,118 @@ export type WalletP2PKHEvents = {
|
|||||||
* await walletP2PKH.startMonitoring();
|
* await walletP2PKH.startMonitoring();
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class WalletP2PKH extends WalletBase {
|
export class WalletP2PKH implements IWallet {
|
||||||
// Events.
|
// Events.
|
||||||
public events: EventEmitter<WalletP2PKHEvents> = new EventEmitter();
|
public events: EventEmitter<WalletP2PKHEvents> = new EventEmitter();
|
||||||
|
|
||||||
// State.
|
// State.
|
||||||
public transactions: Array<Transaction> = [];
|
public transactions: Array<Transaction> = [];
|
||||||
public unspents: AddressListUnspent['response'] = [];
|
public unspents: Array<UTXO> = [];
|
||||||
|
|
||||||
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
|
|
||||||
super(privateKeyBytes, electrum);
|
|
||||||
|
|
||||||
|
constructor(public privateKey: PrivateKey, public blockchain: IBlockchain) {
|
||||||
// Monitor the following properties and emit an event when they change.
|
// Monitor the following properties and emit an event when they change.
|
||||||
this.events.monitorProperty(this, 'transactions', 'transactionsUpdated');
|
this.events.monitorProperty(this, 'transactions', 'transactionsUpdated');
|
||||||
this.events.monitorProperty(this, 'unspents', 'unspentsUpdated');
|
this.events.monitorProperty(this, 'unspents', 'unspentsUpdated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromBytes(privateKeyBytes: Uint8Array, blockchain: IBlockchain) {
|
||||||
|
// Create a Private Key entity from the provided bytes.
|
||||||
|
const privateKey = PrivateKey.fromBytes(privateKeyBytes);
|
||||||
|
|
||||||
|
// Return a new instance of Wallet P2PKH.
|
||||||
|
return new WalletP2PKH(privateKey, blockchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(privateKeyHex: string, blockchain: IBlockchain) {
|
||||||
|
// Create a Private Key entity from the provided bytes.
|
||||||
|
const privateKey = PrivateKey.fromHex(privateKeyHex);
|
||||||
|
|
||||||
|
// Return a new instance of Wallet P2PKH.
|
||||||
|
return new WalletP2PKH(privateKey, blockchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromWif(privateKeyWif: string, blockchain: IBlockchain) {
|
||||||
|
// Create a Private Key entity from the provided bytes.
|
||||||
|
const privateKey = PrivateKey.fromWIF(privateKeyWif);
|
||||||
|
|
||||||
|
// Return a new instance of Wallet P2PKH.
|
||||||
|
return new WalletP2PKH(privateKey, blockchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateRandom(blockchain: IBlockchain) {
|
||||||
|
// Create a Private Key entity from the provided bytes.
|
||||||
|
const privateKey = PrivateKey.generateRandom();
|
||||||
|
|
||||||
|
// Return a new instance of Wallet P2PKH.
|
||||||
|
return new WalletP2PKH(privateKey, blockchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await this.blockchain.subscribeAddress(
|
||||||
|
this.getAddress().toCashAddr(),
|
||||||
|
this.onAddressNotification.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
await this.blockchain.unsubscribeAddress(
|
||||||
|
this.getAddress().toCashAddr(),
|
||||||
|
this.onAddressNotification.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
async startMonitoring() {
|
async fetchTransactions() {
|
||||||
await this.electrum.subscribeAddress(
|
|
||||||
this.getAddress(),
|
|
||||||
this.onAddressNotification.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopMonitoring() {
|
|
||||||
await this.electrum.unsubscribeAddress(
|
|
||||||
this.getAddress(),
|
|
||||||
this.onAddressNotification.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAddress() {
|
|
||||||
return this.derivePublicKey().deriveAddress().toCashAddr();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchHistory() {
|
|
||||||
// Get this node's address.
|
// Get this node's address.
|
||||||
const address = this.getAddress();
|
const address = this.getAddress().toCashAddr();
|
||||||
|
|
||||||
// Get the history of the address from electrum
|
// Get transactions involving this address.
|
||||||
const history = await this.electrum.request<AddressGetHistory>(
|
const transactions = await this.blockchain.fetchTransactions(address);
|
||||||
'blockchain.address.get_history',
|
|
||||||
address,
|
|
||||||
0,
|
|
||||||
-1
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the transactions for each history item.
|
|
||||||
const transactions = await Promise.all(
|
|
||||||
history.map((item) =>
|
|
||||||
this.electrum.request<TransactionGet>(
|
|
||||||
'blockchain.transaction.get',
|
|
||||||
item.tx_hash,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decode the transactions.
|
|
||||||
const decodedTransactions = transactions.map((tx) =>
|
|
||||||
Transaction.fromHex(tx)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store the transactions.
|
// Store the transactions.
|
||||||
this.transactions = decodedTransactions;
|
this.transactions = transactions;
|
||||||
|
|
||||||
return decodedTransactions;
|
// Return the fetched transactions.
|
||||||
|
return transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchUnspentOutputs() {
|
async fetchUnspents() {
|
||||||
const address = this.getAddress();
|
// Get this wallet's address.
|
||||||
|
const address = this.getAddress().toCashAddr();
|
||||||
|
|
||||||
const unspentOutputs = await this.electrum.request<AddressListUnspent>(
|
// Fetch unspents belonging to this address.
|
||||||
'blockchain.address.listunspent',
|
const unspents = await this.blockchain.fetchUnspents(address);
|
||||||
address,
|
|
||||||
'include_tokens'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Emit event with unspent outputs.
|
// Store the unspents.
|
||||||
this.unspents = unspentOutputs;
|
this.unspents = unspents;
|
||||||
|
|
||||||
return unspentOutputs;
|
// Return the unspents.
|
||||||
|
return unspents;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUnspentDirectives() {
|
async fetchUnspentDirectives() {
|
||||||
// Get a list of unspent outputs.
|
// Get a list of unspent outputs.
|
||||||
const unspentOutputs = await this.fetchUnspentOutputs();
|
const unspentOutputs = await this.fetchUnspents();
|
||||||
|
|
||||||
// Create our P2PKH Compiler.
|
// Create our P2PKH Compiler.
|
||||||
const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd);
|
const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd);
|
||||||
|
|
||||||
// Transform our inputs into unlocking directives.
|
// Transform our inputs into unlocking directives.
|
||||||
const inputDirectives = unspentOutputs.map((unspent) => ({
|
const inputDirectives = unspentOutputs.map((unspent) => ({
|
||||||
outpointIndex: unspent.tx_pos,
|
outpointIndex: unspent.outpointIndex,
|
||||||
outpointTransactionHash: hexToBin(unspent.tx_hash),
|
outpointTransactionHash: unspent.outpointTransactionHash,
|
||||||
sequenceNumber: 0,
|
sequenceNumber: 0,
|
||||||
unlockingBytecode: {
|
unlockingBytecode: {
|
||||||
compiler: compilerP2PKH,
|
compiler: compilerP2PKH,
|
||||||
data: {
|
data: {
|
||||||
keys: { privateKeys: { key: this.toBytes() } },
|
keys: { privateKeys: { key: this.privateKey.toBytes() } },
|
||||||
},
|
},
|
||||||
script: 'unlock',
|
script: 'unlock',
|
||||||
token: unspent.token_data
|
token: unspent.token,
|
||||||
? {
|
valueSatoshis: unspent.valueSatoshis,
|
||||||
amount: BigInt(unspent.token_data.amount),
|
|
||||||
category: hexToBin(unspent.token_data.category),
|
|
||||||
nft: unspent.token_data.nft
|
|
||||||
? {
|
|
||||||
capability: unspent.token_data.nft.capability,
|
|
||||||
commitment: hexToBin(unspent.token_data.nft.commitment),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
valueSatoshis: BigInt(unspent.value),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -156,11 +147,67 @@ export class WalletP2PKH extends WalletBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refresh(): Promise<void> {
|
async refresh(): Promise<void> {
|
||||||
await Promise.all([this.fetchUnspentOutputs(), this.fetchHistory()]);
|
await Promise.all([this.fetchUnspents(), this.fetchTransactions()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAddressNotification(status: string | null) {
|
getAddress() {
|
||||||
|
return this.privateKey.derivePublicKey().deriveAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
async sweep(payoutBytecode: Uint8Array) {
|
||||||
|
// Flatten the inputs.
|
||||||
|
const inputs = await this.fetchUnspentDirectives();
|
||||||
|
|
||||||
|
// Calculate the total sats available in our inputs.
|
||||||
|
const totalSats = inputs.reduce((total, input) => {
|
||||||
|
if (input.unlockingBytecode instanceof Uint8Array) {
|
||||||
|
return (total += 0n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (total += input.unlockingBytecode.valueSatoshis);
|
||||||
|
}, 0n);
|
||||||
|
|
||||||
|
// We need to calculate the number of bytes so that we can calculate the fee.
|
||||||
|
// So we loop twice and store the final transaction here each time.
|
||||||
|
// 1st time will have zero fee. 2nd time will accommodate the fee.
|
||||||
|
let encodedTransaction = new Uint8Array();
|
||||||
|
|
||||||
|
// Create the transaction by looping twice.
|
||||||
|
// 1st loop: Transaction without a fee.
|
||||||
|
// 2nd loop: Accommodate the fee.
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
// Get the fee using 1000 sats/KB.
|
||||||
|
const feeSats = getMinimumFee(BigInt(encodedTransaction.length), 1000n);
|
||||||
|
|
||||||
|
// Attempt to generate the transaction.
|
||||||
|
const generatedTransaction = generateTransaction({
|
||||||
|
version: 2,
|
||||||
|
locktime: 0,
|
||||||
|
inputs: inputs,
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
lockingBytecode: payoutBytecode,
|
||||||
|
valueSatoshis: totalSats - feeSats,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!generatedTransaction.success) {
|
||||||
|
console.error(generatedTransaction.errors);
|
||||||
|
|
||||||
|
throw new Error('Failed to generate transaction');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the transaction for broadcasting (and fee estimation).
|
||||||
|
encodedTransaction = encodeTransaction(generatedTransaction.transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onAddressNotification(status: string | null) {
|
||||||
// If status is null, it simply means that our subscribe call to Electrum was successful.
|
// If status is null, it simply means that our subscribe call to Electrum was successful.
|
||||||
|
// TODO: I don't know if we need this anymore on later versions of Electurm.
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -171,13 +218,13 @@ export class WalletP2PKH extends WalletBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type WalletP2PKHFactory<T extends WalletP2PKH = WalletP2PKH> = (
|
export type WalletP2PKHFactory<T extends WalletP2PKH = WalletP2PKH> = (
|
||||||
privateKeyBytes: Uint8Array,
|
privateKey: PrivateKey,
|
||||||
electrum: Blockchain
|
blockchain: IBlockchain
|
||||||
) => T;
|
) => T;
|
||||||
|
|
||||||
export function useWalletP2PKH<T extends WalletP2PKH = WalletP2PKH>(
|
export function useWalletP2PKH<T extends WalletP2PKH = WalletP2PKH>(
|
||||||
privateKeyBytes: Uint8Array,
|
privateKey: PrivateKey,
|
||||||
electrum: Blockchain
|
blockchain: IBlockchain
|
||||||
): T {
|
): T {
|
||||||
return new WalletP2PKH(privateKeyBytes, electrum) as T;
|
return new WalletP2PKH(privateKey, blockchain) as T;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user