BlueWallet/class/wallets/watch-only-wallet.ts

316 lines
11 KiB
TypeScript
Raw Normal View History

2022-01-17 15:22:15 +00:00
import BIP32Factory from 'bip32';
2024-03-15 23:05:15 +03:00
import * as bitcoin from 'bitcoinjs-lib';
2024-05-20 10:54:13 +01:00
import ecc from '../../blue_modules/noble_ecc';
2024-03-15 23:05:15 +03:00
import { AbstractWallet } from './abstract-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { LegacyWallet } from './legacy-wallet';
import { THDWalletForWatchOnly } from './types';
2022-01-17 15:22:15 +00:00
const bip32 = BIP32Factory(ecc);
2018-07-08 20:01:52 +01:00
export class WatchOnlyWallet extends LegacyWallet {
2024-03-15 23:05:15 +03:00
static readonly type = 'watchOnly';
static readonly typeReadable = 'Watch-only';
// @ts-ignore: override
public readonly type = WatchOnlyWallet.type;
// @ts-ignore: override
public readonly typeReadable = WatchOnlyWallet.typeReadable;
2024-06-22 14:30:29 -04:00
public isWatchOnlyWarningVisible = true;
2018-07-08 20:01:52 +01:00
2024-03-15 23:05:15 +03:00
public _hdWalletInstance?: THDWalletForWatchOnly;
use_with_hardware_wallet = false;
masterFingerprint: number = 0;
2019-09-27 15:49:56 +01:00
2021-04-21 11:29:34 +01:00
/**
* @inheritDoc
*/
getLastTxFetch() {
if (this._hdWalletInstance) return this._hdWalletInstance.getLastTxFetch();
return super.getLastTxFetch();
}
timeToRefreshTransaction() {
if (this._hdWalletInstance) return this._hdWalletInstance.timeToRefreshTransaction();
return super.timeToRefreshTransaction();
}
timeToRefreshBalance() {
if (this._hdWalletInstance) return this._hdWalletInstance.timeToRefreshBalance();
return super.timeToRefreshBalance();
}
2018-07-08 20:01:52 +01:00
allowSend() {
2024-03-15 23:05:15 +03:00
return this.useWithHardwareWalletEnabled() && this.isHd() && this._hdWalletInstance!.allowSend();
2019-09-27 15:49:56 +01:00
}
2021-03-23 15:16:32 +03:00
allowSignVerifyMessage() {
return false;
}
2018-07-08 20:01:52 +01:00
getAddress() {
if (this.isAddressValid(this.secret)) return this.secret; // handling case when there is an XPUB there
if (this._hdWalletInstance) throw new Error('Should not be used in watch-only HD wallets');
throw new Error('Not initialized');
2018-07-08 20:01:52 +01:00
}
valid() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) return this.isXpubValid();
try {
bitcoin.address.toOutputScript(this.getAddress());
return true;
2020-02-24 21:45:14 +00:00
} catch (_) {
return false;
}
}
/**
* this method creates appropriate HD wallet class, depending on whether we have xpub, ypub or zpub
* as a property of `this`, and in case such property exists - it recreates it and copies data from old one.
* this is needed after serialization/save/load/deserialization procedure.
*/
init() {
2024-03-15 23:05:15 +03:00
let hdWalletInstance: THDWalletForWatchOnly;
if (this.secret.startsWith('xpub')) hdWalletInstance = new HDLegacyP2PKHWallet();
2019-05-15 00:19:35 +01:00
else if (this.secret.startsWith('ypub')) hdWalletInstance = new HDSegwitP2SHWallet();
else if (this.secret.startsWith('zpub')) hdWalletInstance = new HDSegwitBech32Wallet();
else return this;
hdWalletInstance._xpub = this.secret;
// if derivation path recovered from JSON file it should be moved to hdWalletInstance
if (this._derivationPath) {
hdWalletInstance._derivationPath = this._derivationPath;
}
if (this._hdWalletInstance) {
// now, porting all properties from old object to new one
for (const k of Object.keys(this._hdWalletInstance)) {
2024-03-15 23:05:15 +03:00
// @ts-ignore: JS magic here
hdWalletInstance[k] = this._hdWalletInstance[k];
}
2019-09-27 15:49:56 +01:00
// deleting properties that cant survive serialization/deserialization:
delete hdWalletInstance._node1;
delete hdWalletInstance._node0;
}
this._hdWalletInstance = hdWalletInstance;
return this;
}
2020-07-02 14:43:35 +01:00
prepareForSerialization() {
if (this._hdWalletInstance) {
delete this._hdWalletInstance._node0;
delete this._hdWalletInstance._node1;
2023-03-15 20:42:25 +00:00
delete this._hdWalletInstance._bip47_instance;
2020-07-02 14:43:35 +01:00
}
}
getBalance() {
if (this._hdWalletInstance) return this._hdWalletInstance.getBalance();
return super.getBalance();
}
getTransactions() {
if (this._hdWalletInstance) return this._hdWalletInstance.getTransactions();
return super.getTransactions();
}
async fetchBalance() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
2024-03-15 23:05:15 +03:00
if (!this._hdWalletInstance) throw new Error('Internal error: _hdWalletInstance is not initialized');
return this._hdWalletInstance.fetchBalance();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
return super.fetchBalance();
}
}
async fetchTransactions() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
2024-03-15 23:05:15 +03:00
if (!this._hdWalletInstance) throw new Error('Internal error: _hdWalletInstance is not initialized');
return this._hdWalletInstance.fetchTransactions();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
return super.fetchTransactions();
}
}
2024-03-15 23:05:15 +03:00
async getAddressAsync(): Promise<string> {
if (this.isAddressValid(this.secret)) return new Promise(resolve => resolve(this.secret));
if (this._hdWalletInstance) return this._hdWalletInstance.getAddressAsync();
throw new Error('Not initialized');
}
2019-09-27 15:49:56 +01:00
2024-03-15 23:05:15 +03:00
_getExternalAddressByIndex(index: number) {
2019-09-27 15:49:56 +01:00
if (this._hdWalletInstance) return this._hdWalletInstance._getExternalAddressByIndex(index);
throw new Error('Not initialized');
}
2024-03-15 23:05:15 +03:00
_getInternalAddressByIndex(index: number) {
if (this._hdWalletInstance) return this._hdWalletInstance._getInternalAddressByIndex(index);
throw new Error('Not initialized');
}
getNextFreeAddressIndex() {
if (this._hdWalletInstance) return this._hdWalletInstance.next_free_address_index;
throw new Error('Not initialized');
}
getNextFreeChangeAddressIndex() {
if (this._hdWalletInstance) return this._hdWalletInstance.next_free_change_address_index;
throw new Error('Not initialized');
}
2019-09-27 15:49:56 +01:00
async getChangeAddressAsync() {
if (this._hdWalletInstance) return this._hdWalletInstance.getChangeAddressAsync();
throw new Error('Not initialized');
}
async fetchUtxo() {
if (this._hdWalletInstance) return this._hdWalletInstance.fetchUtxo();
throw new Error('Not initialized');
}
2024-03-15 23:05:15 +03:00
getUtxo(...args: Parameters<THDWalletForWatchOnly['getUtxo']>) {
2020-10-23 13:49:17 +03:00
if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo(...args);
2019-09-27 15:49:56 +01:00
throw new Error('Not initialized');
}
2024-03-15 23:05:15 +03:00
combinePsbt(...args: Parameters<THDWalletForWatchOnly['combinePsbt']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.combinePsbt(...args);
2019-09-27 15:49:56 +01:00
throw new Error('Not initialized');
}
2024-03-15 23:05:15 +03:00
broadcastTx(...args: Parameters<THDWalletForWatchOnly['broadcastTx']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.broadcastTx(...args);
2019-09-27 15:49:56 +01:00
throw new Error('Not initialized');
}
/**
* signature of this method is the same ad BIP84 createTransaction, BUT this method should be used to create
* unsinged PSBT to be used with HW wallet (or other external signer)
*/
2024-03-15 23:05:15 +03:00
createTransaction(...args: Parameters<THDWalletForWatchOnly['createTransaction']>) {
const [utxos, targets, feeRate, changeAddress, sequence] = args;
if (this._hdWalletInstance && this.isHd()) {
2024-03-15 23:05:15 +03:00
const masterFingerprint = this.getMasterFingerprint();
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, masterFingerprint);
2019-09-27 15:49:56 +01:00
} else {
throw new Error('Not a HD watch-only wallet, cant create PSBT (or just not initialized)');
2019-09-27 15:49:56 +01:00
}
}
2020-02-24 21:45:14 +00:00
getMasterFingerprint() {
return this.masterFingerprint;
}
getMasterFingerprintHex() {
2020-02-26 14:39:19 +00:00
if (!this.masterFingerprint) return '00000000';
2020-02-24 21:45:14 +00:00
let masterFingerprintHex = Number(this.masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
// poor man's little-endian conversion:
// ¯\_(ツ)_/¯
return (
masterFingerprintHex[6] +
masterFingerprintHex[7] +
masterFingerprintHex[4] +
masterFingerprintHex[5] +
masterFingerprintHex[2] +
masterFingerprintHex[3] +
masterFingerprintHex[0] +
masterFingerprintHex[1]
);
}
2020-02-26 14:39:19 +00:00
isHd() {
return this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub');
}
2024-03-15 23:05:15 +03:00
weOwnAddress(address: string) {
if (this.isHd()) {
if (this._hdWalletInstance) return this._hdWalletInstance.weOwnAddress(address);
throw new Error('Not initialized');
}
2021-05-20 09:49:40 +01:00
if (address && address.startsWith('BC1')) address = address.toLowerCase();
return this.getAddress() === address;
}
allowMasterFingerprint() {
return this.getSecret().startsWith('zpub');
}
2020-02-26 14:39:19 +00:00
useWithHardwareWalletEnabled() {
return !!this.use_with_hardware_wallet;
}
2024-03-15 23:05:15 +03:00
setUseWithHardwareWalletEnabled(enabled: boolean) {
2020-02-26 14:39:19 +00:00
this.use_with_hardware_wallet = !!enabled;
}
/**
* @inheritDoc
*/
getAllExternalAddresses() {
if (this._hdWalletInstance) return this._hdWalletInstance.getAllExternalAddresses();
return super.getAllExternalAddresses();
}
isXpubValid() {
let xpub;
try {
if (this.secret.startsWith('zpub')) {
xpub = this._zpubToXpub(this.secret);
} else if (this.secret.startsWith('ypub')) {
2024-03-15 23:05:15 +03:00
xpub = AbstractWallet._ypubToXpub(this.secret);
} else {
xpub = this.secret;
}
2022-01-17 15:22:15 +00:00
const hdNode = bip32.fromBase58(xpub);
hdNode.derive(0);
return true;
} catch (_) {}
return false;
}
2020-10-29 21:42:40 +03:00
2024-03-15 23:05:15 +03:00
addressIsChange(...args: Parameters<THDWalletForWatchOnly['addressIsChange']>) {
2020-10-29 21:42:40 +03:00
if (this._hdWalletInstance) return this._hdWalletInstance.addressIsChange(...args);
2020-10-30 10:44:44 +03:00
return super.addressIsChange(...args);
2020-10-29 21:42:40 +03:00
}
2024-03-15 23:05:15 +03:00
getUTXOMetadata(...args: Parameters<THDWalletForWatchOnly['getUTXOMetadata']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getUTXOMetadata(...args);
return super.getUTXOMetadata(...args);
}
2024-03-15 23:05:15 +03:00
setUTXOMetadata(...args: Parameters<THDWalletForWatchOnly['setUTXOMetadata']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.setUTXOMetadata(...args);
return super.setUTXOMetadata(...args);
}
2021-09-09 12:00:11 +01:00
2024-03-15 23:05:15 +03:00
getDerivationPath(...args: Parameters<THDWalletForWatchOnly['getDerivationPath']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getDerivationPath(...args);
throw new Error("Not a HD watch-only wallet, can't use derivation path");
}
2024-03-15 23:05:15 +03:00
setDerivationPath(...args: Parameters<THDWalletForWatchOnly['setDerivationPath']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.setDerivationPath(...args);
throw new Error("Not a HD watch-only wallet, can't use derivation path");
}
2024-03-15 23:05:15 +03:00
isSegwit(): boolean {
2021-09-09 12:00:11 +01:00
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
return super.isSegwit();
}
2018-07-08 20:01:52 +01:00
}