Skip to content

Commit

Permalink
Cancel transaction with RBF (#1197)
Browse files Browse the repository at this point in the history
* Cancel transaction with RBF

* Fix condition

* Show cancelled received tx as cancelled
  • Loading branch information
ben-kaufman authored May 28, 2021
1 parent fb64940 commit fee766c
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 46 deletions.
23 changes: 19 additions & 4 deletions src/cryptoadvance/specter/server_endpoints/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,10 +670,9 @@ def send_new(wallet_alias):
# calculate new amount if we need to subtract
if subtract:
for v in psbt["tx"]["vout"]:
if (
addresses[0] in v["scriptPubKey"]["addresses"]
or addresses[0] == v["scriptPubKey"]["address"]
):
if addresses[0] in v["scriptPubKey"].get(
"addresses", [""]
) or addresses[0] == v["scriptPubKey"].get("address", ""):
amounts[0] = v["value"]
except Exception as e:
err = e
Expand Down Expand Up @@ -709,6 +708,22 @@ def send_new(wallet_alias):
)
except Exception as e:
flash("Failed to perform RBF. Error: %s" % e, "error")
elif action == "rbf_cancel":
try:
rbf_tx_id = request.form["rbf_tx_id"]
rbf_fee_rate = float(request.form["rbf_fee_rate"])
psbt = wallet.canceltx(rbf_tx_id, rbf_fee_rate)
return render_template(
"wallet/send/sign/wallet_send_sign_psbt.jinja",
psbt=psbt,
labels=[],
wallet_alias=wallet_alias,
wallet=wallet,
specter=app.specter,
rand=rand,
)
except Exception as e:
flash("Failed to cancel transaction with RBF. Error: %s" % e, "error")
elif action == "rbf_edit":
try:
decoded_tx = wallet.decode_tx(rbf_tx_id)
Expand Down
4 changes: 4 additions & 0 deletions src/cryptoadvance/specter/static/img/cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/cryptoadvance/specter/templates/includes/tx-data.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ <h2>Transaction details</h2><br>
<tr><td>Fee rate:</td><td>${parseFloat((-1e8 * tx.fee / rawtx.vsize).toFixed(2)).toString()} sat/vbyte</td></tr>
`;
}
if (tx.confirmations) {
if (tx.confirmations && tx.confirmations > 0) {
rawtxHTML += `
<tr><td>Mined at block:</td><td>${tx.blockheight}</td></tr>
<tr><td>Block hash:</td><td style="word-break: break-all;">${tx.blockhash}</td></tr>
Expand Down
107 changes: 67 additions & 40 deletions src/cryptoadvance/specter/templates/includes/tx-row.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
.svg-selftransfer {
filter: invert(77%) sepia(68%) saturate(3384%) hue-rotate(44deg) brightness(112%) contrast(74%);
}
.svg-cancelled {
filter: invert(20%) sepia(32%) saturate(4838%) hue-rotate(332deg) brightness(81%) contrast(97%);
}
</style>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
<tr class="tx-row">
Expand All @@ -27,7 +30,8 @@
<td class="time"></td>
<td>
<span class="confirmations"></span>
<button class="rbf btn optional hidden" style="width: 120px; float: right;" type="button">Speed up</button>
<button class="rbf btn optional hidden" style="width: 130px; float: right;" type="button">Speed up</button>
<button class="rbf-cancel danger btn optional hidden" style="width: 130px; float: right; margin-right: 10px;" type="button">Cancel transaction</button>
</td>
<td class="hidden optional blockhash"></td>
<td><input class="select-tx-value" type="hidden" value=""><img style="vertical-align: middle;" class="select-tx-img" src="{{ url_for('static', filename='img/checkbox-untick.svg') }}" width="25px"></tool-tip></td>
Expand All @@ -52,6 +56,7 @@
this.confirmations = clone.querySelector(".confirmations");
this.blockhash = clone.querySelector(".blockhash");
this.rbf = clone.querySelector(".rbf");
this.rbfCancel = clone.querySelector(".rbf-cancel");
this.selectTxValue = clone.querySelector(".select-tx-value");
this.selectTxImg = clone.querySelector(".select-tx-img");
this.frozenImg = clone.querySelector(".frozen-img");
Expand Down Expand Up @@ -143,55 +148,32 @@
// Set confirmations
if (this.tx.confirmations > 0) {
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `${this.tx.confirmations}<span class="optional"> Confirmations</span>`;
} else {
} else if (this.tx.confirmations == 0) {
this.el.classList.add('unconfirmed');
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `Unconfirmed`;
} else {
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `Cancelled (replaced by sender)`;
this.category.src = `{{ url_for('static', filename='img') }}/cross.svg`;
this.category.classList.remove('svg-' + this.tx.category);
this.category.classList.add('svg-cancelled');
}

if ((this.tx.category == "send" || this.tx.category == "selftransfer") && this.tx["bip125-replaceable"] == "yes") {
this.rbf.classList.remove('hidden');
this.rbf.onclick = () => {
let txDataPopup = document.getElementById('tx-popup');
let url = `{{ url_for('wallets_endpoint.send_new', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
let newFee = parseFloat((((this.tx.fee * -1) / this.tx.vsize) * 1e8).toFixed(2));
if (newFee <= 1.02) {
newFee = 1;
}
newFee += 2;
txDataPopup.innerHTML = `
<form action="${url}" method="POST">
<h1>Speed up the Transaction</h1>
<p>You can speedup the transaction by increasing its fee rate:</p>
<input type="hidden" name="rbf_tx_id" value='${this.tx.txid}' />
<input type="number" min="${newFee}" value="${newFee}" class="fee_rate" name="rbf_fee_rate" id="rbf_fee_rate" min="1" step="any" autocomplete="off"> sat/vbyte
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<br>
<br>
<button type="submit" name="action" value="rbf" class="btn centered">Speed up!</button>
<br>
<span class="toggle_advanced_rbf" style="cursor: pointer;">Advanced {% if show_advanced_settings %}&#9660;{% else %}&#9654;{% endif %}</span>
<div class="advanced_rbf hidden warning">
<p style="max-width: 400px;">If you would like further customization, you can click here to fully edit the transaction.<br>(advanced, not recommended for new users)</p>
<button type="submit" name="action" value="rbf_edit" class="btn centered">Edit the transaction (advanced)</button>
</div>
</form>
`;
txDataPopup.querySelector('.toggle_advanced_rbf').onclick = () => {
let advancedButton = txDataPopup.querySelector('.toggle_advanced_rbf')
let advancedSettings = txDataPopup.querySelector('.advanced_rbf')
if (advancedSettings.classList.contains('hidden')) {
advancedSettings.classList.remove('hidden')
advancedButton.innerHTML = 'Advanced &#9660;';
} else {
advancedSettings.classList.add('hidden')
advancedButton.innerHTML = 'Advanced &#9654;';
}
if (this.tx.category == "selftransfer") {
this.rbfCancel.classList.add('hidden');
} else {
this.rbfCancel.classList.remove('hidden');
this.rbfCancel.onclick = () => {
rbfPopup(this, 'cancel');
}

showPageOverlay('tx-popup');
}
this.rbf.onclick = () => {
rbfPopup(this, 'speedup');
}
} else {
this.rbf.classList.add('hidden');
this.rbfCancel.classList.add('hidden');
}

// Set time
Expand Down Expand Up @@ -233,6 +215,51 @@ <h1>Speed up the Transaction</h1>
}
}

function rbfPopup(self, rbfType) {
let txDataPopup = document.getElementById('tx-popup');
let url = `{{ url_for('wallets_endpoint.send_new', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", self.wallet);
let newFee = parseFloat((((self.tx.fee * -1) / self.tx.vsize) * 1e8).toFixed(2));
if (newFee <= 1.02) {
newFee = 1;
}
if (rbfType == 'cancel') {
newFee *= 1.5; // Tx likely to have lower size so will need higher fee
} else {
newFee += 2;
}
txDataPopup.innerHTML = `
<form action="${url}" method="POST">
<h1>${rbfType == 'cancel' ? 'Cancel' : 'Speed up'} the Transaction</h1>
<p>You can ${rbfType == 'cancel' ? 'cancel' : 'speed up'} the transaction by increasing its fee rate${rbfType == 'cancel' ? ' and sending the funds back to yourself' : ''}:</p>
<input type="hidden" name="rbf_tx_id" value='${self.tx.txid}' />
<input type="number" min="${newFee}" value="${newFee}" class="fee_rate" name="rbf_fee_rate" id="rbf_fee_rate" min="1" step="any" autocomplete="off"> sat/vbyte
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<br>
<br>
<button type="submit" name="action" value="${rbfType == 'cancel' ? 'rbf_cancel' : 'rbf'}" class="btn centered ${rbfType == 'cancel' ? 'danger' : ''}">${rbfType == 'cancel' ? 'Cancel transaction' : 'Speed up!'}</button>
<br>
<span class="toggle_advanced_rbf" style="cursor: pointer;">Advanced {% if show_advanced_settings %}&#9660;{% else %}&#9654;{% endif %}</span>
<div class="advanced_rbf hidden warning">
<p style="max-width: 400px;">If you would like further customization, you can click here to fully edit the transaction.<br>(advanced, not recommended for new users)</p>
<button type="submit" name="action" value="rbf_edit" class="btn centered">Edit the transaction (advanced)</button>
</div>
</form>
`;
txDataPopup.querySelector('.toggle_advanced_rbf').onclick = () => {
let advancedButton = txDataPopup.querySelector('.toggle_advanced_rbf')
let advancedSettings = txDataPopup.querySelector('.advanced_rbf')
if (advancedSettings.classList.contains('hidden')) {
advancedSettings.classList.remove('hidden')
advancedButton.innerHTML = 'Advanced &#9660;';
} else {
advancedSettings.classList.add('hidden')
advancedButton.innerHTML = 'Advanced &#9654;';
}
}

showPageOverlay('tx-popup');
}

function getCategoryImg(category, isConfirmed) {
if (!isConfirmed) {
return `{{ url_for('static', filename='img') }}/clock.svg`;
Expand Down
33 changes: 32 additions & 1 deletion src/cryptoadvance/specter/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,9 @@ def txlist(
tx.get("confirmations") == 0
and tx.get("bip125-replaceable", "no") == "yes"
):
tx["fee"] = self.rpc.gettransaction(tx["txid"]).get("fee", 1)
rpc_tx = self.rpc.gettransaction(tx["txid"])
tx["fee"] = rpc_tx.get("fee", 1)
tx["confirmations"] = rpc_tx.get("confirmations", 0)

if isinstance(tx["address"], str):
tx["label"] = self.getlabel(tx["address"])
Expand Down Expand Up @@ -1471,6 +1473,35 @@ def decode_tx(self, txid):
],
}

def canceltx(self, txid, fee_rate):
self.check_unused()
raw_tx = self.gettransaction(txid)["hex"]
raw_psbt = self.rpc.utxoupdatepsbt(
self.rpc.converttopsbt(raw_tx, True),
[self.recv_descriptor, self.change_descriptor],
)

psbt = self.rpc.decodepsbt(raw_psbt)
decoded_tx = self.decode_tx(txid)
selected_coins = [
f"{utxo['txid']}, {utxo['vout']}" for utxo in decoded_tx["used_utxo"]
]
return self.createpsbt(
addresses=[self.address],
amounts=[
sum(
vout["witness_utxo"]["amount"]
for i, vout in enumerate(psbt["inputs"])
)
],
subtract=True,
fee_rate=float(fee_rate),
selected_coins=selected_coins,
readonly=False,
rbf=True,
rbf_edit_mode=True,
)

def bumpfee(self, txid, fee_rate):
raw_tx = self.gettransaction(txid)["hex"]
raw_psbt = self.rpc.utxoupdatepsbt(
Expand Down

0 comments on commit fee766c

Please sign in to comment.