Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel transaction with RBF #1197

Merged
merged 3 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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