mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-20 02:09:10 +01:00
328 lines
8.3 KiB
JavaScript
328 lines
8.3 KiB
JavaScript
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
|
import b58 from 'bs58check';
|
|
const createHash = require('create-hash');
|
|
|
|
export class AbstractWallet {
|
|
static type = 'abstract';
|
|
static typeReadable = 'abstract';
|
|
|
|
static fromJson(obj) {
|
|
const obj2 = JSON.parse(obj);
|
|
const temp = new this();
|
|
for (const key2 of Object.keys(obj2)) {
|
|
temp[key2] = obj2[key2];
|
|
}
|
|
|
|
return temp;
|
|
}
|
|
|
|
constructor() {
|
|
this.type = this.constructor.type;
|
|
this.typeReadable = this.constructor.typeReadable;
|
|
this.label = '';
|
|
this.secret = ''; // private key or recovery phrase
|
|
this.balance = 0;
|
|
this.unconfirmed_balance = 0;
|
|
this._address = false; // cache
|
|
this.utxo = [];
|
|
this._lastTxFetch = 0;
|
|
this._lastBalanceFetch = 0;
|
|
this.preferredBalanceUnit = BitcoinUnit.BTC;
|
|
this.chain = Chain.ONCHAIN;
|
|
this.hideBalance = false;
|
|
this.userHasSavedExport = false;
|
|
this._hideTransactionsInWalletsList = false;
|
|
this._utxoMetadata = {};
|
|
}
|
|
|
|
getID() {
|
|
return createHash('sha256').update(this.getSecret()).digest().toString('hex');
|
|
}
|
|
|
|
getTransactions() {
|
|
throw new Error('not implemented');
|
|
}
|
|
|
|
getUserHasSavedExport() {
|
|
return this.userHasSavedExport;
|
|
}
|
|
|
|
setUserHasSavedExport(value) {
|
|
this.userHasSavedExport = value;
|
|
}
|
|
|
|
getHideTransactionsInWalletsList() {
|
|
return this._hideTransactionsInWalletsList;
|
|
}
|
|
|
|
setHideTransactionsInWalletsList(value) {
|
|
this._hideTransactionsInWalletsList = value;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getLabel() {
|
|
if (this.label.trim().length === 0) {
|
|
return 'Wallet';
|
|
}
|
|
return this.label;
|
|
}
|
|
|
|
getXpub() {
|
|
return this._address;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {number} Available to spend amount, int, in sats
|
|
*/
|
|
getBalance() {
|
|
return this.balance + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
|
|
}
|
|
|
|
getPreferredBalanceUnit() {
|
|
for (const value of Object.values(BitcoinUnit)) {
|
|
if (value === this.preferredBalanceUnit) {
|
|
return this.preferredBalanceUnit;
|
|
}
|
|
}
|
|
return BitcoinUnit.BTC;
|
|
}
|
|
|
|
allowReceive() {
|
|
return true;
|
|
}
|
|
|
|
allowSend() {
|
|
return true;
|
|
}
|
|
|
|
allowSendMax(): boolean {
|
|
return false;
|
|
}
|
|
|
|
allowRBF() {
|
|
return false;
|
|
}
|
|
|
|
allowBatchSend() {
|
|
return false;
|
|
}
|
|
|
|
allowHodlHodlTrading() {
|
|
return false;
|
|
}
|
|
|
|
allowPayJoin() {
|
|
return false;
|
|
}
|
|
|
|
allowCosignPsbt() {
|
|
return false;
|
|
}
|
|
|
|
weOwnAddress(address) {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
weOwnTransaction(txid) {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
/**
|
|
* Returns delta of unconfirmed balance. For example, if theres no
|
|
* unconfirmed balance its 0
|
|
*
|
|
* @return {number} Satoshis
|
|
*/
|
|
getUnconfirmedBalance() {
|
|
return this.unconfirmed_balance;
|
|
}
|
|
|
|
setLabel(newLabel) {
|
|
this.label = newLabel;
|
|
return this;
|
|
}
|
|
|
|
getSecret() {
|
|
return this.secret;
|
|
}
|
|
|
|
setSecret(newSecret) {
|
|
this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
|
|
|
|
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();
|
|
|
|
// [fingerprint/derivation]zpub
|
|
const re = /\[([^\]]+)\](.*)/;
|
|
const m = this.secret.match(re);
|
|
if (m && m.length === 3) {
|
|
let hexFingerprint = m[1].split('/')[0];
|
|
if (hexFingerprint.length === 8) {
|
|
hexFingerprint = Buffer.from(hexFingerprint, 'hex').reverse().toString('hex');
|
|
this.masterFingerprint = parseInt(hexFingerprint, 16);
|
|
}
|
|
this.secret = m[2];
|
|
}
|
|
|
|
try {
|
|
let parsedSecret;
|
|
// regex might've matched invalid data. if so, parse newSecret.
|
|
if (this.secret.trim().length > 0) {
|
|
try {
|
|
parsedSecret = JSON.parse(this.secret);
|
|
} catch (e) {
|
|
parsedSecret = JSON.parse(newSecret);
|
|
}
|
|
} else {
|
|
parsedSecret = JSON.parse(newSecret);
|
|
}
|
|
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
|
|
let masterFingerprint = false;
|
|
if (parsedSecret.keystore.ckcc_xfp) {
|
|
// It is a ColdCard Hardware Wallet
|
|
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
|
|
} else if (parsedSecret.keystore.root_fingerprint) {
|
|
masterFingerprint = Number(parsedSecret.keystore.root_fingerprint);
|
|
}
|
|
if (parsedSecret.keystore.label) {
|
|
this.setLabel(parsedSecret.keystore.label);
|
|
}
|
|
this.secret = parsedSecret.keystore.xpub;
|
|
this.masterFingerprint = masterFingerprint;
|
|
}
|
|
// It is a Cobo Vault Hardware Wallet
|
|
if (parsedSecret && parsedSecret.ExtPubKey && parsedSecret.MasterFingerprint) {
|
|
this.secret = parsedSecret.ExtPubKey;
|
|
const mfp = Buffer.from(parsedSecret.MasterFingerprint, 'hex').reverse().toString('hex');
|
|
this.masterFingerprint = parseInt(mfp, 16);
|
|
this.setLabel('Cobo Vault ' + parsedSecret.MasterFingerprint);
|
|
}
|
|
} catch (_) {}
|
|
return this;
|
|
}
|
|
|
|
getLatestTransactionTime() {
|
|
return 0;
|
|
}
|
|
|
|
getLatestTransactionTimeEpoch() {
|
|
if (this.getTransactions().length === 0) {
|
|
return 0;
|
|
}
|
|
let max = 0;
|
|
for (const tx of this.getTransactions()) {
|
|
max = Math.max(new Date(tx.received) * 1, max);
|
|
}
|
|
return max;
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
createTx() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
|
|
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
|
|
* @param feeRate {Number} satoshi per byte
|
|
* @param changeAddress {String} Excessive coins will go back to that address
|
|
* @param sequence {Number} Used in RBF
|
|
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
|
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
|
|
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
|
*/
|
|
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
getAddress() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
getAddressAsync() {
|
|
return new Promise(resolve => resolve(this.getAddress()));
|
|
}
|
|
|
|
async getChangeAddressAsync() {
|
|
return new Promise(resolve => resolve(this.getAddress()));
|
|
}
|
|
|
|
useWithHardwareWalletEnabled() {
|
|
return false;
|
|
}
|
|
|
|
async wasEverUsed() {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
/**
|
|
* Returns _all_ external addresses in hierarchy (for HD wallets) or just address for single-address wallets
|
|
* _Not_ internal ones, as this method is supposed to be used for subscription of external notifications.
|
|
*
|
|
* @returns string[] Addresses
|
|
*/
|
|
getAllExternalAddresses() {
|
|
return [];
|
|
}
|
|
|
|
/*
|
|
* Converts zpub to xpub
|
|
*
|
|
* @param {String} zpub
|
|
* @returns {String} xpub
|
|
*/
|
|
static _zpubToXpub(zpub) {
|
|
let data = b58.decode(zpub);
|
|
data = data.slice(4);
|
|
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
|
|
|
return b58.encode(data);
|
|
}
|
|
|
|
/**
|
|
* Converts ypub to xpub
|
|
* @param {String} ypub - wallet ypub
|
|
* @returns {*}
|
|
*/
|
|
static _ypubToXpub(ypub) {
|
|
let data = b58.decode(ypub);
|
|
if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!');
|
|
data = data.slice(4);
|
|
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
|
|
|
return b58.encode(data);
|
|
}
|
|
|
|
prepareForSerialization() {}
|
|
|
|
/*
|
|
* Get metadata (frozen, memo) for a specific UTXO
|
|
*
|
|
* @param {String} txid - transaction id
|
|
* @param {number} vout - an index number of the output in transaction
|
|
*/
|
|
getUTXOMetadata(txid, vout) {
|
|
return this._utxoMetadata[`${txid}:${vout}`] || {};
|
|
}
|
|
|
|
/*
|
|
* Set metadata (frozen, memo) for a specific UTXO
|
|
*
|
|
* @param {String} txid - transaction id
|
|
* @param {number} vout - an index number of the output in transaction
|
|
* @param {{memo: String, frozen: Boolean}} opts - options to attach to UTXO
|
|
*/
|
|
setUTXOMetadata(txid, vout, opts) {
|
|
const meta = this._utxoMetadata[`${txid}:${vout}`] || {};
|
|
if ('memo' in opts) meta.memo = opts.memo;
|
|
if ('frozen' in opts) meta.frozen = opts.frozen;
|
|
this._utxoMetadata[`${txid}:${vout}`] = meta;
|
|
}
|
|
}
|