Compare commits

..

5 Commits

7 changed files with 154 additions and 20 deletions

View File

@ -19,20 +19,24 @@ export class HDSegwitBech32Transaction {
private _wallet: HDSegwitBech32Wallet | undefined;
private _txDecoded: bitcoin.Transaction | undefined;
private _remoteTx: any;
private _mfp?: number;
/**
* @param txhex {string|null} Object is initialized with txhex
* @param txid {string|null} If txhex not present - txid whould be present
* @param wallet {HDSegwitBech32Wallet|null} If set - a wallet object to which transacton belongs
*/
constructor(txhex: string | null, txid: string | null, wallet: HDSegwitBech32Wallet | null) {
constructor(txhex: string | null, txid: string | null, wallet: HDSegwitBech32Wallet | null, mfp?: number) {
if (!txhex && !txid) throw new Error('Bad arguments');
this._txhex = txhex;
this._txid = txid;
if (mfp) {
this._mfp = mfp;
}
if (wallet) {
if (wallet.type === HDSegwitBech32Wallet.type) {
/** @type {HDSegwitBech32Wallet} */
this._wallet = wallet;
} else {
throw new Error('Only HD Bech32 wallets supported');
@ -306,6 +310,19 @@ export class HDSegwitBech32Transaction {
if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one');
const myAddress = await this._wallet.getChangeAddressAsync();
// if there is no secret then its a watch only wallet, skip signing and also pass the masterfingerprint
if (!this._wallet.secret && this?._mfp) {
return this._wallet.createTransaction(
utxos,
[{ address: myAddress }],
newFeerate,
myAddress,
(await this.getMaxUsedSequence()) + 1,
true,
this._mfp,
);
}
return this._wallet.createTransaction(
utxos,
[{ address: myAddress }],
@ -342,6 +359,11 @@ export class HDSegwitBech32Transaction {
// not checking emptiness on purpose: it could unpredictably generate too far address because of unconfirmed tx.
}
// if there is no secret then its a watch only wallet, skip signing and also pass the masterfingerprint
if (!this._wallet.secret && this?._mfp) {
return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, (await this.getMaxUsedSequence()) + 1, true, this._mfp);
}
return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, (await this.getMaxUsedSequence()) + 1);
}

View File

@ -1,6 +1,5 @@
import BIP32Factory from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractWallet } from './abstract-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
@ -47,6 +46,10 @@ export class WatchOnlyWallet extends LegacyWallet {
return this.useWithHardwareWalletEnabled() && this.isHd() && this._hdWalletInstance!.allowSend();
}
allowRBF() {
return this._hdWalletInstance?.type === HDSegwitBech32Wallet.type;
}
allowSignVerifyMessage() {
return false;
}

View File

@ -280,7 +280,12 @@ const SendDetails = () => {
setParams({
...(walletActuallyChanged ? { utxos: null } : {}),
isTransactionReplaceable: wallet.type === HDSegwitBech32Wallet.type && !routeParams.isTransactionReplaceable ? true : undefined,
isTransactionReplaceable:
(wallet.type === HDSegwitBech32Wallet.type ||
(wallet.type === WatchOnlyWallet.type && wallet._hdWalletInstance?.type === HDSegwitBech32Wallet.type)) &&
!routeParams.isTransactionReplaceable
? true
: undefined,
});
prevWalletIdForCoinResetRef.current = currentId;
@ -1161,7 +1166,11 @@ const SendDetails = () => {
{
...CommonToolTipActions.AllowRBF,
menuState: isTransactionReplaceable,
hidden: !(wallet.type === HDSegwitBech32Wallet.type && isTransactionReplaceable !== undefined),
hidden: !(
(wallet.type === HDSegwitBech32Wallet.type ||
(wallet.type === WatchOnlyWallet.type && wallet._hdWalletInstance?.type === HDSegwitBech32Wallet.type)) &&
isTransactionReplaceable !== undefined
),
},
];
walletActions.push(rbfAction);

View File

@ -17,6 +17,7 @@ import { StorageContext } from '../../components/Context/StorageProvider';
import ReplaceFeeSuggestions from '../../components/ReplaceFeeSuggestions';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
import { BlueSpacing, BlueSpacing20 } from '../../components/BlueSpacing';
import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
const styles = StyleSheet.create({
root: {
@ -120,11 +121,19 @@ export default class CPFP extends Component {
}
async checkPossibilityOfCPFP() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) {
let tx;
if (this.state.wallet?.type === WatchOnlyWallet.type && this.state.wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
tx = new HDSegwitBech32Transaction(
null,
this.state.txid,
this.state.wallet._hdWalletInstance,
this.state.wallet.getMasterFingerprint(),
);
} else if (this.state.wallet?.type === HDSegwitBech32Wallet.type) {
tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet);
} else {
return this.setState({ nonReplaceable: true, isLoading: false });
}
const tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet);
if ((await tx.isToUsTransaction()) && (await tx.getRemoteConfirmationsNum()) === 0) {
const info = await tx.getInfo();
return this.setState({ nonReplaceable: false, feeRate: info.feeRate + 1, isLoading: false, tx });

View File

@ -10,6 +10,7 @@ import loc from '../../loc';
import CPFP from './CPFP';
import { StorageContext } from '../../components/Context/StorageProvider';
import { BlueSpacing20 } from '../../components/BlueSpacing';
import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
const styles = StyleSheet.create({
root: {
@ -32,11 +33,19 @@ export default class RBFBumpFee extends CPFP {
}
async checkPossibilityOfRBFBumpFee() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) {
let tx;
if (this.state.wallet?.type === WatchOnlyWallet.type && this.state.wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
tx = new HDSegwitBech32Transaction(
null,
this.state.txid,
this.state.wallet._hdWalletInstance,
this.state.wallet.getMasterFingerprint(),
);
} else if (this.state.wallet?.type === HDSegwitBech32Wallet.type) {
tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet);
} else {
return this.setState({ nonReplaceable: true, isLoading: false });
}
const tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet);
if ((await tx.isOurTransaction()) && (await tx.getRemoteConfirmationsNum()) === 0 && (await tx.isSequenceReplaceable())) {
const info = await tx.getInfo();
return this.setState({ nonReplaceable: false, feeRate: info.feeRate + 1, isLoading: false, tx });
@ -53,7 +62,34 @@ export default class RBFBumpFee extends CPFP {
const tx = this.state.tx;
this.setState({ isLoading: true });
try {
const { tx: newTx } = await tx.createRBFbumpFee(newFeeRate);
const { tx: newTx, psbt } = await tx.createRBFbumpFee(newFeeRate);
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
// user whether he wants to broadcast it
if (this.state.wallet?.type === WatchOnlyWallet.type && this.state.wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
let memo;
// porting memo from old tx:
if (this.context.txMetadata[this.state.txid]?.memo) {
memo = this.context.txMetadata[this.state.txid]?.memo;
}
this.props.navigation
.getParent()
?.getParent()
?.navigate('SendDetailsRoot', {
screen: 'PsbtWithHardwareWallet',
params: {
memo,
walletID: this.state.wallet.getID(),
psbt,
launchedBy: this.props.route?.params?.launchedBy,
},
});
this.setState({ isLoading: false });
return;
}
this.setState({ stage: 2, txhex: newTx.toHex(), newTxid: newTx.getId() });
this.setState({ isLoading: false });
} catch (_) {

View File

@ -10,6 +10,7 @@ import loc from '../../loc';
import CPFP from './CPFP';
import { StorageContext } from '../../components/Context/StorageProvider';
import { BlueSpacing20 } from '../../components/BlueSpacing';
import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
export default class RBFCancel extends CPFP {
static contextType = StorageContext;
@ -24,11 +25,19 @@ export default class RBFCancel extends CPFP {
}
async checkPossibilityOfRBFCancel() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) {
let tx;
if (this.state.wallet?.type === WatchOnlyWallet.type && this.state.wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
tx = new HDSegwitBech32Transaction(
null,
this.state.txid,
this.state.wallet._hdWalletInstance,
this.state.wallet.getMasterFingerprint(),
);
} else if (this.state.wallet?.type === HDSegwitBech32Wallet.type) {
tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet);
} else {
return this.setState({ nonReplaceable: true, isLoading: false });
}
const tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet);
if (
(await tx.isOurTransaction()) &&
(await tx.getRemoteConfirmationsNum()) === 0 &&
@ -36,7 +45,6 @@ export default class RBFCancel extends CPFP {
(await tx.canCancelTx())
) {
const info = await tx.getInfo();
console.log({ info });
return this.setState({ nonReplaceable: false, feeRate: info.feeRate + 1, isLoading: false, tx });
// 1 sat makes a lot of difference, since sometimes because of rounding created tx's fee might be insufficient
} else {
@ -51,7 +59,37 @@ export default class RBFCancel extends CPFP {
const tx = this.state.tx;
this.setState({ isLoading: true });
try {
const { tx: newTx } = await tx.createRBFcancelTx(newFeeRate);
const { tx: newTx, psbt } = await tx.createRBFcancelTx(newFeeRate);
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
// user whether he wants to broadcast it
if (this.state.wallet?.type === WatchOnlyWallet.type && this.state.wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
let memo;
// porting tx memo
if (this.context.txMetadata[this.state.txid]?.memo) {
memo = 'Cancelled: ' + this.context.txMetadata[this.state.txid]?.memo;
} else {
memo = 'Cancelled transaction';
}
this.props.navigation
.getParent()
?.getParent()
?.navigate('SendDetailsRoot', {
screen: 'PsbtWithHardwareWallet',
params: {
memo,
walletID: this.state.wallet.getID(),
psbt,
launchedBy: this.props.route?.params?.launchedBy,
},
});
this.setState({ isLoading: false });
return;
}
this.setState({ stage: 2, txhex: newTx.toHex(), newTxid: newTx.getId() });
this.setState({ isLoading: false });
} catch (_) {

View File

@ -33,6 +33,7 @@ import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { isOnChainTransaction, resolveTxDisplayState } from '../../blue_modules/transactionDisplayState';
import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
dayjs.extend(relativeTime);
@ -662,7 +663,13 @@ const TransactionStatus: React.FC = () => {
return setIsCPFPPossible(ButtonStatus.NotPossible);
}
const cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet as HDSegwitBech32Wallet);
let cpfbTx: HDSegwitBech32Transaction;
if (wallet?.type === WatchOnlyWallet.type && wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet._hdWalletInstance);
} else {
cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet as HDSegwitBech32Wallet);
}
if ((await cpfbTx.isToUsTransaction()) && (await cpfbTx.getRemoteConfirmationsNum()) === 0) {
return setIsCPFPPossible(ButtonStatus.Possible);
} else {
@ -678,7 +685,12 @@ const TransactionStatus: React.FC = () => {
return setIsRBFBumpFeePossible(ButtonStatus.NotPossible);
}
const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet as HDSegwitBech32Wallet);
let rbfTx: HDSegwitBech32Transaction;
if (wallet?.type === WatchOnlyWallet.type && wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet._hdWalletInstance);
} else {
rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet as HDSegwitBech32Wallet);
}
if (
(await rbfTx.isOurTransaction()) &&
(await rbfTx.getRemoteConfirmationsNum()) === 0 &&
@ -699,7 +711,12 @@ const TransactionStatus: React.FC = () => {
return setIsRBFCancelPossible(ButtonStatus.NotPossible);
}
const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet as HDSegwitBech32Wallet);
let rbfTx: HDSegwitBech32Transaction;
if (wallet?.type === WatchOnlyWallet.type && wallet?._hdWalletInstance?.type === HDSegwitBech32Wallet.type) {
rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet._hdWalletInstance);
} else {
rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet as HDSegwitBech32Wallet);
}
if (
(await rbfTx.isOurTransaction()) &&
(await rbfTx.getRemoteConfirmationsNum()) === 0 &&