diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index c49f4683d..5914acea0 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -47,7 +47,7 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum.transaction import SerializationError, Transaction, PartialTransaction, TxOutpoint, TxinDataFetchProgress from electrum.logging import get_logger -from electrum.util import ShortID, get_asyncio_loop, UI_UNIT_NAME_TXSIZE_VBYTES +from electrum.util import ShortID, get_asyncio_loop, UI_UNIT_NAME_TXSIZE_VBYTES, delta_time_str from electrum.network import Network from electrum.wallet import TxSighashRiskLevel, TxSighashDanger @@ -862,6 +862,19 @@ class TxDialog(QDialog, MessageBoxMixin): locktime_final_str = _("LockTime: {} ({})").format(self.tx.locktime, locktime_str) self.locktime_final_label.setText(locktime_final_str) + nsequence_time = self.tx.get_time_based_relative_locktime() + nsequence_blocks = self.tx.get_block_based_relative_locktime() + if nsequence_time or nsequence_blocks: + if nsequence_time: + seconds = nsequence_time * 512 + time_str = delta_time_str(datetime.timedelta(seconds=seconds)) + else: + time_str = '{} blocks'.format(nsequence_blocks) + nsequence_str = _("Relative locktime: {}").format(time_str) + self.nsequence_label.setText(nsequence_str) + else: + self.nsequence_label.hide() + # TODO: 'Yes'/'No' might be better translatable than 'True'/'False'? self.rbf_label.setText(_('Replace by fee: {}').format(_('True') if self.tx.is_rbf_enabled() else _('False'))) @@ -1001,6 +1014,9 @@ class TxDialog(QDialog, MessageBoxMixin): self.locktime_final_label = TxDetailLabel() vbox_right.addWidget(self.locktime_final_label) + self.nsequence_label = TxDetailLabel() + vbox_right.addWidget(self.nsequence_label) + self.block_height_label = TxDetailLabel() vbox_right.addWidget(self.block_height_label) vbox_right.addStretch(1) diff --git a/electrum/transaction.py b/electrum/transaction.py index 82f0afbfa..0df5276ce 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -349,6 +349,19 @@ class TxInput: self.__address = None # type: Optional[str] self.__value_sats = None # type: Optional[int] + def get_time_based_relative_locktime(self) -> Optional[int]: + # see bip 68 + if self.nsequence & (1<<31): + return + if self.nsequence & (1<<22): + return self.nsequence & 0xffff + + def get_block_based_relative_locktime(self) -> Optional[int]: + if self.nsequence & (1<<31): + return + if not self.nsequence & (1<<22): + return self.nsequence & 0xffff + @property def short_id(self): if self.block_txpos is not None and self.block_txpos >= 0: @@ -1136,6 +1149,14 @@ class Transaction: return False return True + def get_time_based_relative_locktime(self) -> Optional[int]: + locktimes = list(filter(None, [txin.get_time_based_relative_locktime() for txin in self.inputs()])) + return max(locktimes) if locktimes else None + + def get_block_based_relative_locktime(self) -> Optional[int]: + locktimes = list(filter(None, [txin.get_block_based_relative_locktime() for txin in self.inputs()])) + return max(locktimes) if locktimes else None + def is_rbf_enabled(self) -> bool: """Whether the tx explicitly signals BIP-0125 replace-by-fee.""" return any([txin.nsequence < 0xffffffff - 1 for txin in self.inputs()]) diff --git a/electrum/util.py b/electrum/util.py index 7a4451f9f..ef6c7dfc0 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -28,7 +28,7 @@ from collections import defaultdict, OrderedDict from concurrent.futures.process import ProcessPoolExecutor from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, Sequence, Dict, Generic, TypeVar, List, Iterable, Set, Awaitable) -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta import decimal from decimal import Decimal import urllib @@ -875,72 +875,41 @@ def age( """Takes a timestamp and returns a string with the approximation of the age""" if from_date is None: return _("Unknown") - from_date = datetime.fromtimestamp(from_date) if since_date is None: since_date = datetime.now(target_tz) - distance_in_time = from_date - since_date is_in_past = from_date < since_date + s = delta_time_str(distance_in_time) + return _("{} ago").format(s) if is_in_past else _("in {}").format(s) + + +def delta_time_str(distance_in_time: timedelta) -> str: distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds))) distance_in_minutes = int(round(distance_in_seconds / 60)) - if distance_in_minutes == 0: if include_seconds: - if is_in_past: - return _("{} seconds ago").format(distance_in_seconds) - else: - return _("in {} seconds").format(distance_in_seconds) + return _("{} seconds").format(distance_in_seconds) else: - if is_in_past: - return _("less than a minute ago") - else: - return _("in less than a minute") + return _("less than a minute") elif distance_in_minutes < 45: - if is_in_past: - return _("about {} minutes ago").format(distance_in_minutes) - else: - return _("in about {} minutes").format(distance_in_minutes) + return _("about {} minutes").format(distance_in_minutes) elif distance_in_minutes < 90: - if is_in_past: - return _("about 1 hour ago") - else: - return _("in about 1 hour") + return _("about 1 hour") elif distance_in_minutes < 1440: - if is_in_past: - return _("about {} hours ago").format(round(distance_in_minutes / 60.0)) - else: - return _("in about {} hours").format(round(distance_in_minutes / 60.0)) + return _("about {} hours").format(round(distance_in_minutes / 60.0)) elif distance_in_minutes < 2880: - if is_in_past: - return _("about 1 day ago") - else: - return _("in about 1 day") + return _("about 1 day") elif distance_in_minutes < 43220: - if is_in_past: - return _("about {} days ago").format(round(distance_in_minutes / 1440)) - else: - return _("in about {} days").format(round(distance_in_minutes / 1440)) + return _("about {} days").format(round(distance_in_minutes / 1440)) elif distance_in_minutes < 86400: - if is_in_past: - return _("about 1 month ago") - else: - return _("in about 1 month") + return _("about 1 month") elif distance_in_minutes < 525600: - if is_in_past: - return _("about {} months ago").format(round(distance_in_minutes / 43200)) - else: - return _("in about {} months").format(round(distance_in_minutes / 43200)) + return _("about {} months").format(round(distance_in_minutes / 43200)) elif distance_in_minutes < 1051200: - if is_in_past: - return _("about 1 year ago") - else: - return _("in about 1 year") + return _("about 1 year") else: - if is_in_past: - return _("over {} years ago").format(round(distance_in_minutes / 525600)) - else: - return _("in over {} years").format(round(distance_in_minutes / 525600)) + return _("over {} years").format(round(distance_in_minutes / 525600)) mainnet_block_explorers = { '3xpl.com': ('https://3xpl.com/bitcoin/',