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();
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
blockchain.broadcastTransaction(tx);
|
||||
blockchain.commitTransaction(tx);
|
||||
});
|
||||
|
||||
console.log(blockchain.pendingTransactions.deriveUtxoState());
|
||||
|
||||
Generated
+8
-1
@@ -6007,6 +6007,12 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -10940,7 +10946,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@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.
|
||||
*
|
||||
* @param address {string} The CashAddr to decode.
|
||||
* @param cashAddr The CashAddr to decode.
|
||||
*
|
||||
* @throws {Error} If address cannot be decoded.
|
||||
*
|
||||
@@ -71,7 +71,7 @@ export class Address {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -128,7 +128,7 @@ export class Address {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -156,7 +156,7 @@ export class Address {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -30,7 +30,7 @@ export class Bytes {
|
||||
/**
|
||||
* Instantiates bytes from a Base64 string.
|
||||
*
|
||||
* @param utf8 {string} The Base64 string.
|
||||
* @param base64 The Base64 string.
|
||||
*
|
||||
* @throws {Error} If Base64 is invalid.
|
||||
*
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deriveHdPath,
|
||||
deriveHdPublicNode,
|
||||
deriveHdPrivateNodeFromSeed,
|
||||
deriveSeedFromBip39Mnemonic,
|
||||
generateRandomBytes,
|
||||
} from '@bitauth/libauth';
|
||||
|
||||
@@ -26,38 +27,16 @@ export class HDPrivateNode {
|
||||
/**
|
||||
* Construct a new HD Private Node.
|
||||
*
|
||||
* @param node {HdPrivateNodeValid} The HD Private Node.
|
||||
* @param node The HD Private Node.
|
||||
*/
|
||||
constructor(node: HdPrivateNodeValid) {
|
||||
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.
|
||||
*
|
||||
* @param seed {Uint8Array} The raw seed bytes.
|
||||
* @param seed The raw seed bytes.
|
||||
*
|
||||
* @throws {Error} If HD Private Node cannot be created.
|
||||
*
|
||||
@@ -83,6 +62,45 @@ export class HDPrivateNode {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -44,6 +44,19 @@ export class PrivateKey {
|
||||
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).
|
||||
*
|
||||
@@ -232,7 +245,7 @@ export class PrivateKey {
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -106,4 +106,25 @@ export class Transaction {
|
||||
public getLocktime() {
|
||||
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 { computed, ref, shallowRef } from 'vue';
|
||||
|
||||
import { HdPrivateNodeValid } from '@bitauth/libauth';
|
||||
|
||||
export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
|
||||
// Reactive State.
|
||||
public rShouldMonitor = ref(false);
|
||||
@@ -14,13 +13,13 @@ export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
|
||||
public rBalance = computed(() => {
|
||||
return this.rWallets.value.reduce(
|
||||
(total, node) => total + node.rBalance.value,
|
||||
0
|
||||
0n
|
||||
);
|
||||
});
|
||||
|
||||
constructor(
|
||||
node: HdPrivateNodeValid,
|
||||
electrum: Blockchain,
|
||||
node: HDPrivateNode,
|
||||
electrum: IBlockchain,
|
||||
walletFactory = useWalletP2PKHVue
|
||||
) {
|
||||
super(node, electrum, walletFactory);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Transaction } from '@xo/primitives';
|
||||
import { type AddressListUnspent, Blockchain, WalletP2PKH } from '@xo/wallet';
|
||||
import { PrivateKey, Transaction } from '@xo/primitives';
|
||||
import { type IBlockchain, type UTXO, WalletP2PKH } from '@xo/wallet';
|
||||
|
||||
import { computed, shallowRef } from 'vue';
|
||||
/**
|
||||
@@ -15,19 +15,19 @@ import { computed, shallowRef } from 'vue';
|
||||
*/
|
||||
export class WalletP2PKHVue extends WalletP2PKH {
|
||||
// Reactive State.
|
||||
public rUnspents = shallowRef<AddressListUnspent['response']>([]);
|
||||
public rUnspents = shallowRef<Array<UTXO>>([]);
|
||||
public rTransactions = shallowRef<Array<Transaction>>([]);
|
||||
|
||||
// Computeds.
|
||||
public rBalance = computed(() => {
|
||||
return this.rUnspents.value.reduce(
|
||||
(total, unspent) => total + unspent.value,
|
||||
0
|
||||
(total, unspent) => total + unspent.valueSatoshis,
|
||||
0n
|
||||
);
|
||||
});
|
||||
|
||||
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
|
||||
super(privateKeyBytes, electrum);
|
||||
constructor(privateKey: PrivateKey, electrum: IBlockchain) {
|
||||
super(privateKey, electrum);
|
||||
|
||||
this.events.on('unspentsUpdated', (unspents) => {
|
||||
this.rUnspents.value = unspents;
|
||||
@@ -40,8 +40,8 @@ export class WalletP2PKHVue extends WalletP2PKH {
|
||||
}
|
||||
|
||||
export function useWalletP2PKHVue(
|
||||
privateKeyBytes: Uint8Array,
|
||||
electrum: Blockchain
|
||||
privateKey: PrivateKey,
|
||||
electrum: IBlockchain
|
||||
): WalletP2PKHVue {
|
||||
return new WalletP2PKHVue(privateKeyBytes, electrum);
|
||||
return new WalletP2PKHVue(privateKey, electrum);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@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]
|
||||
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 './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-p2pkh.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 { Blockchain } from './blockchain.js';
|
||||
import {
|
||||
type IBlockchain,
|
||||
type AddressListUnspent,
|
||||
} from '../blockchain/index.js';
|
||||
|
||||
import { PrivateKey, Transaction } from '@xo/primitives';
|
||||
|
||||
@@ -17,11 +19,11 @@ export type InputTemplateBCH = InputTemplate<
|
||||
|
||||
export abstract class WalletBase extends PrivateKey {
|
||||
// Dependencies/Services
|
||||
public electrum: Blockchain;
|
||||
public blockchain: IBlockchain;
|
||||
|
||||
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
|
||||
constructor(privateKeyBytes: Uint8Array, blockchain: IBlockchain) {
|
||||
super(privateKeyBytes);
|
||||
this.electrum = electrum;
|
||||
this.blockchain = blockchain;
|
||||
}
|
||||
|
||||
abstract startMonitoring(): Promise<void>;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// Import the Stamp class to derive the private keys
|
||||
import { type IWallet } from './interface.js';
|
||||
|
||||
import { HDPrivateNode, Transaction } from '@xo/primitives';
|
||||
import { IBlockchain } from '../blockchain/index.js';
|
||||
import { EventEmitter } from '../utils/index.js';
|
||||
import { Blockchain } from './blockchain.js';
|
||||
import {
|
||||
WalletP2PKH,
|
||||
WalletP2PKHFactory,
|
||||
useWalletP2PKH,
|
||||
} from './wallet-p2pkh.js';
|
||||
|
||||
import {
|
||||
type HdPrivateNodeValid,
|
||||
deriveSeedFromBip39Mnemonic,
|
||||
encodeTransaction,
|
||||
generateTransaction,
|
||||
@@ -24,9 +24,9 @@ export type WalletHDEvents<WalletType> = {
|
||||
walletsUpdated: Array<WalletType>;
|
||||
};
|
||||
|
||||
export class WalletHD<
|
||||
WalletType extends WalletP2PKH = WalletP2PKH
|
||||
> extends HDPrivateNode {
|
||||
export class WalletHD<WalletType extends WalletP2PKH = WalletP2PKH>
|
||||
implements IWallet
|
||||
{
|
||||
// Events.
|
||||
public events: EventEmitter<WalletHDEvents<WalletType>> = new EventEmitter();
|
||||
|
||||
@@ -35,22 +35,32 @@ export class WalletHD<
|
||||
public wallets: Array<WalletType> = [];
|
||||
|
||||
constructor(
|
||||
public node: HdPrivateNodeValid,
|
||||
public electrum: Blockchain,
|
||||
public node: HDPrivateNode,
|
||||
public blockchain: IBlockchain,
|
||||
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH
|
||||
) {
|
||||
// Instantiate parent class.
|
||||
super(node);
|
||||
|
||||
// Monitor the following properties and emit an event when they change.
|
||||
this.events.monitorProperty(this, 'shouldMonitor', 'shouldMonitorUpdated');
|
||||
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>>(
|
||||
this: WalletHDConstructor<T>,
|
||||
mnemonic: string,
|
||||
electrum: Blockchain,
|
||||
blockchain: IBlockchain,
|
||||
walletFactory?: WalletP2PKHFactory<any>
|
||||
): T {
|
||||
// Derive the seed from the mnemonic.
|
||||
@@ -60,46 +70,93 @@ export class WalletHD<
|
||||
const hdPrivateNode = HDPrivateNode.fromSeed(seed);
|
||||
|
||||
// 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() {
|
||||
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) {
|
||||
// Clear existing nodes.
|
||||
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) {
|
||||
// Create an array to store our wallets.
|
||||
const wallets: Array<WalletType> = [];
|
||||
|
||||
// Create the given count of wallets.
|
||||
for (let i = 0; i < count; i++) {
|
||||
const childNode = this.derivePath(
|
||||
const childNode = this.node.derivePath(
|
||||
`${DERIVATION_PATH}/0/${startIndex + i}`
|
||||
);
|
||||
const wallet = this.walletFactory(
|
||||
childNode.privateKey().toBytes(),
|
||||
this.electrum
|
||||
childNode.privateKey(),
|
||||
this.blockchain
|
||||
);
|
||||
|
||||
if (this.shouldMonitor) {
|
||||
wallet.startMonitoring();
|
||||
wallet.start();
|
||||
}
|
||||
|
||||
wallets.push(wallet);
|
||||
@@ -128,7 +185,7 @@ export class WalletHD<
|
||||
|
||||
// Get the transaction history of each wallet in the batch
|
||||
const results = await Promise.all(
|
||||
batch.map((wallet) => wallet.fetchHistory())
|
||||
batch.map((wallet) => wallet.fetchTransactions())
|
||||
);
|
||||
|
||||
// Process the results
|
||||
@@ -156,14 +213,10 @@ export class WalletHD<
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async refreshChildNodes() {
|
||||
await Promise.all(this.wallets.map((node) => node.refresh()));
|
||||
}
|
||||
|
||||
async sweep(payoutBytecode: Uint8Array) {
|
||||
// Get a list of inputs belonging to each wallet.
|
||||
const inputs = await Promise.all(
|
||||
this.wallets.map((wallet) => wallet.getUnspentDirectives())
|
||||
this.wallets.map((wallet) => wallet.fetchUnspentDirectives())
|
||||
);
|
||||
|
||||
// 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 {
|
||||
type AddressListUnspent,
|
||||
type AddressGetHistory,
|
||||
type TransactionGet,
|
||||
} from './blockchain-types.js';
|
||||
import { Blockchain } from './blockchain.js';
|
||||
|
||||
import { WalletBase } from './wallet-base.js';
|
||||
|
||||
import {
|
||||
hexToBin,
|
||||
encodeTransaction,
|
||||
generateTransaction,
|
||||
getMinimumFee,
|
||||
walletTemplateP2pkhNonHd,
|
||||
walletTemplateToCompilerBCH,
|
||||
} from '@bitauth/libauth';
|
||||
|
||||
export type WalletP2PKHEvents = {
|
||||
transactionsUpdated: Array<Transaction>;
|
||||
unspentsUpdated: AddressListUnspent['response'];
|
||||
unspentsUpdated: Array<UTXO>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,124 +27,118 @@ export type WalletP2PKHEvents = {
|
||||
* await walletP2PKH.startMonitoring();
|
||||
* ```
|
||||
*/
|
||||
export class WalletP2PKH extends WalletBase {
|
||||
export class WalletP2PKH implements IWallet {
|
||||
// Events.
|
||||
public events: EventEmitter<WalletP2PKHEvents> = new EventEmitter();
|
||||
|
||||
// State.
|
||||
public transactions: Array<Transaction> = [];
|
||||
public unspents: AddressListUnspent['response'] = [];
|
||||
|
||||
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
|
||||
super(privateKeyBytes, electrum);
|
||||
public unspents: Array<UTXO> = [];
|
||||
|
||||
constructor(public privateKey: PrivateKey, public blockchain: IBlockchain) {
|
||||
// Monitor the following properties and emit an event when they change.
|
||||
this.events.monitorProperty(this, 'transactions', 'transactionsUpdated');
|
||||
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() {
|
||||
this.events.removeAllListeners();
|
||||
}
|
||||
|
||||
async startMonitoring() {
|
||||
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() {
|
||||
async fetchTransactions() {
|
||||
// Get this node's address.
|
||||
const address = this.getAddress();
|
||||
const address = this.getAddress().toCashAddr();
|
||||
|
||||
// Get the history of the address from electrum
|
||||
const history = await this.electrum.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.electrum.request<TransactionGet>(
|
||||
'blockchain.transaction.get',
|
||||
item.tx_hash,
|
||||
false
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Decode the transactions.
|
||||
const decodedTransactions = transactions.map((tx) =>
|
||||
Transaction.fromHex(tx)
|
||||
);
|
||||
// Get transactions involving this address.
|
||||
const transactions = await this.blockchain.fetchTransactions(address);
|
||||
|
||||
// Store the transactions.
|
||||
this.transactions = decodedTransactions;
|
||||
this.transactions = transactions;
|
||||
|
||||
return decodedTransactions;
|
||||
// Return the fetched transactions.
|
||||
return transactions;
|
||||
}
|
||||
|
||||
async fetchUnspentOutputs() {
|
||||
const address = this.getAddress();
|
||||
async fetchUnspents() {
|
||||
// Get this wallet's address.
|
||||
const address = this.getAddress().toCashAddr();
|
||||
|
||||
const unspentOutputs = await this.electrum.request<AddressListUnspent>(
|
||||
'blockchain.address.listunspent',
|
||||
address,
|
||||
'include_tokens'
|
||||
);
|
||||
// Fetch unspents belonging to this address.
|
||||
const unspents = await this.blockchain.fetchUnspents(address);
|
||||
|
||||
// Emit event with unspent outputs.
|
||||
this.unspents = unspentOutputs;
|
||||
// Store the unspents.
|
||||
this.unspents = unspents;
|
||||
|
||||
return unspentOutputs;
|
||||
// Return the unspents.
|
||||
return unspents;
|
||||
}
|
||||
|
||||
async getUnspentDirectives() {
|
||||
async fetchUnspentDirectives() {
|
||||
// Get a list of unspent outputs.
|
||||
const unspentOutputs = await this.fetchUnspentOutputs();
|
||||
const unspentOutputs = await this.fetchUnspents();
|
||||
|
||||
// Create our P2PKH Compiler.
|
||||
const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd);
|
||||
|
||||
// Transform our inputs into unlocking directives.
|
||||
const inputDirectives = unspentOutputs.map((unspent) => ({
|
||||
outpointIndex: unspent.tx_pos,
|
||||
outpointTransactionHash: hexToBin(unspent.tx_hash),
|
||||
outpointIndex: unspent.outpointIndex,
|
||||
outpointTransactionHash: unspent.outpointTransactionHash,
|
||||
sequenceNumber: 0,
|
||||
unlockingBytecode: {
|
||||
compiler: compilerP2PKH,
|
||||
data: {
|
||||
keys: { privateKeys: { key: this.toBytes() } },
|
||||
keys: { privateKeys: { key: this.privateKey.toBytes() } },
|
||||
},
|
||||
script: 'unlock',
|
||||
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,
|
||||
valueSatoshis: BigInt(unspent.value),
|
||||
token: unspent.token,
|
||||
valueSatoshis: unspent.valueSatoshis,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -156,11 +147,67 @@ export class WalletP2PKH extends WalletBase {
|
||||
}
|
||||
|
||||
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.
|
||||
// TODO: I don't know if we need this anymore on later versions of Electurm.
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
@@ -171,13 +218,13 @@ export class WalletP2PKH extends WalletBase {
|
||||
}
|
||||
|
||||
export type WalletP2PKHFactory<T extends WalletP2PKH = WalletP2PKH> = (
|
||||
privateKeyBytes: Uint8Array,
|
||||
electrum: Blockchain
|
||||
privateKey: PrivateKey,
|
||||
blockchain: IBlockchain
|
||||
) => T;
|
||||
|
||||
export function useWalletP2PKH<T extends WalletP2PKH = WalletP2PKH>(
|
||||
privateKeyBytes: Uint8Array,
|
||||
electrum: Blockchain
|
||||
privateKey: PrivateKey,
|
||||
blockchain: IBlockchain
|
||||
): T {
|
||||
return new WalletP2PKH(privateKeyBytes, electrum) as T;
|
||||
return new WalletP2PKH(privateKey, blockchain) as T;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user