Compare commits
5 Commits
master
...
enable-rbf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68cdefe849 | ||
|
|
980ad492be | ||
|
|
05c278c915 | ||
|
|
c316a9fd75 | ||
|
|
0a64093a08 |
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 (_) {
|
||||
|
||||
@ -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 (_) {
|
||||
|
||||
@ -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 &&
|
||||
|
||||
Loading…
Reference in New Issue
Block a user