Skip to content
Snippets Groups Projects
Commit b8dd0fd0 authored by Vincent Texier's avatar Vincent Texier
Browse files

[feat] add button displaying unclaimed balance

user can click on button to claim UDs
db updated by migration
parent ed4da33d
No related branches found
No related tags found
No related merge requests found
Showing
with 244 additions and 18 deletions
......@@ -183,7 +183,7 @@ To test on a local docker image, specify pull policy:
Run a gdev local node:
docker run -p 9944:9944 duniter/duniter-v2s-gdev
docker run -p 9944:9944 -e DUNITER_VALIDATOR=true duniter/duniter-v2s-gdev-800
Set connexion to url `ws://localhost:9944`.
......
......@@ -121,6 +121,7 @@ def test_v1_import_wizard_with_identity(
address=source_account_address,
old_address=None,
status=IdentityStatus.MEMBER,
first_eligible_ud=0,
)
with qtbot.waitSignal(
......@@ -254,6 +255,7 @@ def test_v1_import_wizard_with_identity(
address=destination_account_address,
old_address=None,
status=IdentityStatus.MEMBER,
first_eligible_ud=0,
)
# transfer network response mock success
......
......@@ -56,7 +56,7 @@ class NetworkAccounts(NetworkAccountsInterface):
if result.meta_info["result_found"] is False:
balance = None
else:
balance = result.value["data"]["free"]
balance = result.value["data"]["free"] + result.value["data"]["reserved"]
return balance
......@@ -86,6 +86,54 @@ class NetworkAccounts(NetworkAccountsInterface):
if value_obj.meta_info["result_found"] is False:
balances[storage_key.params[0]] = None
else:
balances[storage_key.params[0]] = value_obj.value["data"]["free"]
balances[storage_key.params[0]] = (
value_obj.value["data"]["free"]
+ value_obj.value["data"]["reserved"]
)
return balances
def get_unclaimed_ud_balance(self, first_eligible_ud: int) -> int:
__doc__ = ( # pylint: disable=redefined-builtin, unused-variable
NetworkAccountsInterface.get_unclaimed_ud_balance.__doc__
)
if not self.connections.is_connected() or self.connections.rpc.client is None:
raise NetworkAccountsException(NetworkConnectionError())
if first_eligible_ud == 0:
return 0
try:
result = self.connections.rpc.client.query(
"UniversalDividend", "CurrentUdIndex"
)
except Exception as exception:
logging.exception(exception)
raise NetworkAccountsException(exception)
current_index = result.value
try:
result = self.connections.rpc.client.query(
"UniversalDividend", "PastReevals"
)
except Exception as exception:
logging.exception(exception)
raise NetworkAccountsException(exception)
if result.meta_info["result_found"] is False:
return 0
balance = 0
index = current_index
for reeval_index, reeval_value in reversed(result.value):
if reeval_index <= first_eligible_ud:
count = index - first_eligible_ud
balance += count * reeval_value
break
else:
count = index - reeval_index
balance += count * reeval_value
index = reeval_index
return balance
......@@ -111,6 +111,7 @@ class NetworkIdentities(NetworkIdentitiesInterface):
status=self.status_map[result["status"].value],
address=result["owner_key"].value,
old_address=old_address,
first_eligible_ud=result.value["data"]["first_eligible_ud"],
)
def get_identities(self, identity_indice: List[int]) -> List[Optional[Identity]]:
......@@ -196,6 +197,7 @@ class NetworkIdentities(NetworkIdentitiesInterface):
status=self.status_map[value_obj["status"].value],
address=value_obj["owner_key"].value,
old_address=old_address,
first_eligible_ud=value_obj.value["data"]["first_eligible_ud"],
)
)
else:
......@@ -307,3 +309,42 @@ class NetworkIdentities(NetworkIdentitiesInterface):
)
return certifications
def claim_uds(self, keypair: Keypair) -> None:
__doc__ = ( # pylint: disable=redefined-builtin, unused-variable
NetworkIdentitiesInterface.claim_uds.__doc__
)
if not self.connections.is_connected() or self.connections.rpc.client is None:
raise NetworkIdentitiesException(NetworkConnectionError())
try:
# create raw call (extrinsic)
call = self.connections.rpc.client.compose_call(
call_module="UniversalDividend",
call_function="claim_uds",
)
except Exception as exception:
logging.exception(exception)
raise NetworkIdentitiesException(exception)
try:
# create extrinsic signed by current owner
extrinsic = self.connections.rpc.client.create_signed_extrinsic(
call=call, keypair=keypair
)
except Exception as exception:
logging.exception(exception)
raise NetworkIdentitiesException(exception)
try:
print("send claim uds...")
result = self.connections.rpc.client.submit_extrinsic(
extrinsic, wait_for_inclusion=True
)
except SubstrateRequestException as exception:
logging.exception(exception)
raise NetworkIdentitiesException(exception)
print("claim uds sent !")
if result.is_success is False:
logging.error(result.error_message)
raise NetworkIdentitiesException(result.error_message["name"])
......@@ -7,4 +7,5 @@ drop table if exists nodes;
drop table if exists identities;
drop table if exists categories;
drop table if exists passwords;
drop table if exists smiths;
drop table if exists authorities;
ALTER TABLE identities ADD COLUMN first_eligible_ud integer not null default 0;
......@@ -23,6 +23,7 @@ from tikka.domains.currencies import Currencies
from tikka.domains.entities.account import Account
from tikka.domains.entities.constants import DERIVATION_SCAN_MAX_NUMBER
from tikka.domains.entities.events import AccountEvent
from tikka.domains.entities.identity import Identity
from tikka.domains.events import EventDispatcher
from tikka.domains.passwords import Passwords
from tikka.domains.wallets import Wallets
......@@ -264,6 +265,17 @@ class Accounts:
"""
return self.network.get_balances(addresses)
def network_get_unclaimed_ud_balance(self, identity: Identity) -> int:
"""
Return the unclaimed UD balance of identity from network
:param identity: Identity instance
:return:
"""
balance = self.network.get_unclaimed_ud_balance(identity.first_eligible_ud)
return balance
def list_by_category_id(self, category_id: Optional[UUID]) -> List[Account]:
"""
Return all accounts in category_id
......
......@@ -33,6 +33,7 @@ class Identity:
status: IdentityStatus
address: str
old_address: Optional[str]
first_eligible_ud: int
@dataclass
......
......@@ -51,6 +51,7 @@ class Identities:
address: str,
old_address: Optional[str],
status: IdentityStatus = IdentityStatus.UNCONFIRMED,
first_eligible_ud: int = 0,
):
"""
Return an identity instance from params
......@@ -61,6 +62,7 @@ class Identities:
:param address: Account address
:param old_address: Previous account address
:param status: Identity status
:param first_eligible_ud: First elligible UD index
:return:
"""
return Identity(
......@@ -70,6 +72,7 @@ class Identities:
status=status,
address=address,
old_address=old_address,
first_eligible_ud=first_eligible_ud,
)
def add(self, identity: Identity):
......@@ -236,3 +239,12 @@ class Identities:
:return:
"""
return self.network.certs_by_receiver(receiver_address, receiver_identity_index)
def network_claim_uds(self, keypair: Keypair) -> None:
"""
Add unclaimed UDs of identity to keypair account balance
:param keypair: Keypair of account
:return:
"""
return self.network.claim_uds(keypair)
......@@ -57,6 +57,16 @@ class NetworkAccountsInterface(abc.ABC):
"""
raise NotImplementedError
@abc.abstractmethod
def get_unclaimed_ud_balance(self, first_eligible_ud: int) -> int:
"""
Return the balance amount of the unclaimed UDs
:param first_eligible_ud: First eligible UD index of identity
:return:
"""
raise NotImplementedError
class NetworkAccountsException(Exception):
"""
......
......@@ -104,6 +104,16 @@ class NetworkIdentitiesInterface(abc.ABC):
"""
raise NotImplementedError
@abc.abstractmethod
def claim_uds(self, keypair: Keypair) -> None:
"""
Add unclaimed UDs of identity to keypair account balance
:param keypair: Keypair of account
:return:
"""
raise NotImplementedError
class NetworkIdentitiesException(Exception):
"""
......
......@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>615</width>
<height>274</height>
<height>275</height>
</rect>
</property>
<property name="contextMenuPolicy">
......@@ -45,16 +45,36 @@
</widget>
</item>
<item>
<widget class="QLabel" name="balanceLabel">
<property name="font">
<font>
<pointsize>40</pointsize>
</font>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="balanceLabel">
<property name="font">
<font>
<pointsize>40</pointsize>
</font>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="claimUdsButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Click to claim your Universal Dividends</string>
</property>
<property name="text">
<string>Claim UDs</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer">
......
......@@ -16,9 +16,9 @@ import sys
from typing import Optional
import qrcode
from PyQt5.QtCore import QMutex, QPoint
from PyQt5.QtCore import QEvent, QMutex, QPoint
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow, QWidget
from tikka.domains.application import Application
from tikka.domains.entities.account import Account
......@@ -38,6 +38,7 @@ from tikka.slots.pyqt.entities.qrcode_image import QRCodeImage
from tikka.slots.pyqt.entities.worker import AsyncQWorker
from tikka.slots.pyqt.resources.gui.widgets.account_rc import Ui_AccountWidget
from tikka.slots.pyqt.widgets.account_menu import AccountPopupMenu
from tikka.slots.pyqt.windows.account_unlock import AccountUnlockWindow
from tikka.slots.pyqt.windows.transfer import TransferWindow
......@@ -69,6 +70,7 @@ class AccountWidget(QWidget, Ui_AccountWidget):
self.account = account
self.mutex = mutex
self.unclaimed_ud_balance = 0
self.monospace_font = QFont(ADDRESS_MONOSPACE_FONT_NAME)
self.monospace_font.setStyleHint(QFont.Monospace)
......@@ -95,11 +97,18 @@ class AccountWidget(QWidget, Ui_AccountWidget):
self.fetch_from_network_async_qworker.finished.connect(
self._on_finished_fetch_from_network
)
self.network_claim_uds_async_qworker = AsyncQWorker(
self.network_claim_uds, self.mutex
)
self.network_claim_uds_async_qworker.finished.connect(
self._on_finished_network_claim_uds
)
# events
self.refreshButton.clicked.connect(self.fetch_from_network_async_qworker.start)
self.transferToButton.clicked.connect(self.transfer)
self.customContextMenuRequested.connect(self.on_context_menu)
self.claimUdsButton.clicked.connect(self.on_claim_uds_button_clicked)
# application events
self.application.event_dispatcher.add_event_listener(
......@@ -159,7 +168,9 @@ class AccountWidget(QWidget, Ui_AccountWidget):
else:
if identity_index is not None:
try:
self.application.identities.network_get_identity(identity_index)
identity = self.application.identities.network_get_identity(
identity_index
)
except Exception as exception:
self.errorLabel.setText(self._(str(exception)))
else:
......@@ -168,6 +179,11 @@ class AccountWidget(QWidget, Ui_AccountWidget):
except Exception as exception:
self.errorLabel.setText(self._(str(exception)))
if identity is not None:
self.unclaimed_ud_balance = self.application.accounts.network_get_unclaimed_ud_balance(
identity
)
def _on_finished_fetch_from_network(self):
"""
Triggered when async request fetch_from_network is finished
......@@ -219,6 +235,14 @@ class AccountWidget(QWidget, Ui_AccountWidget):
amount.value(self.account.balance), amount.symbol()
)
)
if self.unclaimed_ud_balance > 0:
display_unclaimed_uds_balance = self.locale().toCurrencyString(
amount.value(self.unclaimed_ud_balance), amount.symbol()
)
self.claimUdsButton.setText(f"+{display_unclaimed_uds_balance}")
self.claimUdsButton.show()
else:
self.claimUdsButton.hide()
if self.application.wallets.exists(self.account.address):
if self.application.wallets.is_unlocked(self.account.address):
......@@ -265,6 +289,50 @@ class AccountWidget(QWidget, Ui_AccountWidget):
menu = AccountPopupMenu(self.application, self.account, self.mutex, self)
menu.exec_(self.mapToGlobal(position))
def on_claim_uds_button_clicked(self, event: QEvent):
"""
Triggered when user click on claim uds button
:param event: QEvent instance
:return:
"""
if not self.application.connections.is_connected():
return
# if account locked...
if not self.application.wallets.is_unlocked(self.account.address):
# ask password...
dialog_code = AccountUnlockWindow(
self.application, self.account, self
).exec_()
if dialog_code == QDialog.Rejected:
return
self.network_claim_uds_async_qworker.start()
def network_claim_uds(self):
"""
Send claim UDs request to network
:return:
"""
self.claimUdsButton.setDisabled(True)
try:
self.application.identities.network_claim_uds(
self.application.wallets.get_keypair(self.account.address)
)
except Exception as exception:
self.errorLabel.setText(self._(str(exception)))
def _on_finished_network_claim_uds(self):
"""
Triggered when the claim uds function is finished
:return:
"""
self.claimUdsButton.setDisabled(False)
self.fetch_from_network_async_qworker.start()
if __name__ == "__main__":
qapp = QApplication(sys.argv)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment