diff --git a/.gitignore b/.gitignore index 6e52fa4522039a6336bc278f9d2b6217f194a35e..7ff4882a41ca87f23f0d46663f615bc66a0adaba 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ res/i18n/lang-* out .directory temp +*_uic.py \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 0b22a0d50fa101b4efb365076ed7f9f8454a1b6b..42b0704102dc58a6571cb0f76274f42197e31eb0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,9 +38,6 @@ install: - "%CMD_IN_ENV% conda config --add channels inso/channel/sakia" - "%CMD_IN_ENV% conda create -q -n test-environment python=%PYTHON_VERSION% libsodium=1.0.3 setuptools=19.2" -cache: - - C:\Qt\5.6\5.6 - build_script: - ".\\ci\\appveyor\\build.cmd" diff --git a/ci/appveyor/build.cmd b/ci/appveyor/build.cmd index 49965b50196df5d37a81ca8873326f912b4f6501..37c9a8dd3efe0a934f58e7355c17b5dabd088cd0 100644 --- a/ci/appveyor/build.cmd +++ b/ci/appveyor/build.cmd @@ -5,13 +5,12 @@ call activate test-environment echo "%PATH%" echo "%QT_PLUGIN_PATH%" python -V -call pyuic5 --version - -pyrcc5 -version lrelease -version -pip install PyQt5 +call pyuic5 --version +pyrcc5 -version + pip install -r requirements.txt pip install pyinstaller pip install six diff --git a/ci/appveyor/tests.cmd b/ci/appveyor/tests.cmd index 094bc394e38247295483cfccd0019374eccfd14e..61e3c79ae7545d9c5f931b70fee343862959e63b 100644 --- a/ci/appveyor/tests.cmd +++ b/ci/appveyor/tests.cmd @@ -11,8 +11,7 @@ pyrcc5 -version lrelease -version -echo "%CWD%" - -python setup.py test +echo "%cd%" +py.test tests/ if %errorlevel% neq 0 exit /b 1 \ No newline at end of file diff --git a/ci/travis/before_install.sh b/ci/travis/before_install.sh index d83c13a9639a392c11e4ae3637ea5ff4be54d8b3..5ff6df18644fd7d351abe1f4dd71f07a47f51a62 100755 --- a/ci/travis/before_install.sh +++ b/ci/travis/before_install.sh @@ -16,20 +16,21 @@ then brew install libsodium ## Ensure your brew QT version is up to date. (brew install qt -> qt 4.8) brew install qt5 - brew link --force qt5 brew install pyenv-virtualenv pyenv update + pip install PyQt5 elif [ $TRAVIS_OS_NAME == "linux" ] then sudo apt-get update sudo apt-get install -qq -y libxcb1 libxcb1-dev libx11-xcb1 libx11-xcb-dev libxcb-keysyms1 libxcb-keysyms1-dev libxcb-image0 \ libxcb-image0-dev libxcb-shm0 libxcb-shm0-dev libxcb-icccm4 libxcb-icccm4-dev \ libxcb-xfixes0-dev libxrender-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0 \ - libxcb-render-util0-dev libxcb-glx0-dev libgl1-mesa-dri libegl1-mesa libpcre3-dev \ - curl libdbus-1-dev libdbus-glib-1-dev autoconf automake libtool libgstreamer-plugins-base0.10-0 - wget https://download.qt.io/official_releases/qt/5.7/5.7.0/qt-opensource-linux-x64-5.7.0.run - chmod +x qt-opensource-linux-x64-5.7.0.run - ./qt-opensource-linux-x64-5.7.0.run --script $HOME/build/duniter/sakia/ci/travis/qt-installer-noninteractive.qs + libxcb-render-util0-dev libxcb-glx0-dev libgl1-mesa-dri libegl1-mesa libpcre3 libgles2-mesa-dev \ + freeglut3-dev libfreetype6-dev xorg-dev xserver-xorg-input-void xserver-xorg-video-dummy xpra libosmesa6-dev \ + curl libdbus-1-dev libdbus-glib-1-dev autoconf automake libtool libgstreamer-plugins-base0.10-0 dunst + wget https://download.qt.io/official_releases/qt/5.7/5.7.1/qt-opensource-linux-x64-5.7.1.run + chmod +x qt-opensource-linux-x64-5.7.1.run + ./qt-opensource-linux-x64-5.7.1.run --script $HOME/build/duniter/sakia/ci/travis/qt-installer-noninteractive.qs wget http://archive.ubuntu.com/ubuntu/pool/universe/libs/libsodium/libsodium13_1.0.1-1_amd64.deb sudo dpkg -i libsodium13_1.0.1-1_amd64.deb @@ -54,7 +55,4 @@ fi pyenv shell $PYENV_PYTHON_VERSION -pip install PyQt5 - -make -j 2 && make install -pyenv rehash +cd $HOME diff --git a/ci/travis/build.sh b/ci/travis/build.sh index 2cd869bb8399bdc0362effa7f2571bf84e3bb140..5b374636ea1c17decf82ec565a5bbaba8856e778 100755 --- a/ci/travis/build.sh +++ b/ci/travis/build.sh @@ -8,7 +8,9 @@ pyenv shell $PYENV_PYTHON_VERSION pip install --upgrade pip pyenv rehash pip install coveralls +pip install pytest-cov pip install pyinstaller +pip install PyQt5 pip install -r requirements.txt if [ $TRAVIS_OS_NAME == "linux" ] then diff --git a/ci/travis/debian/usr/share/applications/sakia.desktop b/ci/travis/debian/usr/share/applications/sakia.desktop index 6e6c0cbe0c4fe0860c65c733d241b879152c8956..331392380d9f7dd4cbf8a2a92b1e13b5da47a64f 100644 --- a/ci/travis/debian/usr/share/applications/sakia.desktop +++ b/ci/travis/debian/usr/share/applications/sakia.desktop @@ -7,3 +7,4 @@ Icon=/opt/sakia/sakia.png Terminal=false Type=Application Categories=Utility;Application; +MimeType=x-scheme-handler/duniter diff --git a/ci/travis/test.sh b/ci/travis/test.sh index 2ef4409d195aeceeaf55fceaf6a4846d1ec71ab5..ad1112601a75072e1566ada9e689c12f77e3369a 100755 --- a/ci/travis/test.sh +++ b/ci/travis/test.sh @@ -6,7 +6,7 @@ if [ $TRAVIS_OS_NAME == "linux" ] then export XVFBARGS="-screen 0 1280x1024x24" export DISPLAY=:99.0 - sudo sh -e /etc/init.d/xvfb restart + sh -e /etc/init.d/xvfb start sleep 3 fi @@ -14,9 +14,9 @@ cd $HOME/build/duniter/sakia pyenv shell $PYENV_PYTHON_VERSION if [ $TRAVIS_OS_NAME == "linux" ] then - coverage run --source=sakia.core,sakia.gui,sakia.models setup.py test + py.test --cov=sakia tests/ else - python setup.py test + py.test fi diff --git a/doc/uml/api.png b/doc/uml/api.png deleted file mode 100644 index f557d71281f747f665355c62849852fd427b0826..0000000000000000000000000000000000000000 Binary files a/doc/uml/api.png and /dev/null differ diff --git a/doc/uml/api.pu b/doc/uml/api.pu deleted file mode 100644 index f4a6ca46ace9b1302ecc225a51a719b5e9698648..0000000000000000000000000000000000000000 --- a/doc/uml/api.pu +++ /dev/null @@ -1,21 +0,0 @@ -@startuml - -package api { - package api.bma { - class BMADataAccess { - {static} _cache - {static} _request(req : Request, network) - {static} _post(req : Request, network) - {static} _broadcast(req : Request, network) - } - BMADataAccess ..> api.bma.API - } - package api.es { - class ESDataAccess { - } - ESDataAccess ..> api.es.API - } - -} - -@enduml \ No newline at end of file diff --git a/doc/uml/backend.png b/doc/uml/backend.png new file mode 100644 index 0000000000000000000000000000000000000000..37c008fe788da0529c54093419b0463df4c529fa Binary files /dev/null and b/doc/uml/backend.png differ diff --git a/doc/uml/backend.pu b/doc/uml/backend.pu new file mode 100644 index 0000000000000000000000000000000000000000..fdbf31b29e477d68ad5d17cba2605ee0e50fe022 --- /dev/null +++ b/doc/uml/backend.pu @@ -0,0 +1,51 @@ +@startuml + +!include data.pu +!include processors.pu +!include services.pu + +ProfileService "1" --> "1" UserParameters +ProfileService "*" --> "1" UserParametersRepo + +AccountService "1" --> "1" Key +AccountService "*" --> "1" KeyRepo + + +TransactionsService "1" --> "*" Transaction +TransactionsService "*" --> "1" TransactionProcessor +TransactionProcessor "1" --> "1" TransactionRepo + +RegistryService "1" --> "*" Identity +RegistryService "1" --> "*" Certification +RegistryService "*" --> "1" IdentitiesProcessor +IdentitiesProcessor "1" --> "1" IdentitiesRepo +RegistryService "*" --> "1" CertificationProcessor +CertificationProcessor "1" --> "1" CertificationRepo + +NetworkService "1" --> "*" Node +NetworkService "*" --> "1" NodesProcessor +NodesProcessor "1" --> "1" NodesRepo + +BlockchainService "1" --> "1" Blockchain +BlockchainService "1" --> "1" Community +BlockchainService "*" --> "1" BlockchainProcessor +BlockchainProcessor "1" --> "1" BlockchainRepo +BlockchainService "*" --> "1" CommunityProcessor +CommunityProcessor "1" --> "1" CommunityRepo + +package Connectors { + class BMAConnector << (S,cyan) >> { + get() + post() + broadcast() + } +} + +AccountService --> BMAConnector +BlockchainProcessor --> BMAConnector +CommunityProcessor --> BMAConnector +TransactionProcessor --> BMAConnector +IdentitiesProcessor --> BMAConnector +CertificationProcessor --> BMAConnector + +@enduml \ No newline at end of file diff --git a/doc/uml/core-classes.png b/doc/uml/core-classes.png deleted file mode 100644 index 54e0d69e92bc3200fa7bcd1bc6d2458cad95ff81..0000000000000000000000000000000000000000 Binary files a/doc/uml/core-classes.png and /dev/null differ diff --git a/doc/uml/core-classes.pu b/doc/uml/core-classes.pu deleted file mode 100644 index c1463421238e08d35f9872ef6c926a4aa55c5003..0000000000000000000000000000000000000000 --- a/doc/uml/core-classes.pu +++ /dev/null @@ -1,108 +0,0 @@ -@startuml - -hide fields -hide methods - -package core { - class App { - -- Signals -- - current_account_changed(str : account_name) - data_changed() - -- Slots -- - -- Properties -- - current_account - accounts - -- Methods -- - } - App --* Account : accounts - - class Account { - -- Signals -- - wallets_changed(int : nb_wallets) - community_added(int : index) - community_removed(int : index) - data_changed() - -- Slots -- - -- Properties -- - communities - wallets - -- Methods -- - } - Account "1" --* "*" Wallet - Account "1" --* "*" Community - - class Wallet { - -- Signals -- - money_received(Transfer) - money_sent(Transfer) - name_changed(str : new_name - data_changed() - -- Slots -- - -- Properties -- - transfers - -- Methods -- - } - Wallet "1" --* "*" Transfer - - class Transfer { - -- Signals -- - state_changed(int : new_state) - -- Slots -- - -- Properties -- - -- Methods -- - } - - class Community { - -- Signals -- - members_changed() - data_changed() - -- Slots -- - -- Properties -- - network - -- Methods -- - - } - App --> Identity - class Identity { - {static} _identities - {static} load(data : dict) - {static} lookup(search : str) - } - -} - - -package net { - class Network { - -- Signals -- - node_found(int : index) - node_removed(int : index) - block_found(int : block_number) - -- Slots -- - -- Properties -- - nodes - root_nodes - -- Methods -- - } - Community "1" --* "1" Network - Network "1" --* "*" Node - - class Node { - -- Signals -- - changed() - -- Slots -- - -- Properties -- - endpoints - pubkey - uid - block - state - -- Methods -- - } - - Network "1" --* "1" BmaAccess - - class BmaAccess { - } -} -@enduml diff --git a/doc/uml/cutecoin.png b/doc/uml/cutecoin.png deleted file mode 100644 index 417230847dec853471dc5e7a618273d5be5e898b..0000000000000000000000000000000000000000 Binary files a/doc/uml/cutecoin.png and /dev/null differ diff --git a/doc/uml/cutecoin.pu b/doc/uml/cutecoin.pu deleted file mode 100644 index d79f67dfb4614935631fd76663cdd922aac6b841..0000000000000000000000000000000000000000 --- a/doc/uml/cutecoin.pu +++ /dev/null @@ -1,36 +0,0 @@ -@startuml - -!include core-classes.pu -!include gui-classes.pu -!include models-classes.pu -!include api.pu - -MainWindow "1" --> "1" App - -CertificationDialog --> Community -TransferDialog --> Community - -CurrencyTab "1" --> "1" Community - -CommunityTab -right-> IdentitiesFilterProxyModel -NetworkTab -right-> NetworkFilterProxyModel -WalletTab -right-> WalletsFilterProxyModel - -WalletsFilterProxyModel -up-> Wallet -NetworkFilterProxyModel -up-> Network -TxHistoryFilterProxyModel -up-> Transfer - -ConfigureAccountDialog --> CommunitiesListModel -ConfigureCommunityDialog --> RootNodesTableModel - -ConfigureAccountDialog --> Account -ConfigureCommunityDialog --> Community - -Account ..> BMADataAccess -Community ..> BMADataAccess -Wallet ..> BMADataAccess -Transfer ..> BMADataAccess -Identity ..> BMADataAccess -BMADataAccess .left.> Network - -@enduml \ No newline at end of file diff --git a/doc/uml/data.png b/doc/uml/data.png new file mode 100644 index 0000000000000000000000000000000000000000..9926347691564e7f3e43ae5cc165e46f32d5b913 Binary files /dev/null and b/doc/uml/data.png differ diff --git a/doc/uml/data.pu b/doc/uml/data.pu new file mode 100644 index 0000000000000000000000000000000000000000..8303a44b0c7ee491550adbd5390c2deef6ebf06f --- /dev/null +++ b/doc/uml/data.pu @@ -0,0 +1,180 @@ +@startuml + + +class Identity << (D,orchid) >> { + currency: str (FK) + uid: str + pubkey: str (PK) + blockstamp: BlockUID + timestamp: int + signature: str + written_on: BlockUID + revoked_on: BlockUID + member: bool + membership_buid: BlockUID + membership_timestamp: int + membership_type: str + membership_written_on: BlockUID +} + +class Certification << (D,orchid) >> { + currency: str (PK) + certifier: str (PK) + certified: str (PK) + blockstamp: BlockUID (PK) + timestamp: int + signature: str + written_on: BlockUID +} + +class Transaction << (D,orchid) >> { + currency: str (FK) + blockstamp: str + locktime: int + issuer: str + recipient: str + amount: int + comment: str + sha_hash: str (PK) +} + +class Community << (D,orchid) >> { + profile: str (FK) + pubkey: str (FK) + currency: str (PK) + c: float + dt: int + ud0: int + sig_period: int + sig_stock: int + sig_window: int + sig_validity: int + sig_qty: int + xpercent: float + ms_validity: int + step_max: int + median_time_blocks: int + avg_gen_time: int + dt_diff_eval: int + blocks_rot: int + percent_rot: float +} + +class Blockchain << (D,orchid) >> { + currency: str (PK) + current_buid: BlockUID + nb_members: int + current_mass: int + median_time: int + last_ud: int + last_ud_base: int + previous_mass: int +} + +class Node << (D,orchid) >> { + currency: str (FK) + endpoints: str + uid: str + pubkey: str (PK) + current_buid: BlockUID + previous_buid: BlockUID + state: int + software: str + version: str + merkle_nodes: dict +} + +class Key << (D,orchid) >> { + pubkey: str (PK) + salt: str +} + +class UserParameters << (D,orchid) >> { + profile: str (PK) + lang: str + ref: 0 + expert_mode: bool, + digits_after_comma: int + maximized: bool + notifications: bool + enable_proxy: bool + proxy_type: int + proxy_address: str + proxy_port: 8080: int + international_system_of_units: bool + auto_refresh: bool + forgetfulness: bool +} + +class UserParametersRepo << (R,orange) >> { + Create() + Update() + Save() + Drop() +} + + +class KeyRepo << (R,orange) >> { + Create() + Update() + Save() + Drop() +} + + +class NodesRepo << (R,orange) >> { + Create() + Update() + Save() + Drop() +} + +class BlockchainRepo << (R,orange) >> { + Create() + Update() + Save() + Drop() +} +class CommunityRepo << (R,orange) >> { + Commit() + Update() + Save() + Drop() +} +class TransactionRepo << (R,orange) >> { + Commit() + Update() + Save() + Drop() +} +class CertificationRepo << (R,orange) >> { + Commit() + Update() + Save() + Drop() +} +class IdentitiesRepo << (R,orange) >> { + Commit() + Update() + Save() + Drop() +} + +IdentitiesRepo "1" --* "*" Identity + +CertificationRepo "1" --* "*" Certification + +TransactionRepo "1" --* "*" Transaction + +CommunityRepo "1" --* "*" Community + +BlockchainRepo "1" --* "*" Blockchain + +NodesRepo "1" --* "*" Node + +KeyRepo "1" --* "*" Key + +UserParametersRepo "1" --* "*" UserParameters + + +@enduml \ No newline at end of file diff --git a/doc/uml/gui-classes.png b/doc/uml/gui-classes.png deleted file mode 100644 index e00da1f229639c06e0ee5f201ea8d1d7e487f111..0000000000000000000000000000000000000000 Binary files a/doc/uml/gui-classes.png and /dev/null differ diff --git a/doc/uml/gui-classes.pu b/doc/uml/gui-classes.pu deleted file mode 100644 index 1b00e1a188119584fcbc71d86e0ccf60b92d3ee7..0000000000000000000000000000000000000000 --- a/doc/uml/gui-classes.pu +++ /dev/null @@ -1,50 +0,0 @@ -@startuml - - -package gui { - class MainWindow { - } - MainWindow "1" --* "1" CommunityView - - class CommunityView { - } - CommunityView "1" --* "1" WalletTab - CommunityView "1" -down-* "1" InformationsTab - CommunityView "1" --* "1" TransactionsTab - CommunityView "1" --* "1" IdentitiesTab - CommunityView "1" --* "1" WotTab - CommunityView "1" -down-* "1" NetworkTab - - - class WalletTab { - } - - class InformationsTab { - } - - class TransactionsTab { - } - - class NetworkTab { - } - - class IdentitiesTab { - } - - class WotTab { - } - package dialogs { - class CertificationDialog - class TransferDialog - class ContactDialog - class ConfigureAccountDialog - class ConfigureCommunityDialog - } - - MainWindow --> CertificationDialog - MainWindow --> TransferDialog - MainWindow --> ContactDialog - MainWindow --> ConfigureAccountDialog - ConfigureAccountDialog --> ConfigureCommunityDialog -} -@enduml diff --git a/doc/uml/models-classes.png b/doc/uml/models-classes.png deleted file mode 100644 index 5833c6ecaf2c4c1b57601a5c682bdb941c2a61ea..0000000000000000000000000000000000000000 Binary files a/doc/uml/models-classes.png and /dev/null differ diff --git a/doc/uml/models-classes.pu b/doc/uml/models-classes.pu deleted file mode 100644 index 1650739d5d4c65bfa48ba299c543b4d019cde42b..0000000000000000000000000000000000000000 --- a/doc/uml/models-classes.pu +++ /dev/null @@ -1,39 +0,0 @@ -@startuml - -package models { - class WalletsFilterProxyModel { - } - - WalletsFilterProxyModel --> WalletsTableModel : source - - class WalletsTableModel { - } - - class IdentitiesFilterProxyModel { - } - IdentitiesFilterProxyModel --> IdentitiesTableModel : source - - class IdentitiesTableModel { - } - - class NetworkFilterProxyModel { - } - NetworkFilterProxyModel --> NetworkTableModel : source - - class NetworkTableModel { - } - - class TxHistoryFilterProxyModel { - } - TxHistoryFilterProxyModel --> TxHistoryTableModel : source - class TxHistoryTableModel { - } - - class CommunitiesListModel { - } - - class RootNodesTableModel { - } -} - -@enduml \ No newline at end of file diff --git a/doc/uml/network.png b/doc/uml/network.png index c31386d981219956b30d8dc291e48fd9d69c4745..9de1956d213cea9ce16baf9033d2a17b835dc4f9 100644 Binary files a/doc/uml/network.png and b/doc/uml/network.png differ diff --git a/doc/uml/processors.png b/doc/uml/processors.png new file mode 100644 index 0000000000000000000000000000000000000000..da005cebef0f6e9be98173e638a49fa91061fe2f Binary files /dev/null and b/doc/uml/processors.png differ diff --git a/doc/uml/processors.pu b/doc/uml/processors.pu new file mode 100644 index 0000000000000000000000000000000000000000..92483fb87278b9722a21ae71efb14ed6f951e0f6 --- /dev/null +++ b/doc/uml/processors.pu @@ -0,0 +1,21 @@ +@startuml + + +class IdentitiesProcessor << (P,lightgreen) >> { + find_identities() + find_certifiers_of() + find_certified_by() +} + +class CertificationProcessor << (P,lightgreen) >> { +} +class TransactionProcessor << (P,lightgreen) >> { +} +class CommunityProcessor << (P,lightgreen) >> { +} +class BlockchainProcessor << (P,lightgreen) >> { +} +class NodesProcessor << (P,lightgreen) >> { +} + +@enduml \ No newline at end of file diff --git a/doc/uml/requests.png b/doc/uml/requests.png index 85339a6ae644809ea4836ee4c109da64144f3e71..acfe67960fb3a49efc897fbd6b9d5942b77fe157 100644 Binary files a/doc/uml/requests.png and b/doc/uml/requests.png differ diff --git a/doc/uml/services.png b/doc/uml/services.png new file mode 100644 index 0000000000000000000000000000000000000000..fa2287997a8fb761142da8c2f1b8a16a28515ab9 Binary files /dev/null and b/doc/uml/services.png differ diff --git a/doc/uml/services.pu b/doc/uml/services.pu new file mode 100644 index 0000000000000000000000000000000000000000..7811ff112bc8c7d9571d3466c02c2539a4285ac7 --- /dev/null +++ b/doc/uml/services.pu @@ -0,0 +1,36 @@ +@startuml + + + +class ProfileService << (S,cyan) >> { + add_connection() + remove_connection() +} + +class AccountService << (S,cyan) >> { + send_transaction() + send_certification() + send_membership() + send_identity() + send_revokation() +} + +class TransactionsService << (S,cyan) >> { + handle_new_block() + refresh_transactions() + rollback_transactions() +} + +class RegistryService << (S,cyan) >> { + handle_new_block() +} + +class NetworkService << (S,cyan) >> { + discover_network() +} + +class BlockchainService << (S,cyan) >> { + receive_block() +} + +@enduml \ No newline at end of file diff --git a/doc/uml/tx_lifecycle.png b/doc/uml/tx_lifecycle.png index 7f7102c0cac56b9a5aafa90498611f8caa9df429..83b2ea7451902696950ae5b9761f6ca4a225654f 100644 Binary files a/doc/uml/tx_lifecycle.png and b/doc/uml/tx_lifecycle.png differ diff --git a/gen_resources.py b/gen_resources.py index 2a269dfe5b9d6c49d537caea6dd20b9eb3d92d4b..b73cfa715e5bd3c4889ec59ed3c91df88195eaad 100644 --- a/gen_resources.py +++ b/gen_resources.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- import sys, os, multiprocessing, subprocess + +sakia = os.path.abspath(os.path.join(os.path.dirname(__file__))) resources = os.path.abspath(os.path.join(os.path.dirname(__file__), 'res')) gen_ui = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src', 'sakia', 'gen_resources')) gen_resources = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')) @@ -12,11 +14,15 @@ def convert_ui(args, **kwargs): def build_resources(): try: to_process = [] - for root, dirs, files in os.walk(resources): + for root, dirs, files in os.walk(sakia): for f in files: if f.endswith('.ui'): source = os.path.join(root, f) - dest = os.path.join(gen_ui, os.path.splitext(os.path.basename(source))[0]+'_uic.py') + if os.path.commonpath([resources, root]) == resources: + dest = os.path.join(gen_ui, os.path.splitext(os.path.basename(source))[0]+'_uic.py') + else: + dest = os.path.join(root, os.path.splitext(os.path.basename(source))[0]+'_uic.py') + exe = 'pyuic5' elif f.endswith('.qrc'): source = os.path.join(root, f) diff --git a/requirements.txt b/requirements.txt index 145df061d3a4c6fa1c9cb843d1b39244403a12bf..34e29cb109b7f7257eb14df7dd4d82fbb4dfac49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,8 @@ -duniterpy>=0.20.dev0 git+https://github.com/Insoleet/quamash.git@master asynctest -networkx \ No newline at end of file +networkx +attrs +duniter-mirage +duniterpy>=0.40 +pytest +pytest-asyncio diff --git a/res/ui/account_cfg.ui b/res/ui/account_cfg.ui deleted file mode 100644 index d1010bf905e24889be32e482d1e201103fe6910e..0000000000000000000000000000000000000000 --- a/res/ui/account_cfg.ui +++ /dev/null @@ -1,623 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>AccountConfigurationDialog</class> - <widget class="QDialog" name="AccountConfigurationDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>527</width> - <height>359</height> - </rect> - </property> - <property name="windowTitle"> - <string>Add an account</string> - </property> - <property name="modal"> - <bool>true</bool> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QStackedWidget" name="stacked_pages"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="page_init"> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="title"> - <string>Account parameters</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <spacer name="verticalSpacer_3"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLabel" name="label_action"> - <property name="text"> - <string>Account name (uid)</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_account_name"/> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_8"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_delete"> - <property name="styleSheet"> - <string notr="true">color: rgb(255, 0, 0);</string> - </property> - <property name="text"> - <string>Delete account</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer_4"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="page_gpg"> - <layout class="QVBoxLayout" name="verticalLayout_7"> - <item> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Key parameters</string> - </property> - </widget> - </item> - <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_5"/> - </item> - <item> - <widget class="QLineEdit" name="edit_salt"> - <property name="text"> - <string/> - </property> - <property name="placeholderText"> - <string>Secret key</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_password"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="placeholderText"> - <string>Your password</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_password_repeat"> - <property name="text"> - <string/> - </property> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="placeholderText"> - <string>Please repeat your password</string> - </property> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="title"> - <string>Scrypt Params</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_8"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_9"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>Key strength : </string> - </property> - </widget> - </item> - <item> - <widget class="QComboBox" name="combo_scrypt"> - <item> - <property name="text"> - <string>Light</string> - </property> - </item> - <item> - <property name="text"> - <string>Secure</string> - </property> - </item> - <item> - <property name="text"> - <string>Hardest</string> - </property> - </item> - <item> - <property name="text"> - <string>Extreme</string> - </property> - </item> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>N: </string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spin_N"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="maximum"> - <number>0</number> - </property> - <property name="value"> - <number>0</number> - </property> - <property name="displayIntegerBase"> - <number>10</number> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_2"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>r : </string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spin_r"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="displayIntegerBase"> - <number>10</number> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_4"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>p : </string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spin_p"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_6"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QLabel" name="label_info"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_generate"> - <property name="text"> - <string>Show public key</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - <widget class="QWidget" name="page__communities"> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>Communities</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <widget class="QListView" name="list_communities"> - <property name="contextMenuPolicy"> - <enum>Qt::DefaultContextMenu</enum> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <widget class="QPushButton" name="button_add_community"> - <property name="text"> - <string>Add a community</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_remove_community"> - <property name="text"> - <string>Remove selected community</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_4"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QPushButton" name="button_previous"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Previous</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_next"> - <property name="text"> - <string>Next</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources/> - <connections> - <connection> - <sender>button_add_community</sender> - <signal>clicked()</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>open_process_add_community()</slot> - <hints> - <hint type="sourcelabel"> - <x>109</x> - <y>237</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_remove_community</sender> - <signal>clicked()</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_remove_community()</slot> - <hints> - <hint type="sourcelabel"> - <x>290</x> - <y>237</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>list_communities</sender> - <signal>doubleClicked(QModelIndex)</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>open_process_edit_community(QModelIndex)</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>180</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_next</sender> - <signal>clicked()</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>next()</slot> - <hints> - <hint type="sourcelabel"> - <x>349</x> - <y>278</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_previous</sender> - <signal>clicked()</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>previous()</slot> - <hints> - <hint type="sourcelabel"> - <x>49</x> - <y>278</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_salt</sender> - <signal>textChanged(QString)</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_edit_account_key()</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>69</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_password</sender> - <signal>textChanged(QString)</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_edit_account_key()</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>98</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_password_repeat</sender> - <signal>textChanged(QString)</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_edit_account_key()</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>127</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_account_name</sender> - <signal>textChanged(QString)</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_edit_account_parameters()</slot> - <hints> - <hint type="sourcelabel"> - <x>240</x> - <y>110</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_generate</sender> - <signal>clicked()</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_show_pubkey()</slot> - <hints> - <hint type="sourcelabel"> - <x>290</x> - <y>161</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_delete</sender> - <signal>clicked()</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_delete_account()</slot> - <hints> - <hint type="sourcelabel"> - <x>325</x> - <y>146</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>open_process_add_community()</slot> - <slot>key_changed(int)</slot> - <slot>action_remove_community()</slot> - <slot>open_process_edit_community(QModelIndex)</slot> - <slot>next()</slot> - <slot>previous()</slot> - <slot>open_import_key()</slot> - <slot>open_generate_account_key()</slot> - <slot>action_edit_account_key()</slot> - <slot>action_edit_account_parameters()</slot> - <slot>action_show_pubkey()</slot> - <slot>action_delete_account()</slot> - </slots> -</ui> diff --git a/res/ui/certifications_tab.ui b/res/ui/certifications_tab.ui deleted file mode 100644 index b7e8eacfa6aa09ab59782740abc8f1bd5d9e91e4..0000000000000000000000000000000000000000 --- a/res/ui/certifications_tab.ui +++ /dev/null @@ -1,165 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>certificationsTabWidget</class> - <widget class="QWidget" name="certificationsTabWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>481</width> - <height>456</height> - </rect> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QGroupBox" name="groupbox_balance"> - <property name="title"> - <string>Certifications</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QLabel" name="label_resume"> - <property name="font"> - <font> - <pointsize>22</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> - <property name="text"> - <string>loading...</string> - </property> - <property name="alignment"> - <set>Qt::AlignHCenter|Qt::AlignTop</set> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QDateTimeEdit" name="date_from"> - <property name="displayFormat"> - <string>dd/MM/yyyy</string> - </property> - <property name="calendarPopup"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QDateTimeEdit" name="date_to"> - <property name="displayFormat"> - <string>dd/MM/yyyy</string> - </property> - <property name="calendarPopup"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="QProgressBar" name="progressbar"> - <property name="maximum"> - <number>0</number> - </property> - <property name="value"> - <number>-1</number> - </property> - </widget> - </item> - <item> - <widget class="QTableView" name="table_history"> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="showGrid"> - <bool>true</bool> - </property> - <attribute name="horizontalHeaderShowSortIndicator" stdset="0"> - <bool>true</bool> - </attribute> - <attribute name="horizontalHeaderStretchLastSection"> - <bool>true</bool> - </attribute> - <attribute name="verticalHeaderVisible"> - <bool>false</bool> - </attribute> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections> - <connection> - <sender>table_history</sender> - <signal>customContextMenuRequested(QPoint)</signal> - <receiver>certificationsTabWidget</receiver> - <slot>history_context_menu()</slot> - <hints> - <hint type="sourcelabel"> - <x>273</x> - <y>183</y> - </hint> - <hint type="destinationlabel"> - <x>830</x> - <y>802</y> - </hint> - </hints> - </connection> - <connection> - <sender>date_from</sender> - <signal>dateChanged(QDate)</signal> - <receiver>certificationsTabWidget</receiver> - <slot>dates_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>102</x> - <y>28</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>date_to</sender> - <signal>dateChanged(QDate)</signal> - <receiver>certificationsTabWidget</receiver> - <slot>dates_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>297</x> - <y>28</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>history_context_menu()</slot> - <slot>dates_changed()</slot> - </slots> -</ui> diff --git a/res/ui/community_cfg.ui b/res/ui/community_cfg.ui deleted file mode 100644 index a31cb57384fab2f61fa4dd97c75d6246fb7e6217..0000000000000000000000000000000000000000 --- a/res/ui/community_cfg.ui +++ /dev/null @@ -1,333 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>CommunityConfigurationDialog</class> - <widget class="QDialog" name="CommunityConfigurationDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>317</height> - </rect> - </property> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - <property name="windowTitle"> - <string>Add a community</string> - </property> - <property name="modal"> - <bool>true</bool> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QStackedWidget" name="stacked_pages"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="page_node"> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Please enter the address of a node :</string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="rightMargin"> - <number>5</number> - </property> - <item> - <widget class="QLineEdit" name="lineedit_server"/> - </item> - <item> - <widget class="QLabel" name="label_double_dot"> - <property name="text"> - <string>:</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spinbox_port"> - <property name="maximum"> - <number>65535</number> - </property> - <property name="value"> - <number>8001</number> - </property> - </widget> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QPushButton" name="button_register"> - <property name="text"> - <string>Register your account</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_connect"> - <property name="text"> - <string>Connect using your account</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_guest"> - <property name="text"> - <string>Connect as a guest</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/guest_icon</normaloff>:/icons/guest_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_error"> - <property name="text"> - <string/> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <widget class="QWidget" name="page_add_nodes"> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>Communities nodes</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <widget class="QTreeView" name="tree_peers"> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <widget class="QLineEdit" name="lineedit_add_address"> - <property name="text"> - <string/> - </property> - <property name="placeholderText"> - <string>Server</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spinbox_add_port"> - <property name="minimum"> - <number>0</number> - </property> - <property name="maximum"> - <number>65535</number> - </property> - <property name="singleStep"> - <number>1</number> - </property> - <property name="value"> - <number>8081</number> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_add"> - <property name="text"> - <string>Add</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="layout_previous_next"> - <item> - <widget class="QPushButton" name="button_previous"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Previous</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_next"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="text"> - <string>Next</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections> - <connection> - <sender>button_add</sender> - <signal>clicked()</signal> - <receiver>CommunityConfigurationDialog</receiver> - <slot>add_node()</slot> - <hints> - <hint type="sourcelabel"> - <x>337</x> - <y>236</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>tree_peers</sender> - <signal>customContextMenuRequested(QPoint)</signal> - <receiver>CommunityConfigurationDialog</receiver> - <slot>showContextMenu(QPoint)</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>128</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_next</sender> - <signal>clicked()</signal> - <receiver>CommunityConfigurationDialog</receiver> - <slot>next()</slot> - <hints> - <hint type="sourcelabel"> - <x>349</x> - <y>278</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_previous</sender> - <signal>clicked()</signal> - <receiver>CommunityConfigurationDialog</receiver> - <slot>previous()</slot> - <hints> - <hint type="sourcelabel"> - <x>49</x> - <y>278</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>add_node()</slot> - <slot>showContextMenu(QPoint)</slot> - <slot>check()</slot> - <slot>next()</slot> - <slot>previous()</slot> - <slot>current_wallet_changed(int)</slot> - <slot>remove_node()</slot> - </slots> -</ui> diff --git a/res/ui/community_view.ui b/res/ui/community_view.ui deleted file mode 100644 index 213060dc7294aa314a1b2b10781c79ac7865d500..0000000000000000000000000000000000000000 --- a/res/ui/community_view.ui +++ /dev/null @@ -1,145 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>CommunityWidget</class> - <widget class="QWidget" name="CommunityWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>624</width> - <height>429</height> - </rect> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QFrame" name="frame"> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QPushButton" name="button_home"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/home_icon</normaloff>:/icons/home_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_currency"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_send_money"> - <property name="text"> - <string>Send money</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/payment_icon</normaloff>:/icons/payment_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_certification"> - <property name="text"> - <string>Certification</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/certification_icon</normaloff>:/icons/certification_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_membership"> - <property name="text"> - <string>Renew membership</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="toolbutton_menu"> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/menu_icon</normaloff>:/icons/menu_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="popupMode"> - <enum>QToolButton::InstantPopup</enum> - </property> - <property name="autoRaise"> - <bool>false</bool> - </property> - <property name="arrowType"> - <enum>Qt::NoArrow</enum> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QTabWidget" name="tabs"/> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections/> -</ui> diff --git a/res/ui/contact.ui b/res/ui/contact.ui deleted file mode 100644 index 40c059b6b52a119a714aea29dbbac55e43b890fc..0000000000000000000000000000000000000000 --- a/res/ui/contact.ui +++ /dev/null @@ -1,128 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>ConfigureContactDialog</class> - <widget class="QDialog" name="ConfigureContactDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>228</width> - <height>103</height> - </rect> - </property> - <property name="windowTitle"> - <string>Add a contact</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Name</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_name"/> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Pubkey</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_pubkey"/> - </item> - </layout> - </item> - <item> - <widget class="QDialogButtonBox" name="button_box"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - </layout> - </widget> - <resources/> - <connections> - <connection> - <sender>button_box</sender> - <signal>accepted()</signal> - <receiver>ConfigureContactDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>248</x> - <y>254</y> - </hint> - <hint type="destinationlabel"> - <x>157</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_box</sender> - <signal>rejected()</signal> - <receiver>ConfigureContactDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>316</x> - <y>260</y> - </hint> - <hint type="destinationlabel"> - <x>286</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_pubkey</sender> - <signal>textChanged(QString)</signal> - <receiver>ConfigureContactDialog</receiver> - <slot>pubkey_edited()</slot> - <hints> - <hint type="sourcelabel"> - <x>145</x> - <y>52</y> - </hint> - <hint type="destinationlabel"> - <x>113</x> - <y>66</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_name</sender> - <signal>textChanged(QString)</signal> - <receiver>ConfigureContactDialog</receiver> - <slot>name_edited()</slot> - <hints> - <hint type="sourcelabel"> - <x>129</x> - <y>21</y> - </hint> - <hint type="destinationlabel"> - <x>113</x> - <y>66</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>name_edited()</slot> - <slot>pubkey_edited()</slot> - </slots> -</ui> diff --git a/res/ui/create_wallet.ui b/res/ui/create_wallet.ui deleted file mode 100644 index 4a611d7a5caf318d910868867b3f63d65e2c6efb..0000000000000000000000000000000000000000 --- a/res/ui/create_wallet.ui +++ /dev/null @@ -1,169 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>CreateWalletDialog</class> - <widget class="QDialog" name="CreateWalletDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>234</height> - </rect> - </property> - <property name="windowTitle"> - <string>Create a new wallet</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>1</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QStackedWidget" name="stacked_pages"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="page_wallet"> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="topMargin"> - <number>0</number> - </property> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Wallet name :</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_name"/> - </item> - </layout> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <widget class="QLabel" name="label_error"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item> - <spacer name="verticalSpacer_3"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>2</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QPushButton" name="button_previous"> - <property name="text"> - <string>Previous</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_next"> - <property name="text"> - <string>Next</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources/> - <connections> - <connection> - <sender>edit_name</sender> - <signal>textChanged(QString)</signal> - <receiver>CreateWalletDialog</receiver> - <slot>check()</slot> - <hints> - <hint type="sourcelabel"> - <x>238</x> - <y>91</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>116</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_next</sender> - <signal>clicked()</signal> - <receiver>CreateWalletDialog</receiver> - <slot>next()</slot> - <hints> - <hint type="sourcelabel"> - <x>349</x> - <y>212</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>116</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_previous</sender> - <signal>clicked()</signal> - <receiver>CreateWalletDialog</receiver> - <slot>previous()</slot> - <hints> - <hint type="sourcelabel"> - <x>49</x> - <y>212</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>116</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>open_import_key()</slot> - <slot>open_generate_key()</slot> - <slot>check()</slot> - <slot>next()</slot> - <slot>previous()</slot> - </slots> -</ui> diff --git a/res/ui/currency_tab.ui b/res/ui/currency_tab.ui deleted file mode 100644 index 96fb8c5971e30da25a72bb274cce7d4ad3b27974..0000000000000000000000000000000000000000 --- a/res/ui/currency_tab.ui +++ /dev/null @@ -1,52 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>CurrencyTabWidget</class> - <widget class="QWidget" name="CurrencyTabWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>300</height> - </rect> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <property name="windowIcon"> - <iconset> - <normaloff>:/icons/noun_43022_cc.svg</normaloff>:/icons/noun_43022_cc.svg</iconset> - </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QFrame" name="actionsFrame"> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QTabWidget" name="tabs_account"> - <property name="currentIndex"> - <number>-1</number> - </property> - </widget> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections/> - <slots> - <slot>refresh_wallet_content(QModelIndex)</slot> - <slot>wallet_context_menu(QPoint)</slot> - <slot>dates_changed(QDateTime)</slot> - <slot>history_context_menu(QPoint)</slot> - </slots> -</ui> diff --git a/res/ui/explorer_tab.ui b/res/ui/explorer_tab.ui deleted file mode 100644 index 58aa9891db98f11f4f66fcf45a3be4913937e123..0000000000000000000000000000000000000000 --- a/res/ui/explorer_tab.ui +++ /dev/null @@ -1,95 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>ExplorerTabWidget</class> - <widget class="QWidget" name="ExplorerTabWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>522</width> - <height>442</height> - </rect> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="SearchUserWidget" name="search_user_widget" native="true"/> - </item> - <item> - <widget class="ExplorerView" name="graphicsView"> - <property name="viewportUpdateMode"> - <enum>QGraphicsView::BoundingRectViewportUpdate</enum> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Steps</string> - </property> - </widget> - </item> - <item> - <widget class="QSlider" name="steps_slider"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="tickPosition"> - <enum>QSlider::TicksBothSides</enum> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_go"> - <property name="text"> - <string>Go</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <customwidgets> - <customwidget> - <class>ExplorerView</class> - <extends>QGraphicsView</extends> - <header>sakia.gui.views</header> - </customwidget> - <customwidget> - <class>SearchUserWidget</class> - <extends>QWidget</extends> - <header>sakia.gui.widgets.search_user</header> - <container>1</container> - </customwidget> - </customwidgets> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections/> - <slots> - <slot>reset()</slot> - <slot>search()</slot> - <slot>select_node()</slot> - </slots> -</ui> diff --git a/res/ui/homescreen.ui b/res/ui/homescreen.ui deleted file mode 100644 index f326a247071175967a5e8908579dd6ec4ee79212..0000000000000000000000000000000000000000 --- a/res/ui/homescreen.ui +++ /dev/null @@ -1,197 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>HomescreenWidget</class> - <widget class="QWidget" name="HomescreenWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>648</width> - <height>472</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QFrame" name="frame_connected"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="sizeConstraint"> - <enum>QLayout::SetMaximumSize</enum> - </property> - <item> - <widget class="QLabel" name="label_connected"> - <property name="styleSheet"> - <string notr="true"> font-size:12pt; font-weight:600;</string> - </property> - <property name="text"> - <string>Connected as</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_add_community"> - <property name="text"> - <string>Add a community</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/add_community</normaloff>:/icons/add_community</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_disconnect"> - <property name="text"> - <string>Disconnect</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/logout</normaloff>:/icons/logout</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QFrame" name="frame_disconnected"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> - </property> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QLabel" name="label_disconnected"> - <property name="text"> - <string><html><head/><body><p><span style=" font-size:12pt; font-weight:600;">Not Connected</span></p></body></html></string> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="toolbutton_connect"> - <property name="text"> - <string>Connect</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="popupMode"> - <enum>QToolButton::MenuButtonPopup</enum> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="toolbutton_new_account"> - <property name="text"> - <string>New account</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/add_account_icon</normaloff>:/icons/add_account_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="popupMode"> - <enum>QToolButton::MenuButtonPopup</enum> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - <property name="autoRaise"> - <bool>false</bool> - </property> - <property name="arrowType"> - <enum>Qt::NoArrow</enum> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections/> -</ui> diff --git a/res/ui/import_account.ui b/res/ui/import_account.ui deleted file mode 100644 index 6ca68c0155e14ae4587d38bc51a7a9af3e761eb1..0000000000000000000000000000000000000000 --- a/res/ui/import_account.ui +++ /dev/null @@ -1,135 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>ImportAccountDialog</class> - <widget class="QDialog" name="ImportAccountDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>124</height> - </rect> - </property> - <property name="windowTitle"> - <string>Import an account</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLineEdit" name="edit_file"/> - </item> - <item> - <widget class="QPushButton" name="button_import"> - <property name="text"> - <string>Import a file</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Name of the account :</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_name"/> - </item> - </layout> - </item> - <item> - <widget class="QLabel" name="label_errors"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item> - <widget class="QDialogButtonBox" name="button_box"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - </layout> - </widget> - <resources/> - <connections> - <connection> - <sender>button_box</sender> - <signal>accepted()</signal> - <receiver>ImportAccountDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>248</x> - <y>254</y> - </hint> - <hint type="destinationlabel"> - <x>157</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_box</sender> - <signal>rejected()</signal> - <receiver>ImportAccountDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>316</x> - <y>260</y> - </hint> - <hint type="destinationlabel"> - <x>286</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_import</sender> - <signal>clicked()</signal> - <receiver>ImportAccountDialog</receiver> - <slot>import_account()</slot> - <hints> - <hint type="sourcelabel"> - <x>349</x> - <y>21</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>51</y> - </hint> - </hints> - </connection> - <connection> - <sender>edit_name</sender> - <signal>textEdited(QString)</signal> - <receiver>ImportAccountDialog</receiver> - <slot>name_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>259</x> - <y>52</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>51</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>import_account()</slot> - <slot>name_changed()</slot> - </slots> -</ui> diff --git a/res/ui/node_manager.ui b/res/ui/node_manager.ui deleted file mode 100644 index ef4041a7ec9580838728d1d3066eb14662b72d1f..0000000000000000000000000000000000000000 --- a/res/ui/node_manager.ui +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>NodeManager</class> - <widget class="QDialog" name="NodeManager"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>1024</width> - <height>600</height> - </rect> - </property> - <property name="windowTitle"> - <string>Node manager</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QWidget" name="web_view" native="true"/> - </item> - </layout> - </widget> - <resources/> - <connections/> -</ui> diff --git a/res/ui/transfer.ui b/res/ui/transfer.ui deleted file mode 100644 index 71e077897ca067494ae853a3f50c3981e09a86ac..0000000000000000000000000000000000000000 --- a/res/ui/transfer.ui +++ /dev/null @@ -1,336 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>TransferMoneyDialog</class> - <widget class="QDialog" name="TransferMoneyDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>496</width> - <height>485</height> - </rect> - </property> - <property name="windowTitle"> - <string>Transfer money</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>Community</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout_4"> - <item> - <widget class="QComboBox" name="combo_community"/> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="title"> - <string>Transfer money to</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QRadioButton" name="radio_contact"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Con&tact</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Maximum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QComboBox" name="combo_contact"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QRadioButton" name="radio_pubkey"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>&Recipient public key</string> - </property> - <property name="checked"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Maximum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLineEdit" name="edit_pubkey"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="inputMask"> - <string/> - </property> - <property name="text"> - <string/> - </property> - <property name="placeholderText"> - <string>Key</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_5"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QRadioButton" name="radio_search"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Search &user</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Maximum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="SearchUserWidget" name="search_user" native="true"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QFrame" name="frame"> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> - </property> - <layout class="QVBoxLayout" name="verticalLayout_8"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_7"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Wallet</string> - </property> - </widget> - </item> - <item> - <widget class="QComboBox" name="combo_wallets"> - <property name="minimumSize"> - <size> - <width>0</width> - <height>30</height> - </size> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="QLabel" name="label_total"> - <property name="text"> - <string>Available money : </string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Amount</string> - </property> - </widget> - </item> - <item> - <widget class="QDoubleSpinBox" name="spinbox_relative"> - <property name="suffix"> - <string> UD</string> - </property> - <property name="decimals"> - <number>6</number> - </property> - <property name="maximum"> - <double>99999999999999991611392.000000000000000</double> - </property> - <property name="singleStep"> - <double>0.010000000000000</double> - </property> - </widget> - </item> - <item> - <widget class="QDoubleSpinBox" name="spinbox_amount"> - <property name="wrapping"> - <bool>false</bool> - </property> - <property name="frame"> - <bool>true</bool> - </property> - <property name="readOnly"> - <bool>false</bool> - </property> - <property name="buttonSymbols"> - <enum>QAbstractSpinBox::NoButtons</enum> - </property> - <property name="showGroupSeparator" stdset="0"> - <bool>false</bool> - </property> - <property name="decimals"> - <number>0</number> - </property> - <property name="value"> - <double>0.000000000000000</double> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="title"> - <string>Transaction message</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <widget class="QLineEdit" name="edit_message"> - <property name="inputMask"> - <string/> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QDialogButtonBox" name="button_box"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - </layout> - </widget> - <customwidgets> - <customwidget> - <class>SearchUserWidget</class> - <extends>QWidget</extends> - <header>sakia.gui.widgets.search_user</header> - <container>1</container> - </customwidget> - </customwidgets> - <resources/> - <connections/> - <slots> - <slot>open_manage_wallet_coins()</slot> - <slot>change_displayed_wallet(int)</slot> - <slot>transfer_mode_changed(bool)</slot> - <slot>recipient_mode_changed(bool)</slot> - <slot>change_current_community(int)</slot> - <slot>amount_changed()</slot> - <slot>relative_amount_changed()</slot> - </slots> -</ui> diff --git a/res/ui/wallets_tab.ui b/res/ui/wallets_tab.ui deleted file mode 100644 index 16b4dd1c96d212b9773b06a202151676c417af27..0000000000000000000000000000000000000000 --- a/res/ui/wallets_tab.ui +++ /dev/null @@ -1,129 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>WalletsTab</class> - <widget class="QWidget" name="WalletsTab"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>924</width> - <height>920</height> - </rect> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <property name="styleSheet"> - <string notr="true">QGroupBox { - border: 1px solid gray; - border-radius: 9px; - margin-top: 0.5em; -} - -QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 3px 0 3px; - font-weight: bold; -}</string> - </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>Balance</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <widget class="QLabel" name="label_balance"> - <property name="font"> - <font> - <pointsize>22</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> - <property name="text"> - <string>label_balance</string> - </property> - <property name="alignment"> - <set>Qt::AlignHCenter|Qt::AlignTop</set> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_balance_range"> - <property name="text"> - <string>label_balance_range</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item row="1" column="0"> - <widget class="QTableView" name="table_wallets"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>1</verstretch> - </sizepolicy> - </property> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="selectionMode"> - <enum>QAbstractItemView::SingleSelection</enum> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="sortingEnabled"> - <bool>true</bool> - </property> - <attribute name="horizontalHeaderStretchLastSection"> - <bool>true</bool> - </attribute> - <attribute name="verticalHeaderVisible"> - <bool>false</bool> - </attribute> - </widget> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections> - <connection> - <sender>table_wallets</sender> - <signal>customContextMenuRequested(QPoint)</signal> - <receiver>WalletsTab</receiver> - <slot>wallet_context_menu(QPoint)</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>346</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>225</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>wallet_context_menu(QPoint)</slot> - <slot>wallet_changed()</slot> - <slot>send_membership_demand()</slot> - <slot>send_membership_leaving()</slot> - <slot>revoke_uid()</slot> - <slot>publish_uid()</slot> - </slots> -</ui> diff --git a/src/sakia/__init__.py b/src/sakia/__init__.py index 8233ea0a9c3f1c66888ca87348b0e6f469bdfb62..3d6823a6723c6d5bd2718749465240c45419f0f1 100644 --- a/src/sakia/__init__.py +++ b/src/sakia/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = ('0', '20', '13') +__version_info__ = ('0', '30', '0') __version__ = '.'.join(__version_info__) diff --git a/src/sakia/app.py b/src/sakia/app.py new file mode 100644 index 0000000000000000000000000000000000000000..a88c25a09e8fc8f1758aa5b3e5d4aaada1bd96cd --- /dev/null +++ b/src/sakia/app.py @@ -0,0 +1,231 @@ +import attr +import datetime +import logging + +import aiohttp +from PyQt5.QtCore import QObject, pyqtSignal, QTranslator, QCoreApplication, QLocale +from . import __version__ +from .options import SakiaOptions +from sakia.data.connectors import BmaConnector +from sakia.services import NetworkService, BlockchainService, IdentitiesService, \ + SourcesServices, TransactionsService, DocumentsService +from sakia.data.repositories import SakiaDatabase +from sakia.data.entities import Transaction, Connection, Identity, Dividend +from sakia.data.processors import BlockchainProcessor, NodesProcessor, IdentitiesProcessor, \ + CertificationsProcessor, SourcesProcessor, TransactionsProcessor, ConnectionsProcessor, DividendsProcessor +from sakia.data.files import AppDataFile, UserParametersFile +from sakia.decorators import asyncify +from sakia.money import * + + +@attr.s() +class Application(QObject): + + """ + Managing core application datas : + Accounts list and general configuration + Saving and loading the application state + + + :param QCoreApplication qapp: Qt Application + :param quamash.QEventLoop loop: quamash.QEventLoop instance + :param sakia.options.SakiaOptions options: the options + :param sakia.data.entities.AppData app_data: the application data + :param sakia.data.entities.UserParameters parameters: the application current user parameters + :param sakia.data.repositories.SakiaDatabase db: The database + :param dict network_services: All network services for current currency + :param dict blockchain_services: All blockchain services for current currency + :param dict identities_services: All identities services for current currency + :param dict sources_services: All sources services for current currency + :param dict transactions_services: All transactions services for current currency + :param sakia.services.DocumentsService documents_service: A service to broadcast documents + """ + + new_dividend = pyqtSignal(Dividend) + new_transfer = pyqtSignal(Transaction) + transaction_state_changed = pyqtSignal(Transaction) + identity_changed = pyqtSignal(Identity) + new_connection = pyqtSignal(Connection) + referential_changed = pyqtSignal() + sources_refreshed = pyqtSignal() + + qapp = attr.ib() + loop = attr.ib() + options = attr.ib() + app_data = attr.ib() + parameters = attr.ib() + db = attr.ib() + network_services = attr.ib(default=attr.Factory(dict)) + blockchain_services = attr.ib(default=attr.Factory(dict)) + identities_services = attr.ib(default=attr.Factory(dict)) + sources_services = attr.ib(default=attr.Factory(dict)) + transactions_services = attr.ib(default=attr.Factory(dict)) + documents_service = attr.ib(default=None) + current_ref = attr.ib(default=Relative) + _logger = attr.ib(default=attr.Factory(lambda:logging.getLogger('sakia'))) + available_version = attr.ib(init=False) + _translator = attr.ib(init=False) + + def __attrs_post_init__(self): + super().__init__() + self._translator = QTranslator(self.qapp) + self.available_version = True, __version__, "" + + @classmethod + def startup(cls, argv, qapp, loop): + options = SakiaOptions.from_arguments(argv) + app_data = AppDataFile.in_config_path(options.config_path).load_or_init() + app = cls(qapp, loop, options, app_data, None, None) + #app.set_proxy() + app.get_last_version() + app.load_profile(app_data.default) + app.start_coroutines() + app.documents_service = DocumentsService.instanciate(app) + app.switch_language() + return app + + def load_profile(self, profile_name): + """ + Initialize databases depending on profile loaded + :param profile_name: + :return: + """ + self.parameters = UserParametersFile.in_config_path(self.options.config_path, profile_name).load_or_init() + self.db = SakiaDatabase.load_or_init(self.options, profile_name) + + self.instanciate_services() + + def instanciate_services(self): + nodes_processor = NodesProcessor(self.db.nodes_repo) + bma_connector = BmaConnector(nodes_processor, self.parameters) + connections_processor = ConnectionsProcessor(self.db.connections_repo) + identities_processor = IdentitiesProcessor(self.db.identities_repo, self.db.blockchains_repo, bma_connector) + certs_processor = CertificationsProcessor(self.db.certifications_repo, self.db.identities_repo, bma_connector) + blockchain_processor = BlockchainProcessor.instanciate(self) + sources_processor = SourcesProcessor.instanciate(self) + transactions_processor = TransactionsProcessor.instanciate(self) + dividends_processor = DividendsProcessor.instanciate(self) + + self.blockchain_services = {} + self.network_services = {} + self.identities_services = {} + self.sources_services = {} + self.transactions_services = {} + self.documents_service = DocumentsService.instanciate(self) + + for currency in self.db.connections_repo.get_currencies(): + if currency not in self.identities_services: + self.identities_services[currency] = IdentitiesService(currency, connections_processor, + identities_processor, + certs_processor, blockchain_processor, + bma_connector) + + if currency not in self.transactions_services: + self.transactions_services[currency] = TransactionsService(currency, transactions_processor, + dividends_processor, + identities_processor, connections_processor, + bma_connector) + + if currency not in self.sources_services: + self.sources_services[currency] = SourcesServices(currency, sources_processor, + connections_processor, bma_connector) + + if currency not in self.blockchain_services: + self.blockchain_services[currency] = BlockchainService(self, currency, blockchain_processor, bma_connector, + self.identities_services[currency], + self.transactions_services[currency], + self.sources_services[currency]) + if currency not in self.network_services: + self.network_services[currency] = NetworkService.load(self, currency, nodes_processor, + self.blockchain_services[currency], + self.identities_services[currency]) + + async def remove_connection(self, connection): + await self.stop_current_profile() + connections_processor = ConnectionsProcessor.instanciate(self) + connections_processor.remove_connections(connection) + + if not connections_processor.connections_to(connection.currency): + BlockchainProcessor.instanciate(self).remove_blockchain(connection.currency) + + IdentitiesProcessor.instanciate(self).cleanup_connection(connection) + + CertificationsProcessor.instanciate(self).cleanup_connection(connection, connections_processor.pubkeys()) + + SourcesProcessor.instanciate(self).drop_all_of(currency=connection.currency, pubkey=connection.pubkey) + + DividendsProcessor.instanciate(self).cleanup_connection(connection) + + TransactionsProcessor.instanciate(self).cleanup_connection(connection, connections_processor.pubkeys()) + + if not connections_processor.connections(): + NodesProcessor.instanciate(self).drop_all() + + self.db.commit() + self.start_coroutines() + + def switch_language(self): + logging.debug("Loading translations") + locale = self.parameters.lang + QLocale.setDefault(QLocale(locale)) + QCoreApplication.removeTranslator(self._translator) + self._translator = QTranslator(self.qapp) + if locale == "en_GB": + QCoreApplication.installTranslator(self._translator) + elif self._translator.load(":/i18n/{0}".format(locale)): + if QCoreApplication.installTranslator(self._translator): + logging.debug("Loaded i18n/{0}".format(locale)) + else: + logging.debug("Couldn't load translation") + + def start_coroutines(self): + for currency in self.db.connections_repo.get_currencies(): + self.network_services[currency].start_coroutines() + + async def stop_current_profile(self, closing=False): + """ + Save the account to the cache + and stop the coroutines + """ + for currency in self.db.connections_repo.get_currencies(): + await self.network_services[currency].stop_coroutines(closing) + + @asyncify + async def get_last_version(self): + try: + with aiohttp.ClientSession() as session: + with aiohttp.Timeout(15): + response = await session.get("https://api.github.com/repos/duniter/sakia/releases", + proxy=self.parameters.proxy()) + if response.status == 200: + releases = await response.json() + latest = None + for r in releases: + if not latest: + latest = r + else: + latest_date = datetime.datetime.strptime(latest['published_at'], "%Y-%m-%dT%H:%M:%SZ") + date = datetime.datetime.strptime(r['published_at'], "%Y-%m-%dT%H:%M:%SZ") + if latest_date < date: + latest = r + latest_version = latest["tag_name"] + version = (__version__ == latest_version, + latest_version, + latest["html_url"]) + logging.debug("Found version : {0}".format(latest_version)) + logging.debug("Current version : {0}".format(__version__)) + self.available_version = version + except (aiohttp.errors.ClientError, aiohttp.errors.TimeoutError) as e: + self._logger.debug("Could not connect to github : {0}".format(str(e))) + + def save_parameters(self, parameters): + self.parameters = UserParametersFile\ + .in_config_path(self.options.config_path, parameters.profile_name)\ + .save(parameters) + + def change_referential(self, index): + self.current_ref = Referentials[index] + self.referential_changed.emit() + + def connection_exists(self): + return len(ConnectionsProcessor.instanciate(self).connections()) > 0 diff --git a/src/sakia/constants.py b/src/sakia/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..7ec1e4f04aa9ec3b15ab952d01a8e5e49504b2ca --- /dev/null +++ b/src/sakia/constants.py @@ -0,0 +1 @@ +MAX_CONFIRMATIONS = 6 diff --git a/src/sakia/core/__init__.py b/src/sakia/core/__init__.py deleted file mode 100644 index 6de4a62c9259773dd9a301d9f7e5b616c7328489..0000000000000000000000000000000000000000 --- a/src/sakia/core/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .community import Community -from .wallet import Wallet -from .account import Account -from .app import Application \ No newline at end of file diff --git a/src/sakia/core/account.py b/src/sakia/core/account.py deleted file mode 100644 index 643c92b7fcc69e1086e0c8c5f3b9e44b12ec37ad..0000000000000000000000000000000000000000 --- a/src/sakia/core/account.py +++ /dev/null @@ -1,647 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" - -from duniterpy.documents import Membership, SelfCertification, Certification, Revocation, BlockUID, Block -from duniterpy.key import SigningKey, ScryptParams -from duniterpy.api import bma -from duniterpy.api.bma import PROTOCOL_VERSION -from duniterpy.api import errors - -import logging -import asyncio -from pkg_resources import parse_version -from aiohttp.errors import ClientError -from PyQt5.QtCore import QObject, pyqtSignal - -from . import money -from .wallet import Wallet -from .community import Community -from .registry import LocalState -from ..tools.exceptions import ContactAlreadyExists, LookupFailureError -from .. import __version__ - - -class Account(QObject): - """ - An account is specific to a key. - Each account has only one key, and a key can - be locally referenced by only one account. - """ - loading_progressed = pyqtSignal(Community, int, int) - loading_finished = pyqtSignal(Community, list) - wallets_changed = pyqtSignal() - certification_accepted = pyqtSignal() - contacts_changed = pyqtSignal() - - def __init__(self, salt, pubkey, scrypt_params, name, communities, wallets, contacts, identities_registry): - """ - Create an account - - :param str salt: The root key salt - :param str pubkey: Known account pubkey. Used to check that password \ - is OK by comparing (salt, given_passwd) = (pubkey, privkey) \ - with known pubkey - :param duniterpy.key.ScryptParams scrypt_params: the scrypt params of the key - :param str name: The account name, same as network identity uid - :param list of sakia.core.Community communities: Community objects referenced by this account - :param list of sakia.core.Wallet wallets: Wallet objects owned by this account - :param list of dict contacts: Contacts of this account - :param sakia.core.registry.IdentitiesRegistry: The identities registry intance - - .. warnings:: The class methods create and load should be used to create an account - """ - super().__init__() - self.salt = salt - self.pubkey = pubkey - self.scrypt_params = scrypt_params - self.name = name - self.communities = communities - self.wallets = wallets - self.contacts = contacts - self._identities_registry = identities_registry - self._current_ref = 0 - - self.notifications = {'membership_expire_soon': - [ - self.tr("Warning : Your membership is expiring soon."), - 0 - ], - 'warning_certifications': - [ - self.tr("Warning : Your could miss certifications soon."), - 0 - ], - 'warning_revokation': - [ - self.tr("Warning : If you don't renew soon, your identity will be considered revoked."), - 0 - ], - 'warning_certifying_first_time': True, - } - - @classmethod - def create(cls, name, identities_registry): - """ - Factory method to create an empty account object - This new account doesn't have any key and it should be given - one later - It doesn't have any community nor does it have wallets. - Communities could be added later, wallets will be managed - by its wallet pool size. - - :param str name: The account name, same as network identity uid - :return: A new empty account object - """ - account = cls(None, None, None, name, [], [], [], identities_registry) - return account - - @classmethod - def load(cls, json_data, identities_registry): - """ - Factory method to create an Account object from its json view. - :rtype : sakia.core.account.Account - :param dict json_data: The account view as a json dict - :param PyQt5.QtNetwork import QNetworkManager: network_manager - :param sakia.core.registry.self._identities_registry: identities_registry - :return: A new account object created from the json datas - """ - salt = json_data['salt'] - pubkey = json_data['pubkey'] - if 'file_version' in json_data: - file_version = parse_version(json_data['file_version']) - else: - file_version = parse_version('0.11.5') - - if file_version <= parse_version('0.20.11'): - scrypt_params = ScryptParams(4096, 16, 1) - else: - scrypt_params = ScryptParams(json_data['scrypt_params']['N'], - json_data['scrypt_params']['r'], - json_data['scrypt_params']['p']) - - name = json_data['name'] - contacts = [] - - for contact_data in json_data['contacts']: - contacts.append(contact_data) - - wallets = [] - for data in json_data['wallets']: - wallets.append(Wallet.load(data, identities_registry)) - - communities = [] - for data in json_data['communities']: - community = Community.load(data, file_version) - communities.append(community) - - account = cls(salt, pubkey, scrypt_params, name, communities, wallets, - contacts, identities_registry) - return account - - def __eq__(self, other): - """ - :return: True if account.pubkey == other.pubkey - """ - if other is not None: - return other.pubkey == self.pubkey - else: - return False - - def check_password(self, password): - """ - Method to verify the key password validity - - :param str password: The key password - :return: True if the generated pubkey is the same as the account - .. warnings:: Generates a new temporary SigningKey - """ - key = SigningKey(self.salt, password, self.scrypt_params) - return (key.pubkey == self.pubkey) - - def add_contact(self, new_contact): - same_contact = [contact for contact in self.contacts - if new_contact['pubkey'] == contact['pubkey']] - - if len(same_contact) > 0: - raise ContactAlreadyExists(new_contact['name'], same_contact[0]['name']) - self.contacts.append(new_contact) - self.contacts_changed.emit() - - def edit_contact(self, index, new_data): - self.contacts[index] = new_data - self.contacts_changed.emit() - - def remove_contact(self, contact): - self.contacts.remove(contact) - self.contacts_changed.emit() - - def add_community(self, community): - """ - Add a community to the account - - :param community: A community object to add - """ - self.communities.append(community) - return community - - def refresh_transactions(self, app, community): - """ - Refresh the local account cache - This needs n_wallets * n_communities cache refreshing to end - - .. note:: emit the Account pyqtSignal loading_progressed during refresh - """ - logging.debug("Start refresh transactions") - loaded_wallets = 0 - received_list = [] - values = {} - maximums = {} - - def progressing(value, maximum, hash): - #logging.debug("Loading = {0} : {1} : {2}".format(value, maximum, loaded_wallets)) - values[hash] = value - maximums[hash] = maximum - account_value = sum(values.values()) - account_max = sum(maximums.values()) - self.loading_progressed.emit(community, account_value, account_max) - - def wallet_finished(received): - logging.debug("Finished loading wallet") - nonlocal loaded_wallets - loaded_wallets += 1 - if loaded_wallets == len(self.wallets): - logging.debug("All wallets loaded") - self._refreshing = False - self.loading_finished.emit(community, received_list) - for w in self.wallets: - w.refresh_progressed.disconnect(progressing) - w.refresh_finished.disconnect(wallet_finished) - - for w in self.wallets: - w.refresh_progressed.connect(progressing) - w.refresh_finished.connect(wallet_finished) - w.init_cache(app, community) - w.refresh_transactions(community, received_list) - - def rollback_transaction(self, app, community): - """ - Refresh the local account cache - This needs n_wallets * n_communities cache refreshing to end - - .. note:: emit the Account pyqtSignal loading_progressed during refresh - """ - logging.debug("Start refresh transactions") - loaded_wallets = 0 - received_list = [] - values = {} - maximums = {} - - def progressing(value, maximum, hash): - #logging.debug("Loading = {0} : {1} : {2}".format(value, maximum, loaded_wallets)) - values[hash] = value - maximums[hash] = maximum - account_value = sum(values.values()) - account_max = sum(maximums.values()) - self.loading_progressed.emit(community, account_value, account_max) - - def wallet_finished(received): - logging.debug("Finished loading wallet") - nonlocal loaded_wallets - loaded_wallets += 1 - if loaded_wallets == len(self.wallets): - logging.debug("All wallets loaded") - self._refreshing = False - self.loading_finished.emit(community, received_list) - for w in self.wallets: - w.refresh_progressed.disconnect(progressing) - w.refresh_finished.disconnect(wallet_finished) - - for w in self.wallets: - w.refresh_progressed.connect(progressing) - w.refresh_finished.connect(wallet_finished) - w.init_cache(app, community) - w.rollback_transactions(community, received_list) - - def set_display_referential(self, index): - self._current_ref = index - - def set_scrypt_infos(self, salt, password, scrypt_params): - """ - Change the size of the wallet pool - :param int size: The new size of the wallet pool - :param str password: The password of the account, same for all wallets - :param duniterpy.key.ScryptParams scrypt_params: The scrypt parameters - """ - self.salt = salt - self.scrypt_params = scrypt_params - self.pubkey = SigningKey(self.salt, password, scrypt_params).pubkey - wallet = Wallet.create(0, self.salt, password, scrypt_params, - "Wallet", self._identities_registry) - self.wallets.append(wallet) - - async def identity(self, community): - """ - Get the account identity in the specified community - :param sakia.core.community.Community community: The community where to look after the identity - :return: The account identity in the community - :rtype: sakia.core.registry.Identity - """ - identity = await self._identities_registry.future_find(self.pubkey, community) - if identity.local_state == LocalState.NOT_FOUND: - identity.uid = self.name - return identity - - @property - def current_ref(self): - return money.Referentials[self._current_ref] - - def transfers(self, community): - """ - Get all transfers done in a community by all the wallets - owned by this account - - :param community: The target community of this request - :return: All account wallets transfers - """ - sent = [] - for w in self.wallets: - sent.extend(w.transfers(community)) - return sent - - def dividends(self, community): - """ - Get all dividends received in this community - by the first wallet of this account - - :param community: The target community - :return: All account dividends - """ - return self.wallets[0].dividends(community) - - async def future_amount(self, community): - """ - Get amount of money owned in a community by all the wallets - owned by this account - - :param community: The target community of this request - :return: The value of all wallets values accumulated - """ - value = 0 - for w in self.wallets: - val = await w.future_value(community) - value += val - return value - - async def amount(self, community): - """ - Get amount of money owned in a community by all the wallets - owned by this account - - :param community: The target community of this request - :return: The value of all wallets values accumulated - """ - value = 0 - for w in self.wallets: - val = await w.value(community) - value += val - return value - - async def check_registered(self, community): - """ - Checks for the pubkey and the uid of an account in a community - :param sakia.core.Community community: The community we check for registration - :return: (True if found, local value, network value) - """ - def _parse_uid_certifiers(data): - return self.name == data['uid'], self.name, data['uid'] - - def _parse_uid_lookup(data): - timestamp = BlockUID.empty() - found_uid = "" - for result in data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] - found_uid = uid_data["uid"] - return self.name == found_uid, self.name, found_uid - - def _parse_pubkey_certifiers(data): - return self.pubkey == data['pubkey'], self.pubkey, data['pubkey'] - - def _parse_pubkey_lookup(data): - timestamp = BlockUID.empty() - found_uid = "" - found_result = ["", ""] - for result in data['results']: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] - found_uid = uid_data["uid"] - if found_uid == self.name: - found_result = result['pubkey'], found_uid - if found_result[1] == self.name: - return self.pubkey == found_result[0], self.pubkey, found_result[0] - else: - return False, self.pubkey, None - - async def execute_requests(parsers, search): - tries = 0 - request = bma.wot.CertifiersOf - nonlocal registered - #TODO: The algorithm is quite dirty - #Multiplying the tries without any reason... - while tries < 3 and not registered[0] and not registered[2]: - try: - data = await community.bma_access.simple_request(request, - req_args={'search': search}) - if data: - registered = parsers[request](data) - tries += 1 - except errors.DuniterError as e: - if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, - e.ucode == errors.NO_MATCHING_IDENTITY): - if request == bma.wot.CertifiersOf: - request = bma.wot.Lookup - tries = 0 - else: - tries += 1 - else: - tries += 1 - except asyncio.TimeoutError: - tries += 1 - except ClientError: - tries += 1 - - registered = (False, self.name, None) - # We execute search based on pubkey - # And look for account UID - uid_parsers = { - bma.wot.CertifiersOf: _parse_uid_certifiers, - bma.wot.Lookup: _parse_uid_lookup - } - await execute_requests(uid_parsers, self.pubkey) - - # If the uid wasn't found when looking for the pubkey - # We look for the uid and check for the pubkey - if not registered[0] and not registered[2]: - pubkey_parsers = { - bma.wot.CertifiersOf: _parse_pubkey_certifiers, - bma.wot.Lookup: _parse_pubkey_lookup - } - await execute_requests(pubkey_parsers, self.name) - - return registered - - async def send_selfcert(self, password, community): - """ - Send our self certification to a target community - - :param str password: The account SigningKey password - :param community: The community target of the self certification - """ - try: - block_data = await community.bma_access.simple_request(bma.blockchain.Current) - signed_raw = "{0}{1}\n".format(block_data['raw'], block_data['signature']) - block_uid = Block.from_signed_raw(signed_raw).blockUID - except errors.DuniterError as e: - if e.ucode == errors.NO_CURRENT_BLOCK: - block_uid = BlockUID.empty() - else: - raise - selfcert = SelfCertification(PROTOCOL_VERSION, - community.currency, - self.pubkey, - self.name, - block_uid, - None) - key = SigningKey(self.salt, password, self.scrypt_params) - selfcert.sign([key]) - logging.debug("Key publish : {0}".format(selfcert.signed_raw())) - - responses = await community.bma_access.broadcast(bma.wot.Add, {}, {'identity': selfcert.signed_raw()}) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - if result[0]: - (await self.identity(community)).sigdate = block_uid - return result - - async def send_membership(self, password, community, mstype): - """ - Send a membership document to a target community. - Signal "document_broadcasted" is emitted at the end. - - :param str password: The account SigningKey password - :param community: The community target of the membership document - :param str mstype: The type of membership demand. "IN" to join, "OUT" to leave - """ - logging.debug("Send membership") - - blockUID = community.network.current_blockUID - self_identity = await self._identities_registry.future_find(self.pubkey, community) - selfcert = await self_identity.selfcert(community) - - membership = Membership(PROTOCOL_VERSION, community.currency, - selfcert.pubkey, blockUID, mstype, selfcert.uid, - selfcert.timestamp, None) - key = SigningKey(self.salt, password, self.scrypt_params) - membership.sign([key]) - logging.debug("Membership : {0}".format(membership.signed_raw())) - responses = await community.bma_access.broadcast(bma.blockchain.Membership, {}, - {'membership': membership.signed_raw()}) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - return result - - async def certify(self, password, community, pubkey): - """ - Certify an other identity - - :param str password: The account SigningKey password - :param sakia.core.community.Community community: The community target of the certification - :param str pubkey: The certified identity pubkey - """ - logging.debug("Certdata") - blockUID = community.network.current_blockUID - try: - identity = await self._identities_registry.future_find(pubkey, community) - selfcert = await identity.selfcert(community) - except LookupFailureError as e: - return False, str(e) - - if selfcert: - certification = Certification(PROTOCOL_VERSION, community.currency, - self.pubkey, pubkey, blockUID, None) - - key = SigningKey(self.salt, password, self.scrypt_params) - certification.sign(selfcert, [key]) - signed_cert = certification.signed_raw(selfcert) - logging.debug("Certification : {0}".format(signed_cert)) - - data = {'cert': certification.signed_raw(selfcert)} - logging.debug("Posted data : {0}".format(data)) - responses = await community.bma_access.broadcast(bma.wot.Certify, {}, data) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - # signal certification to all listeners - self.certification_accepted.emit() - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - return result - else: - return False, self.tr("Could not find user self certification.") - - async def revoke(self, password, community): - """ - Revoke self-identity on server, not in blockchain - - :param str password: The account SigningKey password - :param sakia.core.community.Community community: The community target of the revokation - """ - revoked = await self._identities_registry.future_find(self.pubkey, community) - - revokation = Revocation(PROTOCOL_VERSION, community.currency, None) - selfcert = await revoked.selfcert(community) - - key = SigningKey(self.salt, password, self.scrypt_params) - revokation.sign(selfcert, [key]) - - logging.debug("Self-Revokation Document : \n{0}".format(revokation.raw(selfcert))) - logging.debug("Signature : \n{0}".format(revokation.signatures[0])) - - data = { - 'pubkey': revoked.pubkey, - 'self_': selfcert.signed_raw(), - 'sig': revokation.signatures[0] - } - logging.debug("Posted data : {0}".format(data)) - responses = await community.bma_access.broadcast(bma.wot.Revoke, {}, data) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - return result - - async def generate_revokation(self, community, password): - """ - Generate account revokation document for given community - :param sakia.core.Community community: the community - :param str password: the password - :return: the revokation document - :rtype: duniterpy.documents.certification.Revocation - """ - document = Revocation(PROTOCOL_VERSION, community.currency, self.pubkey, "") - identity = await self.identity(community) - selfcert = await identity.selfcert(community) - - key = SigningKey(self.salt, password, self.scrypt_params) - - document.sign(selfcert, [key]) - return document.signed_raw(selfcert) - - def start_coroutines(self): - for c in self.communities: - c.start_coroutines() - - async def stop_coroutines(self, closing=False): - logging.debug("Stop communities coroutines") - for c in self.communities: - await c.stop_coroutines(closing) - - logging.debug("Stop wallets coroutines") - for w in self.wallets: - w.stop_coroutines(closing) - logging.debug("Account coroutines stopped") - - def jsonify(self): - """ - Get the account in a json format. - - :return: A dict view of the account to be saved as json - """ - data_communities = [] - for c in self.communities: - data_communities.append(c.jsonify()) - - data_wallets = [] - for w in self.wallets: - data_wallets.append(w.jsonify()) - - data = {'name': self.name, - 'salt': self.salt, - 'pubkey': self.pubkey, - 'scrypt_params': { - 'N': self.scrypt_params.N, - 'r': self.scrypt_params.r, - 'p': self.scrypt_params.p, - }, - 'communities': data_communities, - 'wallets': data_wallets, - 'contacts': self.contacts, - 'file_version': __version__} - return data diff --git a/src/sakia/core/app.py b/src/sakia/core/app.py deleted file mode 100644 index 70666599b366fca4df2f24712f82a51843629067..0000000000000000000000000000000000000000 --- a/src/sakia/core/app.py +++ /dev/null @@ -1,552 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" - -import os -import logging -import tarfile -import shutil -import json -import datetime -import aiohttp -import asyncio -from pkg_resources import parse_version - -from PyQt5.QtCore import QObject, pyqtSignal, QTranslator, QCoreApplication, QLocale -from duniterpy.api.bma import API -from aiohttp.connector import ProxyConnector -from . import config -from .account import Account -from .registry import IdentitiesRegistry, Identity -from .. import __version__ -from ..tools.exceptions import NameAlreadyExists, BadAccountFile -from ..tools.decorators import asyncify -import i18n_rc - - -class Application(QObject): - - """ - Managing core application datas : - Accounts list and general configuration - Saving and loading the application state - """ - - version_requested = pyqtSignal() - view_identity_in_wot = pyqtSignal(Identity) - refresh_transfers = pyqtSignal() - account_imported = pyqtSignal(str) - account_changed = pyqtSignal() - - def __init__(self, qapp, loop, identities_registry): - """ - Init a new "sakia" application - :param QCoreApplication qapp: Qt Application - :param quamash.QEventLoop loop: quamash.QEventLoop instance - :param sakia.core.registry.IdentitiesRegistry identities_registry: IdentitiesRegistry instance - :return: - """ - - super().__init__() - self.qapp = qapp - self.accounts = {} - self._current_account = None - self.loop = loop - self.available_version = (True, - __version__, - "") - self._translator = QTranslator(self.qapp) - self._identities_registry = identities_registry - self.preferences = {'account': "", - 'lang': 'en_GB', - 'ref': 0, - 'expert_mode': False, - 'digits_after_comma': 6, - 'maximized': False, - 'notifications': True, - 'enable_proxy': False, - 'proxy_type': "HTTP", - 'proxy_address': "", - 'proxy_port': 8080, - 'international_system_of_units': True, - 'auto_refresh': False, - 'forgetfulness':False - } - - @classmethod - def startup(cls, argv, qapp, loop): - config.parse_arguments(argv) - identities_registry = IdentitiesRegistry() - app = cls(qapp, loop, identities_registry) - app.load() - app.switch_language() - app.set_proxy() - app.get_last_version() - if app.preferences["account"] != "": - account = app.get_account(app.preferences["account"]) - app.change_current_account(account) - # no default account... - else: - # if at least one account exists, set it as default... - if len(app.accounts) > 0: - # capture names sorted alphabetically - names = list(app.accounts.keys()) - names.sort() - # set first name in list as default in preferences - app.preferences['account'] = names[0] - app.save_preferences(app.preferences) - # open it - logging.debug("No default account in preferences. Set %s as default account." % names[0]) - - return app - - def set_proxy(self): - if self.preferences['enable_proxy'] is True: - API.aiohttp_connector = ProxyConnector("http://{0}:{1}".format( - self.preferences['proxy_address'], - self.preferences['proxy_port'])) - else: - API.aiohttp_connector = None - - def switch_language(self): - logging.debug("Loading translations") - locale = self.preferences['lang'] - QLocale.setDefault(QLocale(locale)) - QCoreApplication.removeTranslator(self._translator) - self._translator = QTranslator(self.qapp) - if locale == "en_GB": - QCoreApplication.installTranslator(self._translator) - elif self._translator.load(":/i18n/{0}".format(locale)): - if QCoreApplication.installTranslator(self._translator): - logging.debug("Loaded i18n/{0}".format(locale)) - else: - logging.debug("Couldn't load translation") - - @property - def current_account(self): - return self._current_account - - def get_account(self, name): - """ - Load an account then return it - - :param str name: The account name - :return: The loaded account if it's a success, else return None - """ - if name in self.accounts.keys(): - self.load_account(name) - return self.accounts[name] - else: - return None - - def create_account(self, name): - """ - Create a new account from its name - - :param str name: The account name - :return: The new account - :raise: NameAlreadyExists if the account name is already used locally - """ - for a in self.accounts: - if a == name: - raise NameAlreadyExists(a) - - account = Account.create(name, self._identities_registry) - - return account - - @property - def identities_registry(self): - return self._identities_registry - - def add_account(self, account): - self.accounts[account.name] = account - - @asyncify - async def delete_account(self, account): - """ - Delete an account. - Current account changes to None if it is deleted. - """ - await account.stop_coroutines() - self.accounts.pop(account.name) - if self._current_account == account: - self._current_account = None - with open(config.parameters['data'], 'w') as outfile: - json.dump(self.jsonify(), outfile, indent=4, sort_keys=True) - if self.preferences['account'] == account.name: - self.preferences['account'] = "" - self.save_preferences(self.preferences) - - @asyncify - async def change_current_account(self, account): - """ - Change current account displayed and refresh its cache. - - :param sakia.core.Account account: The account object to display - .. note:: Emits the application pyqtSignal loading_progressed - during cache refresh - """ - if self._current_account is not None: - await self.stop_current_account() - - self._current_account = account - if self._current_account is not None: - self._current_account.start_coroutines() - self.account_changed.emit() - - async def stop_current_account(self, closing=False): - """ - Save the account to the cache - and stop the coroutines - """ - self.save_cache(self._current_account) - self.save_notifications(self._current_account) - await self._current_account.stop_coroutines(closing) - - def load(self): - """ - Load a saved application state from the data file. - Loads only jsonified objects but not their cache. - - If the standard application state file can't be found, - no error is raised. - """ - self.load_registries() - self.load_preferences() - try: - logging.debug("Loading data...") - with open(config.parameters['data'], 'r') as json_data: - data = json.load(json_data) - for account_name in data['local_accounts']: - self.accounts[account_name] = None - except FileNotFoundError: - pass - - def load_registries(self): - """ - Load the Person instances of the person module. - Each instance is unique, and can be find by its public key. - """ - try: - identities_path = os.path.join(config.parameters['home'], - '__identities__') - with open(identities_path, 'r') as identities_data: - data = json.load(identities_data) - self._identities_registry.load_json(data) - except FileNotFoundError: - pass - - def load_account(self, account_name): - """ - Load an account from its name - - :param str account_name: The account name - """ - account_path = os.path.join(config.parameters['home'], - account_name, 'properties') - with open(account_path, 'r') as json_data: - data = json.load(json_data) - account = Account.load(data, self._identities_registry) - self.load_cache(account) - self.accounts[account_name] = account - - for community in account.communities: - community.network.blockchain_rollback.connect(community.rollback_cache) - community.network.new_block_mined.connect(lambda b, co=community: - account.refresh_transactions(self, co)) - community.network.blockchain_rollback.connect(lambda b, co=community: - account.rollback_transaction(self, co)) - community.network.root_nodes_changed.connect(lambda acc=account: self.save(acc)) - - account_notifications_path = os.path.join(config.parameters['home'], - account_name, '__notifications__') - - try: - with open(account_notifications_path, 'r') as json_data: - data = json.load(json_data) - for notification in data: - if notification in account.notifications: - account.notifications[notification] = data[notification] - except FileNotFoundError: - logging.debug("Could not find notifications file") - pass - - def load_cache(self, account): - """ - Load an account cache - - :param account: The account object to load the cache - """ - for community in account.communities: - bma_path = os.path.join(config.parameters['home'], - account.name, '__cache__', - community.currency + '_bma') - - network_path = os.path.join(config.parameters['home'], - account.name, '__cache__', - community.currency + '_network') - - if os.path.exists(network_path): - with open(network_path, 'r') as json_data: - data = json.load(json_data) - community.network.merge_with_json(data['network'], parse_version(data['version'])) - - if os.path.exists(bma_path): - with open(bma_path, 'r') as json_data: - data = json.load(json_data) - community.bma_access.load_from_json(data['cache']) - - for wallet in account.wallets: - for c in account.communities: - wallet.init_cache(self, c) - wallet_path = os.path.join(config.parameters['home'], - account.name, '__cache__', wallet.pubkey + "_wal") - if os.path.exists(wallet_path): - with open(wallet_path, 'r') as json_data: - data = json.load(json_data) - wallet.load_caches(self, data) - - def load_preferences(self): - """ - Load the preferences. - """ - try: - preferences_path = os.path.join(config.parameters['home'], - 'preferences') - with open(preferences_path, 'r') as json_data: - data = json.load(json_data) - for key in data: - self.preferences[key] = data[key] - except FileNotFoundError: - pass - - def save_preferences(self, preferences): - """ - Save the preferences. - - :param preferences: A dict containing the keys/values of the preferences - """ - assert('lang' in preferences) - assert('account' in preferences) - assert('ref' in preferences) - - self.preferences = preferences - preferences_path = os.path.join(config.parameters['home'], - 'preferences') - with open(preferences_path, 'w') as outfile: - json.dump(preferences, outfile, indent=4) - - self.set_proxy() - - def save(self, account): - """ - Save an account - - :param account: The account object to save - """ - with open(config.parameters['data'], 'w') as outfile: - json.dump(self.jsonify(), outfile, indent=4, sort_keys=True) - account_path = os.path.join(config.parameters['home'], - account.name) - if account.name in self.accounts: - properties_path = os.path.join(account_path, 'properties') - if not os.path.exists(account_path): - logging.info("Creating account directory") - os.makedirs(account_path) - with open(properties_path, 'w') as outfile: - json.dump(account.jsonify(), outfile, indent=4, sort_keys=True) - else: - account_path = os.path.join(config.parameters['home'], account.name) - shutil.rmtree(account_path) - - def save_notifications(self, account): - """ - Save an account notifications - - :param account: The account object to save - """ - account_path = os.path.join(config.parameters['home'], - account.name) - notifications_path = os.path.join(account_path, '__notifications__') - with open(notifications_path, 'w') as outfile: - json.dump(account.notifications, outfile, indent=4, sort_keys=True) - - def save_registries(self): - """ - Save the registries - """ - identities_path = os.path.join(config.parameters['home'], - '__identities__') - buffer_path = identities_path + ".buf" - with open(buffer_path, 'w') as outfile: - data = self.identities_registry.jsonify() - data['version'] = __version__ - for chunk in json.JSONEncoder().iterencode(data): - outfile.write(chunk) - shutil.move(buffer_path, identities_path) - - def save_wallet(self, account, wallet): - """ - Save wallet of account in cache - - :param sakia.core.account.Account account: Account instance - :param sakia.core.wallet.Wallet wallet: Wallet instance - """ - if not os.path.exists(os.path.join(config.parameters['home'], - account.name, '__cache__')): - os.makedirs(os.path.join(config.parameters['home'], - account.name, '__cache__')) - wallet_path = os.path.join(config.parameters['home'], - account.name, '__cache__', wallet.pubkey + "_wal") - buffer_path = wallet_path + ".buf" - with open(buffer_path, 'w') as outfile: - data = wallet.jsonify_caches() - data['version'] = __version__ - for chunk in json.JSONEncoder().iterencode(data): - outfile.write(chunk) - shutil.move(buffer_path, wallet_path) - - def save_cache(self, account): - """ - Save the cache of an account - - :param account: The account object to save the cache - """ - if not os.path.exists(os.path.join(config.parameters['home'], - account.name, '__cache__')): - os.makedirs(os.path.join(config.parameters['home'], - account.name, '__cache__')) - for wallet in account.wallets: - self.save_wallet(account, wallet) - - for community in account.communities: - bma_path = os.path.join(config.parameters['home'], - account.name, '__cache__', - community.currency + '_bma') - - network_path = os.path.join(config.parameters['home'], - account.name, '__cache__', - community.currency + '_network') - buffer_path = network_path + ".buf" - - with open(buffer_path, 'w') as outfile: - data = dict() - data['network'] = community.network.jsonify() - data['version'] = __version__ - for chunk in json.JSONEncoder().iterencode(data): - outfile.write(chunk) - shutil.move(buffer_path, network_path) - - buffer_path = bma_path + ".buf" - - with open(buffer_path, 'w') as outfile: - data['cache'] = community.bma_access.jsonify() - data['version'] = __version__ - for chunk in json.JSONEncoder().iterencode(data): - outfile.write(chunk) - shutil.move(buffer_path, bma_path) - - def import_account(self, file, name): - """ - Import an account from a tar file and open it - - :param str file: The file path of the tar file - :param str name: The account name - """ - with tarfile.open(file, "r") as tar: - path = os.path.join(config.parameters['home'], - name) - for obj in ["properties"]: - try: - tar.getmember(obj) - except KeyError: - raise BadAccountFile(file) - tar.extractall(path) - - account_path = os.path.join(config.parameters['home'], - name, 'properties') - json_data = open(account_path, 'r') - data = json.load(json_data) - account = Account.load(data, self._identities_registry) - account.name = name - self.add_account(account) - self.save(account) - self.account_imported.emit(account.name) - - def export_account(self, file, account): - """ - Export an account to a tar file - - :param str file: The filepath of the tar file - :param account: The account object to export - """ - with tarfile.open(file, "w") as tar: - for file in ["properties"]: - path = os.path.join(config.parameters['home'], - account.name, file) - tar.add(path, file) - - def jsonify_accounts(self): - """ - Jsonify an account - - :return: The account as a dict to format as json - """ - data = [] - logging.debug("{0}".format(self.accounts)) - for account in self.accounts: - data.append(account) - return data - - def jsonify(self): - """ - Jsonify the app datas - - :return: The accounts of the app to format as json - """ - data = {'local_accounts': self.jsonify_accounts()} - return data - - async def stop(self): - if self._current_account: - await self.stop_current_account(closing=True) - await asyncio.sleep(0) - self.save_registries() - - @asyncify - async def get_last_version(self): - if self.preferences['enable_proxy'] is True: - connector = ProxyConnector("http://{0}:{1}".format( - self.preferences['proxy_address'], - self.preferences['proxy_port'])) - else: - connector = None - try: - with aiohttp.Timeout(15): - response = await aiohttp.get("https://api.github.com/repos/duniter/sakia/releases", connector=connector) - if response.status == 200: - releases = await response.json() - latest = None - for r in releases: - if not latest: - latest = r - else: - latest_date = datetime.datetime.strptime(latest['published_at'], "%Y-%m-%dT%H:%M:%SZ") - date = datetime.datetime.strptime(r['published_at'], "%Y-%m-%dT%H:%M:%SZ") - if latest_date < date: - latest = r - latest_version = latest["tag_name"] - version = (__version__ == latest_version, - latest_version, - latest["html_url"]) - logging.debug("Found version : {0}".format(latest_version)) - logging.debug("Current version : {0}".format(__version__)) - self.available_version = version - self.version_requested.emit() - except (aiohttp.errors.ClientError, aiohttp.errors.TimeoutError) as e: - logging.debug("Could not connect to github : {0}".format(str(e))) - except Exception as e: - pass diff --git a/src/sakia/core/community.py b/src/sakia/core/community.py deleted file mode 100644 index 285b7ef7f017a70f7a83a01bc559181ae99f1665..0000000000000000000000000000000000000000 --- a/src/sakia/core/community.py +++ /dev/null @@ -1,342 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" - -import logging -import re -import math - -from PyQt5.QtCore import QObject - -from ..tools.exceptions import NoPeerAvailable -from .net.network import Network -from duniterpy.api import bma, errors -from .net.api.bma.access import BmaAccess - - -class Community(QObject): - """ - A community is a group of nodes using the same currency. - - .. warning:: The currency name is supposed to be unique in sakia - but nothing exists in duniter to assert that a currency name is unique. - """ - def __init__(self, currency, network, bma_access): - """ - Initialize community attributes with a currency and a network. - - :param str currency: The currency name of the community. - :param sakia.core.net.network.Network network: The network of the community - :param sakia.core.net.api.bma.access.BmaAccess bma_access: The BMA Access object - - .. warning:: The community object should be created using its factory - class methods. - """ - super().__init__() - self.currency = currency - self._network = network - self._bma_access = bma_access - - @classmethod - def create(cls, node): - """ - Create a community from its first node. - - :param node: The first Node of the community - """ - network = Network.create(node) - bma_access = BmaAccess.create(network) - community = cls(node.currency, network, bma_access) - logging.debug("Creating community") - return community - - @classmethod - def load(cls, json_data, file_version): - """ - Load a community from json - - :param dict json_data: The community as a dict in json format - :param NormalizedVersion file_version: the file sakia version - """ - currency = json_data['currency'] - network = Network.from_json(currency, json_data['peers'], file_version) - bma_access = BmaAccess.create(network) - community = cls(currency, network, bma_access) - return community - - @property - def name(self): - """ - The community name is its currency name. - - :return: The community name - """ - return self.currency - - @property - def short_currency(self): - """ - Format the currency name to a short one - - :return: The currency name in a shot format. - """ - words = re.split('[_\W]+', self.currency) - shortened = "" - if len(words) > 1: - shortened = ''.join([w[0] for w in words]) - else: - vowels = ('a', 'e', 'i', 'o', 'u', 'y') - shortened = self.currency - shortened = ''.join([c for c in shortened if c not in vowels]) - return shortened.upper() - - @property - def currency_symbol(self): - """ - Format the currency name to a symbol one. - - :return: The currency name as a utf-8 circled symbol. - """ - letter = self.currency[0] - u = ord('\u24B6') + ord(letter) - ord('A') - return chr(u) - - async def dividend(self, block_number=None): - """ - Get the last generated community universal dividend before block_number. - If block_number is None, returns the last block_number. - - :param int block_number: The block at which we get the latest dividend - - :return: The last UD or 1 if no UD was generated. - """ - block = await self.get_ud_block(block_number=block_number) - if block: - return block['dividend'] * math.pow(10, block['unitbase']) - else: - return 1 - - async def computed_dividend(self): - """ - Get the computed community universal dividend. - - Calculation based on t = last UD block time and on values from the that block : - - UD(computed) = CEIL(MAX(UD(t) ; c * M(t) / N(t))) - - :return: The computed UD or 1 if no UD was generated. - """ - block = await self.get_ud_block() - if block: - parameters = await self.parameters() - return math.ceil( - max( - (await self.dividend()), - float(0) if block['membersCount'] == 0 else - parameters['c'] * block['monetaryMass'] / block['membersCount'] - ) - ) - - else: - return 1 - - async def get_ud_block(self, x=0, block_number=None): - """ - Get a block with universal dividend - If x and block_number are passed to the result, - it returns the 'x' older block with UD in it BEFORE block_number - - :param int x: Get the 'x' older block with UD in it - :param int block_number: Get the latest dividend before this block number - :return: The last block with universal dividend. - :rtype: dict - """ - try: - udblocks = await self.bma_access.future_request(bma.blockchain.UD) - blocks = udblocks['result']['blocks'] - if block_number: - blocks = [b for b in blocks if b <= block_number] - if len(blocks) > 0: - index = len(blocks) - (1+x) - if index < 0: - index = 0 - block_number = blocks[index] - block = await self.bma_access.future_request(bma.blockchain.Block, - req_args={'number': block_number}) - return block - else: - return None - except errors.DuniterError as e: - logging.debug(str(e)) - return None - except NoPeerAvailable as e: - logging.debug(str(e)) - return None - - async def monetary_mass(self): - """ - Get the community monetary mass - - :return: The monetary mass value - """ - # Get cached block by block number - block_number = self.network.current_blockUID.number - if block_number: - block = await self.bma_access.future_request(bma.blockchain.Block, - req_args={'number': block_number}) - return block['monetaryMass'] - else: - return 0 - - async def nb_members(self): - """ - Get the community members number - - :return: The community members number - """ - try: - # Get cached block by block number - block_number = self.network.current_blockUID.number - block = await self.bma_access.future_request(bma.blockchain.Block, - req_args={'number': block_number}) - return block['membersCount'] - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - return 0 - except NoPeerAvailable as e: - logging.debug(str(e)) - return 0 - - async def time(self, block_number=None): - """ - Get the blockchain time - :param block_number: The block number, None if current block - :return: The community blockchain time - :rtype: int - """ - try: - # Get cached block by block number - if block_number is None: - block_number = self.network.current_blockUID.number - block = await self.bma_access.future_request(bma.blockchain.Block, - req_args={'number': block_number}) - return block['medianTime'] - except errors.DuniterError as e: - logging.debug(str(e)) - return 0 - except NoPeerAvailable as e: - logging.debug(str(e)) - return 0 - - @property - def network(self): - """ - Get the community network instance. - - :return: The community network instance. - :rtype: sakia.core.net.Network - """ - return self._network - - @property - def bma_access(self): - """ - Get the community bma_access instance - - :return: The community bma_access instace - :rtype: sakia.core.net.api.bma.access.BmaAccess - """ - return self._bma_access - - async def parameters(self): - """ - Return community parameters in bma format - """ - return await self.bma_access.future_request(bma.blockchain.Parameters) - - async def certification_expired(self, cert_time): - """ - Return True if the certificaton time is too old - - :param int cert_time: the timestamp of the certification - """ - parameters = await self.parameters() - blockchain_time = await self.time() - return blockchain_time - cert_time > parameters['sigValidity'] - - async def certification_writable(self, cert_time): - """ - Return True if the certificaton time is too old - - :param int cert_time: the timestamp of the certification - """ - parameters = await self.parameters() - blockchain_time = await self.time() - return blockchain_time - cert_time < parameters['sigWindow'] * parameters['avgGenTime'] - - def add_node(self, node): - """ - Add a peer to the community. - - :param peer: The new peer as a duniterpy Peer object. - """ - self._network.add_root_node(node) - - def remove_node(self, index): - """ - Remove a node from the community. - - :param index: The index of the removed node. - """ - self._network.remove_root_node(index) - - async def get_block(self, number=None): - """ - Get a block - - :param int number: The block number. If none, returns current block. - """ - if number is None: - block_number = self.network.current_blockUID.number - data = await self.bma_access.future_request(bma.blockchain.Block, - req_args={'number': block_number}) - else: - logging.debug("Requesting block {0}".format(number)) - data = await self.bma_access.future_request(bma.blockchain.Block, - req_args={'number': number}) - return data - - async def members_pubkeys(self): - """ - Listing members pubkeys of a community - - :return: All members pubkeys. - """ - memberships = await self.bma_access.future_request(bma.wot.Members) - return [m['pubkey'] for m in memberships["results"]] - - def start_coroutines(self): - self.network.start_coroutines() - - async def stop_coroutines(self, closing=False): - await self.network.stop_coroutines(closing) - - def rollback_cache(self): - self._bma_access.rollback() - - def jsonify(self): - """ - Jsonify the community datas. - - :return: The community as a dict in json format. - """ - - nodes_data = [] - for node in self._network.root_nodes: - nodes_data.append(node.jsonify_root_node()) - - data = {'currency': self.currency, - 'peers': nodes_data} - return data diff --git a/src/sakia/core/config.py b/src/sakia/core/config.py deleted file mode 100644 index 50a9ecc011676a8064061c7280f08bbb2d18c538..0000000000000000000000000000000000000000 --- a/src/sakia/core/config.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Created on 7 févr. 2014 - -@author: inso -""" - -import logging -from logging import FileHandler -from optparse import OptionParser -from os import environ, path, makedirs - - -if "XDG_CONFIG_HOME" in environ: - config_path = environ["XDG_CONFIG_HOME"] -elif "HOME" in environ: - config_path = path.join(environ["HOME"], ".config") -elif "APPDATA" in environ: - config_path = environ["APPDATA"] -else: - config_path = path.dirname(__file__) - -parameters = {'home': path.join(config_path, 'sakia'), - 'data': path.join(config_path, 'sakia', 'data')} - - -if not path.exists(parameters['home']): - logging.info("Creating home directory") - makedirs((parameters['home'])) - - -def parse_arguments(argv): - parser = OptionParser() - - parser.add_option("-v", "--verbose", - action="store_true", dest="verbose", default=False, - help="Print INFO messages to stdout") - - parser.add_option("-d", "--debug", - action="store_true", dest="debug", default=False, - help="Print DEBUG messages to stdout") - - (options, args) = parser.parse_args(argv) - - if options.debug: - logging.basicConfig(format='%(levelname)s:%(module)s:%(funcName)s:%(message)s', - level=logging.DEBUG) - elif options.verbose: - logging.basicConfig(format='%(levelname)s:%(message)s', - level=logging.INFO) - else: - logging.getLogger().propagate = False - logging.getLogger('quamash').setLevel(logging.INFO) - logfile = FileHandler(path.join(parameters['home'], 'sakia.log')) - logging.getLogger().addHandler(logfile) diff --git a/src/sakia/core/graph/__init__.py b/src/sakia/core/graph/__init__.py deleted file mode 100644 index 21318ddce99b0385dca8da9f9e90ca2314b4df3e..0000000000000000000000000000000000000000 --- a/src/sakia/core/graph/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .base_graph import BaseGraph -from .wot_graph import WoTGraph -from .explorer_graph import ExplorerGraph \ No newline at end of file diff --git a/src/sakia/core/graph/base_graph.py b/src/sakia/core/graph/base_graph.py deleted file mode 100644 index d320660b3348fab71d0d767456ab0d2fcf4650ca..0000000000000000000000000000000000000000 --- a/src/sakia/core/graph/base_graph.py +++ /dev/null @@ -1,189 +0,0 @@ -import logging -import time -import networkx -from PyQt5.QtCore import QLocale, QDateTime, QObject -from ...tools.exceptions import NoPeerAvailable -from ..net.network import MAX_CONFIRMATIONS -from .constants import EdgeStatus, NodeStatus - - -class BaseGraph(QObject): - def __init__(self, app, community, nx_graph=None): - """ - Init Graph instance - :param sakia.core.app.Application app: Application instance - :param sakia.core.community.Community community: Community instance - :param networkx.Graph nx_graph: The networkx graph - :return: - """ - super().__init__() - self.app = app - self.community = community - # graph empty if None parameter - self.nx_graph = nx_graph if nx_graph else networkx.DiGraph() - - async def arc_status(self, cert_time): - """ - Get arc status of a certification - :param int cert_time: the timestamp of the certification - :return: the certification time - """ - parameters = await self.community.parameters() - signature_validity = parameters['sigValidity'] - # arc considered strong during 75% of signature validity time - arc_strong = int(signature_validity * 0.75) - # display validity status - ts = time.time() - if (time.time() - cert_time) > arc_strong: - return EdgeStatus.WEAK - else: - return EdgeStatus.STRONG - - async def node_status(self, node_identity, account_identity): - """ - Return the status of the node depending - :param sakia.core.registry.Identity node_identity: The identity of the node - :param sakia.core.registry.Identity account_identity: The identity of the account displayed - :return: HIGHLIGHTED if node_identity is account_identity and OUT if the node_identity is not a member - :rtype: sakia.core.graph.constants.NodeStatus - """ - # new node - node_status = NodeStatus.NEUTRAL - is_member = await node_identity.is_member(self.community) - if node_identity.pubkey == account_identity.pubkey: - node_status += NodeStatus.HIGHLIGHTED - if is_member is False: - node_status += NodeStatus.OUT - return node_status - - def confirmation_text(self, block_number): - """ - Build confirmation text of an arc - :param int block_number: the block number of the certification - :return: the confirmation text - :rtype: str - """ - try: - current_confirmations = self.community.network.confirmations(block_number) - - if MAX_CONFIRMATIONS > current_confirmations: - if self.app.preferences['expert_mode']: - return "{0}/{1}".format(current_confirmations, - MAX_CONFIRMATIONS) - else: - confirmation = current_confirmations / MAX_CONFIRMATIONS * 100 - return "{0} %".format(QLocale().toString(float(confirmation), 'f', 0)) - except ValueError: - pass - return None - - def is_sentry(self, nb_certs, nb_members): - """ - Check if it is a sentry or not - :param int nb_certs: the number of certs - :param int nb_members: the number of members - :return: True if a sentry - """ - Y = { - 10: 2, - 100: 4, - 1000: 6, - 10000: 12, - 100000: 20 - } - for k in reversed(sorted(Y.keys())): - if nb_members >= k: - return nb_certs >= Y[k] - return False - - async def add_certifier_list(self, certifier_list, identity, account_identity): - """ - Add list of certifiers to graph - :param list certifier_list: List of certifiers from api - :param sakia.core.registry.Identity identity: identity instance which is certified - :param sakia.core.registry.Identity account_identity: Account identity instance - :return: - """ - if self.community: - try: - # add certifiers of uid - for certifier in tuple(certifier_list): - node_status = await self.node_status(certifier['identity'], account_identity) - metadata = { - 'text': certifier['identity'].uid, - 'tooltip': certifier['identity'].pubkey, - 'status': node_status - } - self.nx_graph.add_node(certifier['identity'].pubkey, attr_dict=metadata) - - arc_status = await self.arc_status(certifier['cert_time']) - sig_validity = (await self.community.parameters())['sigValidity'] - arc = { - 'status': arc_status, - 'tooltip': QLocale.toString( - QLocale(), - QDateTime.fromTime_t(certifier['cert_time'] + sig_validity).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ), - 'cert_time': certifier['cert_time'], - 'confirmation_text': self.confirmation_text(certifier['block_number']) - } - - self.nx_graph.add_edge(certifier['identity'].pubkey, identity.pubkey, attr_dict=arc, weight=len(certifier_list)) - except NoPeerAvailable as e: - logging.debug(str(e)) - - async def add_certified_list(self, certified_list, identity, account_identity): - """ - Add list of certified from api to graph - :param list certified_list: List of certified from api - :param identity identity: identity instance which is certifier - :param identity account_identity: Account identity instance - :return: - """ - - if self.community: - try: - # add certified by uid - for certified in tuple(certified_list): - node_status = await self.node_status(certified['identity'], account_identity) - metadata = { - 'text': certified['identity'].uid, - 'tooltip': certified['identity'].pubkey, - 'status': node_status - } - self.nx_graph.add_node(certified['identity'].pubkey, attr_dict=metadata) - - arc_status = await self.arc_status(certified['cert_time']) - sig_validity = (await self.community.parameters())['sigValidity'] - arc = { - 'status': arc_status, - 'tooltip': QLocale.toString( - QLocale(), - QDateTime.fromTime_t(certified['cert_time'] + sig_validity).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ), - 'cert_time': certified['cert_time'], - 'confirmation_text': self.confirmation_text(certified['block_number']) - } - - self.nx_graph.add_edge(identity.pubkey, certified['identity'].pubkey, attr_dict=arc, - weight=len(certified_list)) - - except NoPeerAvailable as e: - logging.debug(str(e)) - - def add_identity(self, identity, status): - """ - Add identity as a new node in graph - :param identity identity: identity instance - :param int status: Optional, default=None, Node status (see sakia.gui.views.wot) - :param list edges: Optional, default=None, List of edges (certified by identity) - :param list connected: Optional, default=None, Public key list of the connected nodes around the identity - """ - metadata = { - 'text': identity.uid, - 'tooltip': identity.pubkey, - 'status': status - } - self.nx_graph.add_node(identity.pubkey, attr_dict=metadata) diff --git a/src/sakia/core/graph/explorer_graph.py b/src/sakia/core/graph/explorer_graph.py deleted file mode 100644 index 856ae22f593ac14d2f8ec988dbbcba8e12f19351..0000000000000000000000000000000000000000 --- a/src/sakia/core/graph/explorer_graph.py +++ /dev/null @@ -1,106 +0,0 @@ -import logging -import networkx -import asyncio -from PyQt5.QtCore import pyqtSignal -from .base_graph import BaseGraph -from ..graph.constants import EdgeStatus, NodeStatus - - -class ExplorerGraph(BaseGraph): - - graph_changed = pyqtSignal() - current_identity_changed = pyqtSignal(str) - - def __init__(self, app, community, nx_graph=None): - """ - Init ExplorerGraph instance - :param sakia.core.app.Application app: Application instance - :param sakia.core.community.Community community: Community instance - :param networkx.Graph nx_graph: The networkx graph - :return: - """ - super().__init__(app, community, nx_graph) - self.exploration_task = None - self.explored_identity = None - self.steps = 0 - - def start_exploration(self, identity, steps): - """ - Start exploration of the wot from given identity - :param sakia.core.registry.Identity identity: The identity source of exploration - :param int steps: The number of steps from identity to explore - """ - if self.exploration_task: - if self.explored_identity is not identity or steps != self.steps: - self.exploration_task.cancel() - else: - return - self.nx_graph.clear() - self.explored_identity = identity - self.steps = steps - self.exploration_task = asyncio.ensure_future(self._explore(identity, steps)) - - def stop_exploration(self): - """ - Stop current exploration task, if present. - """ - if self.exploration_task: - self.exploration_task.cancel() - self.exploration_task = None - - async def _explore(self, identity, steps): - """ - Scan graph recursively - :param sakia.core.registry.Identity identity: identity instance from where we start - :param int steps: The number of steps from given identity to explore - :return: False when the identity is added in the graph - """ - # functions keywords args are persistent... Need to reset it with None trick - logging.debug("search %s in " % identity.uid) - - explored = [] - explorable = {0: [identity]} - current_identity = identity - self.nx_graph.clear() - self.add_identity(current_identity, NodeStatus.HIGHLIGHTED) - self.nx_graph.node[current_identity.pubkey]['is_sentry'] = False - self.graph_changed.emit() - for step in range(1, steps + 1): - explorable[step] = [] - - for step in range(0, steps): - while len(explorable[step]) > 0: - current_identity = explorable[step].pop() - # for each pubkey connected... - if current_identity not in explored: - self.current_identity_changed.emit(current_identity.pubkey) - node = self.add_identity(current_identity, NodeStatus.NEUTRAL) - self.nx_graph.node[current_identity.pubkey]['is_sentry'] = False - logging.debug("New identity explored : {pubkey}".format(pubkey=current_identity.pubkey[:5])) - self.graph_changed.emit() - - certifier_list = await current_identity.unique_valid_certifiers_of(self.app.identities_registry, - self.community) - await self.add_certifier_list(certifier_list, current_identity, identity) - logging.debug("New identity certifiers : {pubkey}".format(pubkey=current_identity.pubkey[:5])) - - is_sentry = self.is_sentry(len(certifier_list), await self.community.nb_members()) - self.nx_graph.node[current_identity.pubkey]['is_sentry'] = is_sentry - - self.graph_changed.emit() - - - certified_list = await current_identity.unique_valid_certified_by(self.app.identities_registry, - self.community) - await self.add_certified_list(certified_list, current_identity, identity) - logging.debug("New identity certified : {pubkey}".format(pubkey=current_identity.pubkey[:5])) - self.graph_changed.emit() - - for cert in certified_list + certifier_list: - if cert['identity'] not in explorable[step + 1]: - explorable[step + 1].append(cert['identity']) - - explored.append(current_identity) - logging.debug("New identity explored : {pubkey}".format(pubkey=current_identity.pubkey[:5])) - self.graph_changed.emit() - self.current_identity_changed.emit("") diff --git a/src/sakia/core/graph/wot_graph.py b/src/sakia/core/graph/wot_graph.py deleted file mode 100644 index 60ab1cd28b9ece3c61eab094f059ddda35be54ab..0000000000000000000000000000000000000000 --- a/src/sakia/core/graph/wot_graph.py +++ /dev/null @@ -1,87 +0,0 @@ -import logging -import asyncio -import networkx -from .base_graph import BaseGraph -from .constants import NodeStatus - - -class WoTGraph(BaseGraph): - def __init__(self, app, community, nx_graph=None): - """ - Init WoTGraph instance - :param sakia.core.app.Application app: Application instance - :param sakia.core.community.Community community: Community instance - :param networkx.Graph nx_graph: The networkx graph - :return: - """ - super().__init__(app, community, nx_graph) - - async def initialize(self, center_identity, account_identity): - node_status = await self.node_status(center_identity, account_identity) - - self.add_identity(center_identity, node_status) - - # create Identity from node metadata - certifier_coro = asyncio.ensure_future(center_identity.unique_valid_certifiers_of(self.app.identities_registry, - self.community)) - certified_coro = asyncio.ensure_future(center_identity.unique_valid_certified_by(self.app.identities_registry, - self.community)) - - certifier_list, certified_list = await asyncio.gather(certifier_coro, certified_coro) - - # populate graph with certifiers-of - certifier_coro = asyncio.ensure_future(self.add_certifier_list(certifier_list, - center_identity, account_identity)) - # populate graph with certified-by - certified_coro = asyncio.ensure_future(self.add_certified_list(certified_list, - center_identity, account_identity)) - - await asyncio.gather(certifier_coro, certified_coro) - - async def get_shortest_path_to_identity(self, from_identity, to_identity): - """ - Return path list of nodes from from_identity to to_identity - :param identity from_identity: - :param identity to_identity: - :return: - """ - path = list() - - logging.debug("path between %s to %s..." % (from_identity.uid, to_identity.uid)) - self.add_identity(from_identity, NodeStatus.HIGHLIGHTED) - - # recursively feed graph searching for account node... - await self.explore_to_find_member(from_identity, to_identity) - - # calculate path of nodes between identity and to_identity - try: - path = networkx.shortest_path(self.nx_graph, from_identity.pubkey, to_identity.pubkey) - except networkx.exception.NetworkXException as e: - logging.debug(str(e)) - - return path - - async def explore_to_find_member(self, account_identity, to_identity): - """ - Scan graph to find identity - :param sakia.core.registry.Identity from_identity: Scan starting point - :param sakia.core.registry.Identity to_identity: Scan goal - """ - explored = [] - explorable = [account_identity] - - while len(explorable) > 0: - current = explorable.pop() - certified_list = await current.unique_valid_certified_by(self.app.identities_registry, - self.community) - - await self.add_certified_list(certified_list, current, account_identity) - if to_identity.pubkey in [data['identity'].pubkey for data in certified_list]: - return True - - explored.append(current) - for entry in certified_list: - if entry['identity'] not in explored + explorable: - explorable.append(entry['identity']) - - return False diff --git a/src/sakia/core/money/base_referential.py b/src/sakia/core/money/base_referential.py deleted file mode 100644 index 57adf6715c7316015c565d037af0a602c661a79a..0000000000000000000000000000000000000000 --- a/src/sakia/core/money/base_referential.py +++ /dev/null @@ -1,45 +0,0 @@ -from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QObject, QLocale -import asyncio - - -class BaseReferential: - """ - Interface to all referentials - """ - def __init__(self, amount, community, app, block_number=None): - self.amount = amount - self.community = community - self.app = app - self._block_number = block_number - - @classmethod - def instance(cls, amount, community, app, block_number=None): - return cls(amount, community, app, block_number) - - @classmethod - def translated_name(self): - pass - - @property - def units(self): - pass - - @property - def diff_units(self): - pass - - async def value(self): - pass - - async def differential(self): - pass - - @staticmethod - def to_si(value, digits): - pass - - async def localized(self, units=False, international_system=False): - pass - - async def diff_localized(self, units=False, international_system=False): - pass diff --git a/src/sakia/core/money/dividend_per_day.py b/src/sakia/core/money/dividend_per_day.py deleted file mode 100644 index 7528c867120fd6c249b4e3ba98466c966f2987e2..0000000000000000000000000000000000000000 --- a/src/sakia/core/money/dividend_per_day.py +++ /dev/null @@ -1,106 +0,0 @@ -from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale -from .base_referential import BaseReferential -from .udd_to_past import UDDToPast - -from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale - - -class DividendPerDay(BaseReferential): - _NAME_STR_ = QT_TRANSLATE_NOOP('DividendPerDay', 'UDD') - _REF_STR_ = QT_TRANSLATE_NOOP('DividendPerDay', "{0} {1}UDD {2}") - _UNITS_STR_ = QT_TRANSLATE_NOOP('DividendPerDay', "UDD {0}") - _FORMULA_STR_ = QT_TRANSLATE_NOOP('DividendPerDay', - """UDD(t) = (Q * 100) / (UD(t) / DT) - <br > - <table> - <tr><td>R</td><td>Dividend per day in percent</td></tr> - <tr><td>t</td><td>Last UD time</td></tr> - <tr><td>Q</td><td>Quantitative value</td></tr> - <tr><td>UD</td><td>Universal Dividend</td></tr> - <tr><td>DT</td><td>Delay between two UD in days</td></tr> - </table>""" - ) - _DESCRIPTION_STR_ = QT_TRANSLATE_NOOP('DividendPerDay', - """Universal Dividend per Day displayed in percent. - The purpose is to have a default unit that is easy to use and understand. - 100 UDD equal the Universal Dividend created per day. - """.replace('\n', '<br >')) - - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) - - @classmethod - def instance(cls, amount, community, app, block_number=None): - if app.preferences['forgetfulness']: - return cls(amount, community, app, block_number) - else: - return UDDToPast(amount, community, app, block_number) - - @classmethod - def translated_name(cls): - return QCoreApplication.translate('DividendPerDay', DividendPerDay._NAME_STR_) - - @property - def units(self): - return QCoreApplication.translate("DividendPerDay", DividendPerDay._UNITS_STR_).format(self.community.short_currency) - - @property - def formula(self): - return QCoreApplication.translate('DividendPerDay', DividendPerDay._FORMULA_STR_) - - @property - def description(self): - return QCoreApplication.translate("DividendPerDay", DividendPerDay._DESCRIPTION_STR_) - - @property - def diff_units(self): - return self.units - - async def value(self): - """ - Return relative value of amount - - value = (Q * 100) / R - Q = Quantitative value - R = UD(t) of one day - t = last UD block time - - :param int amount: Value - :param sakia.core.community.Community community: Community instance - :return: float - """ - dividend = await self.community.dividend() - params = await self.community.parameters() - if dividend > 0: - return (self.amount * 100) / (float(dividend) / (params['dt'] / 86400)) - else: - return self.amount - - async def differential(self): - return await self.value() - - async def localized(self, units=False, international_system=False): - value = await self.value() - prefix = "" - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: - return QCoreApplication.translate("Relative", DividendPerDay._REF_STR_) \ - .format(localized_value, - prefix, - self.community.short_currency if units else "") - else: - return localized_value - - async def diff_localized(self, units=False, international_system=False): - value = await self.differential() - prefix = "" - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: - return QCoreApplication.translate("Relative", DividendPerDay._REF_STR_) \ - .format(localized_value, - prefix, - self.community.short_currency if units else "") - else: - return localized_value diff --git a/src/sakia/core/money/relative_to_past.py b/src/sakia/core/money/relative_to_past.py deleted file mode 100644 index 83229eb2a1bc054831a3006265c804ec793ffc05..0000000000000000000000000000000000000000 --- a/src/sakia/core/money/relative_to_past.py +++ /dev/null @@ -1,117 +0,0 @@ -from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale, QDateTime -from .base_referential import BaseReferential - - -class RelativeToPast(BaseReferential): - _NAME_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', 'Past UD') - _REF_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', "{0} {1}UD({2}) {3}") - _UNITS_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', "UD({0}) {1}") - _FORMULA_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', - """R = Q / UD(t) - <br > - <table> - <tr><td>R</td><td>Relative value</td></tr> - <tr><td>Q</td><td>Quantitative value</td></tr> - <tr><td>UD</td><td>Universal Dividend</td></tr> - <tr><td>t</td><td>Time when the value appeared</td></tr> - </table>""" - ) - _DESCRIPTION_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', - """Relative referential using UD at the Time when the value appeared. - Relative value R is calculated by dividing the quantitative value Q by the - Universal Dividend UD at the Time when the value appeared. - All past UD created are displayed with a value of 1 UD. - This referential is practical to remember what was the value at the Time. - """.replace('\n', '<br >')) - - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) - - @classmethod - def translated_name(cls): - return QCoreApplication.translate('RelativeToPast', RelativeToPast._NAME_STR_) - - @property - def units(self): - return QCoreApplication.translate("RelativeToPast", RelativeToPast._UNITS_STR_).format('t', - self.community.short_currency) - @property - def formula(self): - return QCoreApplication.translate('RelativeToPast', RelativeToPast._FORMULA_STR_) - - @property - def description(self): - return QCoreApplication.translate("RelativeToPast", RelativeToPast._DESCRIPTION_STR_) - - @property - def diff_units(self): - return self.units - - async def value(self): - """ - Return relative to past value of amount - :return: float - """ - dividend = await self.community.dividend() - if dividend > 0: - return self.amount / float(dividend) - else: - return self.amount - - async def differential(self): - """ - Return relative to past differential value of amount - :return: float - """ - dividend = await self.community.dividend(self._block_number) - if dividend > 0: - return self.amount / float(dividend) - else: - return self.amount - - async def localized(self, units=False, international_system=False): - from . import Relative - value = await self.value() - block = await self.community.get_ud_block() - prefix = "" - if international_system: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: - return QCoreApplication.translate("RelativeToPast", RelativeToPast._REF_STR_) \ - .format(localized_value, - prefix, - QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block['medianTime']).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ), - self.community.short_currency if units else "") - else: - return localized_value - - async def diff_localized(self, units=False, international_system=False): - from . import Relative - value = await self.differential() - block = await self.community.get_ud_block(0, self._block_number) - if block: - date = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block['medianTime']).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat)) - else: - date = "###" - prefix = "" - if international_system and value != 0: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: - return QCoreApplication.translate("RelativeToPast", RelativeToPast._REF_STR_)\ - .format(localized_value,prefix,date, - self.community.short_currency if units else "") - else: - return localized_value diff --git a/src/sakia/core/money/udd_to_past.py b/src/sakia/core/money/udd_to_past.py deleted file mode 100644 index fc85821f9b243d41c2e77dda3e731942e440253a..0000000000000000000000000000000000000000 --- a/src/sakia/core/money/udd_to_past.py +++ /dev/null @@ -1,136 +0,0 @@ -from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale, QDateTime -from .base_referential import BaseReferential - - -class UDDToPast(BaseReferential): - _NAME_STR_ = QT_TRANSLATE_NOOP('UDDToPast', 'Past UUD') - _REF_STR_ = QT_TRANSLATE_NOOP('UDDToPast', "{0} {1}UUD({2}) {3}") - _UNITS_STR_ = QT_TRANSLATE_NOOP('UDDToPast', "UUD({0}) {1}") - _FORMULA_STR_ = QT_TRANSLATE_NOOP('UDDToPast', - """R = Q / UD(t) - <br > - <table> - <tr><td>R</td><td>Dividend per day in percent</td></tr> - <tr><td>t</td><td>Last UD time</td></tr> - <tr><td>Q</td><td>Quantitative value</td></tr> - <tr><td>UD</td><td>Universal Dividend</td></tr> - <tr><td>t</td><td>Time when the value appeared</td></tr> - <tr><td>DT</td><td>Delay between two UD in days</td></tr> - </table>>""" - ) - _DESCRIPTION_STR_ = QT_TRANSLATE_NOOP('UDDToPast', - """Universal Dividend per Day displayed in percent, using UD at the Time - when the value appeared. - The purpose is to have a default unit that is easy to use and understand. - 100 UDD equal the Universal Dividend created per day. - Relative referential - Relative value R is calculated by dividing the quantitative value Q by the - """.replace('\n', '<br >')) - - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) - - @classmethod - def translated_name(cls): - return QCoreApplication.translate('UDDToPast', UDDToPast._NAME_STR_) - - @property - def units(self): - return QCoreApplication.translate("UDDToPast", UDDToPast._UNITS_STR_).format('t', self.community.short_currency) - @property - def formula(self): - return QCoreApplication.translate('UDDToPast', UDDToPast._FORMULA_STR_) - - @property - def description(self): - return QCoreApplication.translate("UDDToPast", UDDToPast._DESCRIPTION_STR_) - - @property - def diff_units(self): - return self.units - - async def value(self): - """ - Return relative value of amount - - value = (Q * 100) / R - Q = Quantitative value - R = UD(t) of one day - t = last UD block time - - :param int amount: Value - :param sakia.core.community.Community community: Community instance - :return: float - """ - dividend = await self.community.dividend() - params = await self.community.parameters() - if dividend > 0: - return (self.amount * 100) / (float(dividend) / (params['dt'] / 86400)) - else: - return self.amount - - async def differential(self): - """ - Return relative value of amount - - value = (Q * 100) / R - Q = Quantitative value - R = UD(t) of one day - t = UD block time of when the value was created - - :param int amount: Value - :param sakia.core.community.Community community: Community instance - :return: float - """ - dividend = await self.community.dividend(self._block_number) - params = await self.community.parameters() - if dividend > 0: - return (self.amount * 100) / (float(dividend) / (params['dt'] / 86400)) - else: - return self.amount - - async def localized(self, units=False, international_system=False): - from . import Relative - value = await self.value() - block = await self.community.get_block() - prefix = "" - if international_system: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: - return QCoreApplication.translate("UDDToPast", UDDToPast._REF_STR_) \ - .format(localized_value, - prefix, - QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block['medianTime']).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ), - self.community.short_currency if units else "") - else: - return localized_value - - async def diff_localized(self, units=False, international_system=False): - from . import Relative - value = await self.differential() - block = await self.community.get_block(self._block_number) - prefix = "" - if international_system and value != 0: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: - return QCoreApplication.translate("UDDToPast", UDDToPast._REF_STR_)\ - .format(localized_value, - prefix, - QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block['medianTime']).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ), - self.community.short_currency if units else "") - else: - return localized_value diff --git a/src/sakia/core/net/__init__.py b/src/sakia/core/net/__init__.py deleted file mode 100644 index 6ea6364fb9f0854431d8352f088cf781026b1a1d..0000000000000000000000000000000000000000 --- a/src/sakia/core/net/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .node import Node -from .network import Network \ No newline at end of file diff --git a/src/sakia/core/net/api/bma/access.py b/src/sakia/core/net/api/bma/access.py deleted file mode 100644 index 36d08852067ff17b2a794e29ee21b78dbbd99a15..0000000000000000000000000000000000000000 --- a/src/sakia/core/net/api/bma/access.py +++ /dev/null @@ -1,315 +0,0 @@ -from PyQt5.QtCore import QObject, pyqtSlot -from duniterpy.api import bma -from duniterpy.api import errors -from .....tools.exceptions import NoPeerAvailable -from ..... import __version__ -import logging -from aiohttp.errors import ClientError, ServerDisconnectedError -import asyncio -import random -from socket import gaierror -import jsonschema -from pkg_resources import parse_version -import copy - - -class BmaAccess(QObject): - """ - This class is used to access BMA API. - """ - - __saved_requests = [str(bma.blockchain.Block), str(bma.blockchain.Parameters)] - - def __init__(self, data, network): - """ - Constructor of a network - - :param dict data: The data present in this cache - :param sakia.core.net.network.Network network: The network used to connect - """ - super().__init__() - self._data = data - self._rollback_to = None - self._pending_requests = {} - self._network = network - - @classmethod - def create(cls, network): - """ - Initialize a new BMAAccess object with empty data. - - :param sakia.core.net.network.Network network: - :return: A new BmaAccess object - :rtype: sakia.core.net.api.bma.access.BmaAccess - """ - return cls({}, network) - - @property - def data(self): - return self._data.copy() - - def load_from_json(self, json_data): - """ - Put data in the cache from json datas. - - :param dict data: The cache in json format - """ - data = {} - for entry in json_data['entries']: - key = entry['key'] - cache_key = (key[0], key[1], key[2], key[3], key[4]) - data[cache_key] = entry['value'] - self._data = data - self._rollback_to = json_data['rollback'] - - def jsonify(self): - """ - Get the cache in json format - - :return: The cache as a dict in json format - """ - data = {k: self._data[k] for k in self._data.keys()} - entries = [] - for d in data: - entries.append({'key': d, - 'value': data[d]}) - return {'rollback': self._rollback_to, - 'entries': entries} - - @staticmethod - def _gen_cache_key(request, req_args, get_args): - return (str(request), - str(tuple(frozenset(sorted(req_args.keys())))), - str(tuple(frozenset(sorted(req_args.values())))), - str(tuple(frozenset(sorted(get_args.keys())))), - str(tuple(frozenset(sorted(get_args.values()))))) - - def _compare_json(self, first, second): - """ - Compare two json dicts - :param first: the first dictionnary - :param second: the second dictionnary - :return: True if the json dicts are the same - :rtype: bool - """ - def ordered(obj): - if isinstance(obj, dict): - try: - return sorted((k, ordered(v)) for k, v in obj.items()) - except TypeError: - return obj - if isinstance(obj, list): - try: - return sorted(ordered(x) for x in obj) - except TypeError: - return obj - else: - return obj - return ordered(first) == ordered(second) - - def _get_from_cache(self, request, req_args, get_args): - """ - Get data from the cache - :param request: The requested data - :param cache_key: The key - :rtype: tuple[bool, dict] - """ - cache_key = BmaAccess._gen_cache_key(request, req_args, get_args) - if cache_key in self._data.keys(): - cached_data = self._data[cache_key] - need_reload = True - # If we detected a rollback - # We reload if we don't know if this block changed or not - if self._rollback_to: - if request is bma.blockchain.Block: - if get_args["number"] >= self._rollback_to: - need_reload = True - if request is bma.blockchain.Parameters and self._rollback_to == 0: - need_reload = True - elif str(request) in BmaAccess.__saved_requests \ - or cached_data['metadata']['block_hash'] == self._network.current_blockUID.sha_hash: - need_reload = False - ret_data = copy.deepcopy(cached_data['value']) - else: - need_reload = True - ret_data = None - return need_reload, ret_data - - def _update_rollback(self, request, req_args, get_args, data): - """ - Update the rollback - - If the request is a bma/blockchain/Block, we check if - the hash answered is the same as our hash, in which case, - we know that the rollback didn't reset blocks before this one - :param class request: A bma request class calling for data - :param dict req_args: Arguments to pass to the request constructor - :param dict get_args: Arguments to pass to the request __get__ method - :param dict data: Json data got from the blockchain - """ - if self._rollback_to and request is bma.blockchain.Block: - if get_args['number'] >= self._rollback_to: - cache_key = BmaAccess._gen_cache_key(request, req_args, get_args) - if cache_key in self._data and self._data[cache_key]['value']['hash'] == data['hash']: - self._rollback_to = get_args['number'] - - def _update_cache(self, request, req_args, get_args, data): - """ - Update data in cache and returns True if cached data changed - :param class request: A bma request class calling for data - :param dict req_args: Arguments to pass to the request constructor - :param dict get_args: Arguments to pass to the request __get__ method - :param dict data: Json data to save in cache - :return: True if data changed - :rtype: bool - """ - self._update_rollback(request, req_args, get_args, data) - - cache_key = BmaAccess._gen_cache_key(request, req_args, get_args) - if cache_key not in self._data: - self._data[cache_key] = {'metadata': {}, - 'value': {}} - - self._data[cache_key]['metadata']['block_number'] = self._network.current_blockUID.number - self._data[cache_key]['metadata']['block_hash'] = self._network.current_blockUID.sha_hash - self._data[cache_key]['metadata']['sakia_version'] = __version__ - if not self._compare_json(self._data[cache_key]['value'], data): - self._data[cache_key]['value'] = copy.deepcopy(data) - return True - return False - - def _invalidate_cache(self, post_request): - """ - Invalidate data depending on posted request - :param class post_request: The posted request - """ - invalidated = {bma.wot.Add: bma.wot.Lookup} - if post_request in invalidated: - invalidated_cache = self._data.copy() - for data in self._data: - if data[0] == str(invalidated[post_request]): - invalidated_cache.pop(data) - self._data = invalidated_cache - - def rollback(self): - """ - When a rollback is detected, we move the rollback cursor to 0 - """ - self._rollback_to = 0 - - def filter_nodes(self, request, nodes): - def compare_versions(node, version): - if node.version and node.version != '': - try: - return parse_version(node.version) >= parse_version(version) - except TypeError: - return False - else: - return True - filters = { - bma.ud.History: lambda n: compare_versions(n, "0.11.0"), - bma.tx.History: lambda n: compare_versions(n, "0.11.0"), - bma.blockchain.Membership: lambda n: compare_versions(n, "0.14") - } - if request in filters: - return [n for n in nodes if filters[request](n)] - else: - return nodes - - async def future_request(self, request, req_args={}, get_args={}): - """ - Start a request to the network and returns a future. - - :param class request: A bma request class calling for data - :param dict req_args: Arguments to pass to the request constructor - :param dict get_args: Arguments to pass to the request __get__ method - :return: The future data - :rtype: dict - """ - data = self._get_from_cache(request, req_args, get_args) - need_reload = data[0] - json_data = data[1] - - nodes = self.filter_nodes(request, self._network.synced_nodes) - if need_reload and len(nodes) > 0: - tries = 0 - while tries < 3: - node = random.choice(nodes) - conn_handler = node.endpoint.conn_handler() - req = request(conn_handler, **req_args) - try: - json_data = await req.get(**get_args, session=self._network.session) - self._update_cache(request, req_args, get_args, json_data) - return json_data - except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError) as e: - tries += 1 - except jsonschema.ValidationError as e: - logging.debug(str(e)) - tries += 1 - if len(nodes) == 0 or json_data is None: - raise NoPeerAvailable("", len(nodes)) - return json_data - - async def simple_request(self, request, req_args={}, get_args={}): - """ - Start a request to the network but don't cache its result. - - :param class request: A bma request class calling for data - :param dict req_args: Arguments to pass to the request constructor - :param dict get_args: Arguments to pass to the request __get__ method - :return: The returned data - """ - nodes = self.filter_nodes(request, self._network.synced_nodes) - if len(nodes) > 0: - tries = 0 - json_data = None - while tries < 3: - node = random.choice(nodes) - req = request(node.endpoint.conn_handler(), **req_args) - try: - json_data = await req.get(**get_args, session=self._network.session) - return json_data - except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError) as e: - tries += 1 - #except jsonschema.ValidationError as e: - # logging.debug(str(e)) - # tries += 1 - if len(nodes) == 0 or not json_data: - raise NoPeerAvailable("", len(nodes)) - return json_data - - async def broadcast(self, request, req_args={}, post_args={}): - """ - Broadcast data to a network. - Sends the data to all knew nodes. - - :param request: A duniterpy bma request class - :param req_args: Arguments to pass to the request constructor - :param post_args: Arguments to pass to the request __post__ method - :return: All nodes replies - :rtype: tuple of aiohttp replies - - .. note:: If one node accept the requests (returns 200), - the broadcast should be considered accepted by the network. - """ - nodes = random.sample(self._network.synced_nodes, 6) \ - if len(self._network.synced_nodes) > 6 \ - else self._network.synced_nodes - replies = [] - if len(nodes) > 0: - for node in nodes: - logging.debug("Trying to connect to : " + node.pubkey) - conn_handler = node.endpoint.conn_handler() - req = request(conn_handler, **req_args) - reply = asyncio.ensure_future(req.post(**post_args, session=self._network.session)) - replies.append(reply) - self._invalidate_cache(request) - else: - raise NoPeerAvailable("", len(nodes)) - - try: - result = await asyncio.gather(*replies) - return tuple(result) - except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError) as e: - pass - return () diff --git a/src/sakia/core/net/network.py b/src/sakia/core/net/network.py deleted file mode 100644 index 6fa6608e1b387a0a9537ad367b60e64c77a724da..0000000000000000000000000000000000000000 --- a/src/sakia/core/net/network.py +++ /dev/null @@ -1,441 +0,0 @@ -""" -Created on 24 févr. 2015 - -@author: inso -""" -from .node import Node -from ...tools.exceptions import InvalidNodeCurrency -from ...tools.decorators import asyncify -import logging -import aiohttp -import time -import asyncio -from duniterpy.documents import Peer, Block, BlockUID, MalformedDocumentError -from duniterpy.key import VerifyingKey -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer -from collections import Counter - -MAX_CONFIRMATIONS = 6 - - -class Network(QObject): - """ - A network is managing nodes polling and crawling of a - given community. - """ - nodes_changed = pyqtSignal() - root_nodes_changed = pyqtSignal() - new_block_mined = pyqtSignal(int) - blockchain_rollback = pyqtSignal(int) - - def __init__(self, currency, nodes, session): - """ - Constructor of a network - - :param str currency: The currency name of the community - :param list nodes: The root nodes of the network - """ - super().__init__() - self._root_nodes = nodes - self._nodes = [] - for n in nodes: - self.add_node(n) - self.currency = currency - self._must_crawl = False - self._block_found = self.current_blockUID - self._timer = QTimer() - self._client_session = session - self._discovery_stack = [] - - @classmethod - def create(cls, node): - """ - Create a new network with one knew node - Crawls the nodes from the first node to build the - community network - - :param node: The first knew node of the network - """ - nodes = [node] - network = cls(node.currency, nodes, node.session) - return network - - def merge_with_json(self, json_data, file_version): - """ - We merge with knew nodes when we - last stopped sakia - - :param dict json_data: Nodes in json format - :param NormalizedVersion file_version: The node version - """ - for data in json_data: - try: - node = Node.from_json(self.currency, data, file_version, self.session) - if node.pubkey not in [n.pubkey for n in self.nodes]: - self.add_node(node) - logging.debug("Loading : {:}".format(data['pubkey'])) - else: - other_node = [n for n in self.nodes if n.pubkey == node.pubkey][0] - other_node._uid = node.uid - other_node._version = node.version - other_node._software = node.software - other_node._peer = node.peer - switch = False - if other_node.block and node.block: - if other_node.block['hash'] != node.block['hash']: - switch = True - else: - switch = True - if switch: - other_node.set_block(node.block) - other_node.last_change = node.last_change - other_node.state = node.state - except MalformedDocumentError: - logging.debug("Could not load node {0}".format(data)) - - @classmethod - def from_json(cls, currency, json_data, file_version): - """ - Load a network from a configured community - - :param str currency: The currency name of a community - :param dict json_data: A json_data view of a network - :param NormalizedVersion file_version: the version of the json file - """ - session = aiohttp.ClientSession() - nodes = [] - for data in json_data: - try: - node = Node.from_json(currency, data, file_version, session) - nodes.append(node) - except MalformedDocumentError: - logging.debug("Could not load node {0}".format(data)) - network = cls(currency, nodes, session) - return network - - def jsonify(self): - """ - Get the network in json format. - - :return: The network as a dict in json format. - """ - data = [] - for node in self.nodes: - data.append(node.jsonify()) - return data - - @property - def quality(self): - """ - Get a ratio of the synced nodes vs the rest - """ - synced = len(self.synced_nodes) - total = len(self.nodes) - if total == 0: - ratio_synced = 0 - else: - ratio_synced = synced / total - return ratio_synced - - def start_coroutines(self): - """ - Start network nodes crawling - :return: - """ - asyncio.ensure_future(self.discover_network()) - - async def stop_coroutines(self, closing=False): - """ - Stop network nodes crawling. - """ - self._must_crawl = False - close_tasks = [] - logging.debug("Start closing") - for node in self.nodes: - close_tasks.append(asyncio.ensure_future(node.close_ws())) - logging.debug("Closing {0} websockets".format(len(close_tasks))) - if len(close_tasks) > 0: - await asyncio.wait(close_tasks, timeout=15) - if closing: - logging.debug("Closing client session") - await self._client_session.close() - logging.debug("Closed") - - @property - def session(self): - return self._client_session - - def continue_crawling(self): - return self._must_crawl - - @property - def synced_nodes(self): - """ - Get nodes which are in the ONLINE state. - """ - return [n for n in self.nodes if n.state == Node.ONLINE] - - @property - def online_nodes(self): - """ - Get nodes which are in the ONLINE state. - """ - return [n for n in self.nodes if n.state in (Node.ONLINE, Node.DESYNCED)] - - @property - def nodes(self): - """ - Get all knew nodes. - """ - return self._nodes - - @property - def root_nodes(self): - """ - Get root nodes. - """ - return self._root_nodes - - @property - def current_blockUID(self): - """ - Get the latest block considered valid - It is the most frequent last block of every known nodes - """ - blocks = [n.block for n in self.synced_nodes if n.block] - if len(blocks) > 0: - return BlockUID(blocks[0]['number'], blocks[0]['hash']) - else: - return BlockUID.empty() - - def _check_nodes_sync(self): - """ - Check nodes sync with the following rules : - 1 : The block of the majority - 2 : The more last different issuers - 3 : The more difficulty - 4 : The biggest number or timestamp - """ - # rule number 1 : block of the majority - blocks = [n.block['hash'] for n in self.online_nodes if n.block] - blocks_occurences = Counter(blocks) - blocks_by_occurences = {} - for key, value in blocks_occurences.items(): - the_block = [n.block for n in self.online_nodes if n.block and n.block['hash'] == key][0] - if value not in blocks_by_occurences: - blocks_by_occurences[value] = [the_block] - else: - blocks_by_occurences[value].append(the_block) - - if len(blocks_by_occurences) == 0: - for n in [n for n in self.online_nodes if n.state in (Node.ONLINE, Node.DESYNCED)]: - n.state = Node.ONLINE - return - - most_present = max(blocks_by_occurences.keys()) - - if len(blocks_by_occurences[most_present]) > 1: - # rule number 2 : more last different issuers - # not possible atm - blocks_by_issuers = blocks_by_occurences.copy() - most_issuers = max(blocks_by_issuers.keys()) - if len(blocks_by_issuers[most_issuers]) > 1: - # rule number 3 : biggest PowMin - blocks_by_powmin = {} - for block in blocks_by_issuers[most_issuers]: - if block['powMin'] in blocks_by_powmin: - blocks_by_powmin[block['powMin']].append(block) - else: - blocks_by_powmin[block['powMin']] = [block] - bigger_powmin = max(blocks_by_powmin.keys()) - if len(blocks_by_powmin[bigger_powmin]) > 1: - # rule number 3 : latest timestamp - blocks_by_ts = {} - for block in blocks_by_powmin[bigger_powmin]: - blocks_by_ts[block['time']] = block - latest_ts = max(blocks_by_ts.keys()) - synced_block_hash = blocks_by_ts[latest_ts]['hash'] - else: - synced_block_hash = blocks_by_powmin[bigger_powmin][0]['hash'] - else: - synced_block_hash = blocks_by_issuers[most_issuers][0]['hash'] - else: - synced_block_hash = blocks_by_occurences[most_present][0]['hash'] - - for n in self.online_nodes: - if n.block and n.block['hash'] == synced_block_hash: - n.state = Node.ONLINE - else: - n.state = Node.DESYNCED - - def _check_nodes_unique(self): - """ - Check that all nodes are unique by them pubkeys - """ - pubkeys = set() - unique_nodes = [] - for n in self.nodes: - if n.pubkey not in pubkeys: - unique_nodes.append(n) - pubkeys.add(n.pubkey) - - self._nodes = unique_nodes - - def confirmations(self, block_number): - """ - Get the number of confirmations of a data - :param int block_number: The block number of the data - :return: the number of confirmations of a data - :rtype: int - """ - if block_number is not None: - if block_number > self.current_blockUID.number: - raise ValueError("Could not compute confirmations : data block number is after current block") - return self.current_blockUID.number - block_number + 1 - else: - return 0 - - def add_node(self, node): - """ - Add a nod to the network. - """ - self._nodes.append(node) - node.changed.connect(self.handle_change) - node.error.connect(self.handle_error) - node.identity_changed.connect(self.handle_identity_change) - node.neighbour_found.connect(self.handle_new_node) - logging.debug("{:} connected".format(node.pubkey[:5])) - - def add_root_node(self, node): - """ - Add a node to the root nodes list - """ - self._root_nodes.append(node) - self.root_nodes_changed.emit() - - def remove_root_node(self, index): - """ - Remove a node from the root nodes list - """ - self._root_nodes.pop(index) - self.root_nodes_changed.emit() - - def is_root_node(self, node): - """ - Check if this node is in the root nodes - """ - return node in self._root_nodes - - def root_node_index(self, index): - """ - Get the index of a root node from its index - in all nodes list - """ - node = self.nodes[index] - return self._root_nodes.index(node) - - @asyncify - async def refresh_once(self): - for node in self._nodes: - await asyncio.sleep(1) - node.refresh(manual=True) - - async def discover_network(self): - """ - Start crawling which never stops. - To stop this crawling, call "stop_crawling" method. - """ - self._must_crawl = True - first_loop = True - asyncio.ensure_future(self.pop_discovery_stack()) - while self.continue_crawling(): - for node in self.nodes: - if self.continue_crawling(): - node.refresh() - if not first_loop: - await asyncio.sleep(15) - first_loop = False - await asyncio.sleep(15) - - logging.debug("End of network discovery") - - async def pop_discovery_stack(self): - """ - Handle poping of nodes in discovery stack - :return: - """ - while self.continue_crawling(): - try: - await asyncio.sleep(1) - peer = self._discovery_stack.pop() - pubkeys = [n.pubkey for n in self.nodes] - if peer.pubkey not in pubkeys: - logging.debug("New node found : {0}".format(peer.pubkey[:5])) - try: - node = Node.from_peer(self.currency, peer, self.session) - node.refresh(manual=True) - self.add_node(node) - self.nodes_changed.emit() - except InvalidNodeCurrency as e: - logging.debug(str(e)) - else: - node = [n for n in self.nodes if n.pubkey == peer.pubkey][0] - if node.peer.blockUID.number < peer.blockUID.number: - logging.debug("Update node : {0}".format(peer.pubkey[:5])) - node.peer = peer - except IndexError: - await asyncio.sleep(2) - - def handle_new_node(self, peer): - key = VerifyingKey(peer.pubkey) - if key.verify_document(peer): - if len(self._discovery_stack) < 1000 \ - and peer.signatures[0] not in [p.signatures[0] for p in self._discovery_stack]: - logging.debug("Stacking new peer document : {0}".format(peer.pubkey)) - self._discovery_stack.append(peer) - else: - logging.debug("Wrong document received : {0}".format(peer.signed_raw())) - - @pyqtSlot() - def handle_identity_change(self): - node = self.sender() - self._check_nodes_unique() - if node in self._root_nodes: - self.root_nodes_changed.emit() - self.nodes_changed.emit() - - @pyqtSlot() - def handle_error(self): - node = self.sender() - if node.state in (Node.OFFLINE, Node.CORRUPTED) and \ - node.last_change + 3600 < time.time(): - node.disconnect() - self.nodes.remove(node) - self.nodes_changed.emit() - - @pyqtSlot() - def handle_change(self): - node = self.sender() - - if node.state in (Node.ONLINE, Node.DESYNCED): - self._check_nodes_sync() - self._check_nodes_unique() - self.nodes_changed.emit() - - if node.state == Node.ONLINE: - logging.debug("{0} -> {1}".format(self._block_found.sha_hash[:10], self.current_blockUID.sha_hash[:10])) - if self._block_found.sha_hash != self.current_blockUID.sha_hash: - logging.debug("Latest block changed : {0}".format(self.current_blockUID.number)) - # If new latest block is lower than the previously found one - # or if the previously found block is different locally - # than in the main chain, we declare a rollback - if self._block_found.number and \ - self.current_blockUID.number <= self._block_found.number \ - or node.main_chain_previous_block and \ - node.main_chain_previous_block['hash'] != self._block_found.sha_hash: - - self._block_found = self.current_blockUID - self.blockchain_rollback.emit(self.current_blockUID.number) - else: - self._block_found = self.current_blockUID - self.new_block_mined.emit(self.current_blockUID.number) diff --git a/src/sakia/core/net/node.py b/src/sakia/core/net/node.py deleted file mode 100644 index 9ab058cad3b8db0309af78909203ac1e1e09e4f9..0000000000000000000000000000000000000000 --- a/src/sakia/core/net/node.py +++ /dev/null @@ -1,643 +0,0 @@ -""" -Created on 21 févr. 2015 - -@author: inso -""" - -from duniterpy.documents.peer import Peer, Endpoint, BMAEndpoint -from duniterpy.documents import Block, BlockUID, MalformedDocumentError -from ...tools.exceptions import InvalidNodeCurrency -from ...tools.decorators import asyncify -from duniterpy.api import bma, errors -from duniterpy.api.bma import ConnectionHandler - -from aiohttp.errors import WSServerHandshakeError, ClientResponseError -from aiohttp.errors import ClientError, DisconnectedError -from asyncio import TimeoutError -import logging -import time -import jsonschema -import asyncio -import aiohttp -from pkg_resources import parse_version -from socket import gaierror - -from PyQt5.QtCore import QObject, pyqtSignal - - -class Node(QObject): - """ - A node is a peer send from the client point of view. - This node can have multiple states : - - ONLINE : The node is available for requests - - OFFLINE: The node is disconnected - - DESYNCED : The node is online but is desynced from the network - - CORRUPTED : The node is corrupted, some weird behaviour is going on - """ - - ONLINE = 1 - OFFLINE = 2 - DESYNCED = 3 - CORRUPTED = 4 - - changed = pyqtSignal() - error = pyqtSignal() - identity_changed = pyqtSignal() - neighbour_found = pyqtSignal(Peer) - - def __init__(self, peer, uid, pubkey, block, - state, last_change, last_merkle, - software, version, fork_window, - session): - """ - Constructor - """ - super().__init__() - self._peer = peer - self._uid = uid - self._pubkey = pubkey - self._block = block - self.main_chain_previous_block = None - self._state = state - self._neighbours = [] - self._last_change = last_change - self._last_merkle = last_merkle - self._software = software - self._version = version - self._fork_window = fork_window - self._refresh_counter = 19 - self._ws_tasks = {'block': None, - 'peer': None} - self._connected = {'block': False, - 'peer': False} - self._session = session - - def __del__(self): - for ws in self._ws_tasks.values(): - if ws: - ws.cancel() - - @classmethod - async def from_address(cls, currency, address, port, session): - """ - Factory method to get a node from a given address - - :param str currency: The node currency. None if we don't know\ - the currency it should have, for example if its the first one we add - :param str address: The node address - :param int port: The node port - :return: A new node - :rtype: sakia.core.net.Node - """ - peer_data = await bma.network.Peering(ConnectionHandler(address, port)).get(session) - - peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['raw'], - peer_data['signature'])) - - if currency is not None: - if peer.currency != currency: - raise InvalidNodeCurrency(peer.currency, currency) - - node = cls(peer, - "", peer.pubkey, None, Node.ONLINE, time.time(), - {'root': "", 'leaves': []}, "", "", 0, session) - logging.debug("Node from address : {:}".format(str(node))) - return node - - @classmethod - def from_peer(cls, currency, peer, session): - """ - Factory method to get a node from a peer document. - - :param str currency: The node currency. None if we don't know\ - the currency it should have, for example if its the first one we add - :param peer: The peer document - :return: A new node - :rtype: sakia.core.net.Node - """ - if currency is not None: - if peer.currency != currency: - raise InvalidNodeCurrency(peer.currency, currency) - - node = cls(peer, "", peer.pubkey, None, - Node.OFFLINE, time.time(), - {'root': "", 'leaves': []}, - "", "", 0, session) - logging.debug("Node from peer : {:}".format(str(node))) - return node - - @classmethod - def from_json(cls, currency, data, file_version, session): - """ - Loads a node from json data - - :param str currency: the currency of the community - :param dict data: the json data of the node - :param NormalizedVersion file_version: the version of the file - :return: A new node - :rtype: Node - """ - endpoints = [] - uid = "" - pubkey = "" - software = "" - version = "" - fork_window = 0 - block = None - last_change = time.time() - state = Node.OFFLINE - if 'uid' in data: - uid = data['uid'] - - if 'pubkey' in data: - pubkey = data['pubkey'] - - if 'last_change' in data: - last_change = data['last_change'] - - if 'block' in data: - block = data['block'] - - if 'state' in data: - state = data['state'] - - if 'software' in data: - software = data['software'] - - if 'version' in data: - version = data['version'] - - if 'fork_window' in data: - fork_window = data['fork_window'] - - if parse_version("0.11") <= file_version < parse_version("0.12dev1") : - for endpoint_data in data['endpoints']: - endpoints.append(Endpoint.from_inline(endpoint_data)) - - if currency in data: - currency = data['currency'] - - peer = Peer(2, currency, pubkey, BlockUID(0, Block.Empty_Hash), endpoints, "SOMEFAKESIGNATURE") - else: - peer = Peer.from_signed_raw(data['peer']) - - node = cls(peer, uid, pubkey, block, - state, last_change, - {'root': "", 'leaves': []}, - software, version, fork_window, session) - - logging.debug("Node from json : {:}".format(str(node))) - return node - - def jsonify_root_node(self): - logging.debug("Saving root node : {:}".format(str(self))) - data = {'pubkey': self._pubkey, - 'uid': self._uid, - 'peer': self._peer.signed_raw()} - return data - - def jsonify(self): - logging.debug("Saving node : {:}".format(str(self))) - data = {'pubkey': self._pubkey, - 'uid': self._uid, - 'peer': self._peer.signed_raw(), - 'state': self._state, - 'last_change': self._last_change, - 'block': self.block, - 'software': self._software, - 'version': self._version, - 'fork_window': self._fork_window - } - return data - - async def close_ws(self): - for ws in self._ws_tasks.values(): - if ws: - ws.cancel() - await asyncio.sleep(0) - closed = False - while not closed: - for ws in self._ws_tasks.values(): - if ws: - closed = False - break - else: - closed = True - await asyncio.sleep(0) - await asyncio.sleep(0) - - @property - def session(self): - return self._session - - @property - def pubkey(self): - return self._pubkey - - @property - def endpoint(self) -> BMAEndpoint: - return next((e for e in self._peer.endpoints if type(e) is BMAEndpoint)) - - @property - def block(self): - return self._block - - def set_block(self, block): - self._block = block - - @property - def state(self): - return self._state - - @property - def currency(self): - return self._peer.currency - - @property - def neighbours(self): - return self._neighbours - - @property - def uid(self): - return self._uid - - @property - def last_change(self): - return self._last_change - - @property - def software(self): - return self._software - - @property - def peer(self): - return self._peer - - @peer.setter - def peer(self, new_peer): - if self._peer != new_peer: - self._peer = new_peer - self.changed.emit() - - @software.setter - def software(self, new_soft): - if self._software != new_soft: - self._software = new_soft - self.changed.emit() - - @property - def version(self): - return self._version - - @version.setter - def version(self, new_version): - if self._version != new_version: - self._version = new_version - self.changed.emit() - - @last_change.setter - def last_change(self, val): - #logging.debug("{:} | Changed state : {:}".format(self.pubkey[:5], - # val)) - self._last_change = val - - @state.setter - def state(self, new_state): - #logging.debug("{:} | Last state : {:} / new state : {:}".format(self.pubkey[:5], - # self.state, new_state)) - - if self._state != new_state: - self.last_change = time.time() - self._state = new_state - self.changed.emit() - if new_state in (Node.OFFLINE, Node.ONLINE): - self.error.emit() - - @property - def fork_window(self): - return self._fork_window - - @fork_window.setter - def fork_window(self, new_fork_window): - if self._fork_window != new_fork_window: - self._fork_window = new_fork_window - self.changed.emit() - - def refresh(self, manual=False): - """ - Refresh all data of this node - :param bool manual: True if the refresh was manually initiated - """ - if not self._ws_tasks['block']: - self._ws_tasks['block'] = asyncio.ensure_future(self.connect_current_block()) - - if not self._ws_tasks['peer']: - self._ws_tasks['peer'] = asyncio.ensure_future(self.connect_peers()) - - if manual: - asyncio.ensure_future(self.request_peers()) - - if self._refresh_counter % 20 == 0 or manual: - self.refresh_informations() - self.refresh_uid() - self.refresh_summary() - self._refresh_counter = self._refresh_counter if manual else 1 - else: - self._refresh_counter += 1 - - async def connect_current_block(self): - """ - Connects to the websocket entry point of the node - If the connection fails, it tries the fallback mode on HTTP GET - """ - if not self._connected['block']: - try: - conn_handler = self.endpoint.conn_handler() - block_websocket = bma.ws.Block(conn_handler) - ws_connection = block_websocket.connect(self._session) - async with ws_connection as ws: - self._connected['block'] = True - logging.debug("Connected successfully to block ws : {0}".format(self.pubkey[:5])) - async for msg in ws: - if msg.tp == aiohttp.MsgType.text: - logging.debug("Received a block : {0}".format(self.pubkey[:5])) - block_data = block_websocket.parse_text(msg.data) - await self.refresh_block(block_data) - elif msg.tp == aiohttp.MsgType.closed: - break - elif msg.tp == aiohttp.MsgType.error: - break - except (WSServerHandshakeError, ClientResponseError, ValueError) as e: - logging.debug("Websocket block {0} : {1} - {2}".format(type(e).__name__, str(e), self.pubkey[:5])) - await self.request_current_block() - except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - finally: - self._connected['block'] = False - self._ws_tasks['block'] = None - - async def request_current_block(self): - """ - Request a node on the HTTP GET interface - If an error occurs, the node is considered offline - """ - try: - conn_handler = self.endpoint.conn_handler() - block_data = await bma.blockchain.Current(conn_handler).get(self._session) - await self.refresh_block(block_data) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - self.main_chain_previous_block = None - self.set_block(None) - else: - self.state = Node.OFFLINE - logging.debug("Error in block reply : {0}".format(self.pubkey[:5])) - logging.debug(str(e)) - self.changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - - async def refresh_block(self, block_data): - """ - Refresh the blocks of this node - :param dict block_data: The block data in json format - """ - conn_handler = self.endpoint.conn_handler() - - logging.debug("Requesting {0}".format(conn_handler)) - block_hash = block_data['hash'] - self.state = Node.ONLINE - - if not self.block or block_hash != self.block['hash']: - try: - if self.block: - self.main_chain_previous_block = await bma.blockchain.Block(conn_handler, - self.block['number']).get(self._session) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - self.main_chain_previous_block = None - else: - self.state = Node.OFFLINE - logging.debug("Error in previous block reply : {0}".format(self.pubkey[:5])) - logging.debug(str(e)) - self.changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - finally: - self.set_block(block_data) - logging.debug("Changed block {0} -> {1}".format(self.block['number'], - block_data['number'])) - self.changed.emit() - - @asyncify - async def refresh_informations(self): - """ - Refresh basic information (pubkey and currency) - """ - conn_handler = self.endpoint.conn_handler() - - try: - peering_data = await bma.network.Peering(conn_handler).get(self._session) - node_pubkey = peering_data["pubkey"] - node_currency = peering_data["currency"] - self.state = Node.ONLINE - - if peering_data['raw'] != self.peer.raw(): - peer = Peer.from_signed_raw("{0}{1}\n".format(peering_data['raw'], peering_data['signature'])) - if peer.blockUID.number > peer.blockUID.number: - self.peer = Peer.from_signed_raw("{0}{1}\n".format(peering_data['raw'], peering_data['signature'])) - - if node_pubkey != self.pubkey: - self._pubkey = node_pubkey - self.identity_changed.emit() - - if node_currency != self.currency: - self.state = Node.CORRUPTED - logging.debug("Change : new state corrupted") - self.changed.emit() - - except errors.DuniterError as e: - if e.ucode == errors.PEER_NOT_FOUND: - logging.debug("Error in peering reply : {0}".format(str(e))) - self.state = Node.OFFLINE - self.changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey[:5])) - self.state = Node.OFFLINE - except (MalformedDocumentError, jsonschema.ValidationError) as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - - @asyncify - async def refresh_summary(self): - """ - Refresh the summary of this node - """ - conn_handler = self.endpoint.conn_handler() - - try: - summary_data = await bma.node.Summary(conn_handler).get(self._session) - self.software = summary_data["duniter"]["software"] - self.version = summary_data["duniter"]["version"] - self.state = Node.ONLINE - if "forkWindowSize" in summary_data["duniter"]: - self.fork_window = summary_data["duniter"]["forkWindowSize"] - else: - self.fork_window = 0 - except (ClientError, gaierror, TimeoutError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - - @asyncify - async def refresh_uid(self): - """ - Refresh the node UID - """ - conn_handler = self.endpoint.conn_handler() - try: - data = await bma.wot.Lookup(conn_handler, self.pubkey).get(self._session) - self.state = Node.ONLINE - timestamp = BlockUID.empty() - uid = "" - for result in data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - for uid in uids: - if BlockUID.from_str(uid["meta"]["timestamp"]) >= timestamp: - timestamp = uid["meta"]["timestamp"] - uid = uid["uid"] - if self._uid != uid: - self._uid = uid - self.identity_changed.emit() - except errors.DuniterError as e: - if e.ucode == errors.NO_MATCHING_IDENTITY: - logging.debug("UID not found : {0}".format(self.pubkey[:5])) - else: - logging.debug("error in uid reply : {0}".format(self.pubkey[:5])) - self.state = Node.OFFLINE - self.identity_changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - - async def connect_peers(self): - """ - Connects to the peer websocket entry point - If the connection fails, it tries the fallback mode on HTTP GET - """ - if not self._connected['peer']: - try: - conn_handler = self.endpoint.conn_handler() - peer_websocket = bma.ws.Peer(conn_handler) - ws_connection = peer_websocket.connect(self._session) - async with ws_connection as ws: - self._connected['peer'] = True - logging.debug("Connected successfully to peer ws : {0}".format(self.pubkey[:5])) - async for msg in ws: - if msg.tp == aiohttp.MsgType.text: - logging.debug("Received a peer : {0}".format(self.pubkey[:5])) - peer_data = peer_websocket.parse_text(msg.data) - self.refresh_peer_data(peer_data) - elif msg.tp == aiohttp.MsgType.closed: - break - elif msg.tp == aiohttp.MsgType.error: - break - except (WSServerHandshakeError, ClientResponseError, ValueError) as e: - logging.debug("Websocket peer {0} : {1} - {2}".format(type(e).__name__, str(e), self.pubkey[:5])) - await self.request_peers() - except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - finally: - self._connected['peer'] = False - self._ws_tasks['peer'] = None - - async def request_peers(self): - """ - Refresh the list of peers knew by this node - """ - conn_handler = self.endpoint.conn_handler() - - try: - peers_data = await bma.network.peering.Peers(conn_handler).get(leaves='true', session=self._session) - self.state = Node.ONLINE - if peers_data['root'] != self._last_merkle['root']: - leaves = [leaf for leaf in peers_data['leaves'] - if leaf not in self._last_merkle['leaves']] - for leaf_hash in leaves: - try: - leaf_data = await bma.network.peering.Peers(conn_handler).get(leaf=leaf_hash, - session=self._session) - self.refresh_peer_data(leaf_data['leaf']['value']) - except (AttributeError, ValueError, errors.DuniterError) as e: - logging.debug("{pubkey} : Incorrect peer data in {leaf}".format(pubkey=self.pubkey[:5], - leaf=leaf_hash)) - self.state = Node.OFFLINE - self.changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey[:5])) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey[:5])) - self.state = Node.CORRUPTED - self._last_merkle = {'root' : peers_data['root'], - 'leaves': peers_data['leaves']} - except errors.DuniterError as e: - if e.ucode == errors.PEER_NOT_FOUND: - logging.debug("Error in peers reply") - self.state = Node.OFFLINE - self.changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey)) - self.state = Node.OFFLINE - except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.pubkey)) - self.state = Node.CORRUPTED - - def refresh_peer_data(self, peer_data): - if "raw" in peer_data: - try: - str_doc = "{0}{1}\n".format(peer_data['raw'], - peer_data['signature']) - peer_doc = Peer.from_signed_raw(str_doc) - self.neighbour_found.emit(peer_doc) - except MalformedDocumentError as e: - logging.debug(str(e)) - else: - logging.debug("Incorrect leaf reply") - - def __str__(self): - return ','.join([str(self.pubkey), str(self.endpoint.server), - str(self.endpoint.ipv4), str(self.endpoint.port), - str(self.block['number'] if self.block else "None"), - str(self.currency), str(self.state), str(self.neighbours)]) diff --git a/src/sakia/core/registry/__init__.py b/src/sakia/core/registry/__init__.py deleted file mode 100644 index 5180c368bb56a87483d542c19bb83c71133d1a94..0000000000000000000000000000000000000000 --- a/src/sakia/core/registry/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .identities import IdentitiesRegistry -from .identity import Identity, LocalState, BlockchainState \ No newline at end of file diff --git a/src/sakia/core/registry/identities.py b/src/sakia/core/registry/identities.py deleted file mode 100644 index cd0bb3623a8e843c7bf038f0fe4f15c306b54dd9..0000000000000000000000000000000000000000 --- a/src/sakia/core/registry/identities.py +++ /dev/null @@ -1,163 +0,0 @@ -from duniterpy.api import bma, errors -from duniterpy.documents import BlockUID -from .identity import Identity, LocalState, BlockchainState -from pkg_resources import parse_version -import asyncio -from aiohttp.errors import ClientError -from ...tools.exceptions import NoPeerAvailable - - -class IdentitiesRegistry: - """ - Core class to handle identities lookup - """ - def __init__(self, instances={}): - """ - Initializer of the IdentitiesRegistry - - :param dict instances: A dictionary containing identities based on communities - :return: An IdentitiesRegistry object - :rtype: IdentitiesRegistry - """ - self._instances = instances - - def load_json(self, json_data): - """ - Load json data - - :param dict json_data: The identities in json format - """ - instances = {} - version = parse_version(json_data['version']) - for currency in json_data['registry']: - instances[currency] = {} - for person_data in json_data['registry'][currency]: - pubkey = person_data['pubkey'] - if pubkey not in instances: - person = Identity.from_json(person_data, version) - instances[currency][person.pubkey] = person - self._instances = instances - - def jsonify(self): - communities_json = {} - for currency in self._instances: - identities_json = [] - for identity in self._instances[currency].values(): - identities_json.append(identity.jsonify()) - communities_json[currency] = identities_json - return {'registry': communities_json} - - def _identities(self, community): - """ - If the registry do not have data for this community - Create a new dict and return it - :param sakia.core.Community community: the community - :return: The identities of the community - :rtype: dict - """ - try: - return self._instances[community.currency] - except KeyError: - self._instances[community.currency] = {} - return self._identities(community) - - async def _find_by_lookup(self, pubkey, community): - identity = self._identities(community)[pubkey] - lookup_tries = 0 - while lookup_tries < 3: - try: - data = await community.bma_access.simple_request(bma.wot.Lookup, - req_args={'search': pubkey}) - timestamp = BlockUID.empty() - for result in data['results']: - if result["pubkey"] == identity.pubkey: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - identity.sigdate = BlockUID.from_str(uid_data["meta"]["timestamp"]) - identity.uid = uid_data["uid"] - identity.blockchain_state = BlockchainState.BUFFERED - identity.local_state = LocalState.PARTIAL - timestamp = identity.sigdate - return identity - except errors.DuniterError as e: - lookup_tries += 1 - except asyncio.TimeoutError: - lookup_tries += 1 - except ClientError: - lookup_tries += 1 - except NoPeerAvailable: - return identity - return identity - - async def future_find(self, pubkey, community): - """ - - :param pubkey: The pubkey we look for - :param community: The community where we look for the identity - :return: The identity found - :rtype: sakia.core.registry.Identity - """ - if pubkey in self._identities(community): - identity = self._identities(community)[pubkey] - else: - identity = Identity.empty(pubkey) - self._identities(community)[pubkey] = identity - tries = 0 - while tries < 3 and identity.local_state == LocalState.NOT_FOUND: - try: - data = await community.bma_access.simple_request(bma.blockchain.Membership, - req_args={'search': pubkey}) - identity.uid = data['uid'] - identity.sigdate = BlockUID.from_str(data['sigDate']) - identity.local_state = LocalState.PARTIAL - identity.blockchain_state = BlockchainState.VALIDATED - except errors.DuniterError as e: - if errors.NO_MEMBER_MATCHING_PUB_OR_UID: - identity = await self._find_by_lookup(pubkey, community) - return identity - else: - tries += 1 - except asyncio.TimeoutError: - tries += 1 - except ClientError: - tries += 1 - except NoPeerAvailable: - return identity - return identity - - def from_handled_data(self, uid, pubkey, sigdate, blockchain_state, community): - """ - Get a person from a metadata dict. - A metadata dict has a 'text' key corresponding to the person uid, - and a 'id' key corresponding to the person pubkey. - - :param str uid: The person uid, also known as its uid on the network - :param str pubkey: The person pubkey - :param BlockUID sig_date: The date of signature of the self certification - :param LocalState local_state: The local status of the identity - :param sakia.core.Community community: The community from which we found data - :rtype: sakia.core.registry.Identity - """ - identities = self._identities(community) - if pubkey in identities: - if identities[pubkey].blockchain_state == BlockchainState.NOT_FOUND: - identities[pubkey].blockchain_state = blockchain_state - elif identities[pubkey].blockchain_state != BlockchainState.VALIDATED \ - and blockchain_state == BlockchainState.VALIDATED: - identities[pubkey].blockchain_state = blockchain_state - - if identities[pubkey].uid != uid: - identities[pubkey].uid = uid - - if sigdate and identities[pubkey].sigdate != sigdate: - identities[pubkey].sigdate = sigdate - - if identities[pubkey].local_state == LocalState.NOT_FOUND: - identities[pubkey].local_state = LocalState.COMPLETED - - return identities[pubkey] - else: - identity = Identity.from_handled_data(uid, pubkey, sigdate, blockchain_state) - self._identities(community)[pubkey] = identity - return identity diff --git a/src/sakia/core/registry/identity.py b/src/sakia/core/registry/identity.py deleted file mode 100644 index fcc16720a99c12e2f79b5aea89fc323a79452c91..0000000000000000000000000000000000000000 --- a/src/sakia/core/registry/identity.py +++ /dev/null @@ -1,569 +0,0 @@ -""" -Created on 11 févr. 2014 - -@author: inso -""" - -import logging -import time -from enum import Enum -from pkg_resources import parse_version - -from duniterpy.documents import BlockUID, SelfCertification, MalformedDocumentError -from duniterpy.api import bma, errors -from duniterpy.api.bma import PROTOCOL_VERSION - -from ...tools.exceptions import Error, NoPeerAvailable,\ - MembershipNotFoundError, LookupFailureError -from PyQt5.QtCore import QObject, pyqtSignal - - -class LocalState(Enum): - """ - The local state describes how the identity exists locally : - COMPLETED means all its related datas (certifiers, certified...) - were succefully downloaded - PARTIAL means not all data are present locally - NOT_FOUND means it could not be found anywhere - """ - NOT_FOUND = 0 - PARTIAL = 1 - COMPLETED = 2 - - -class BlockchainState(Enum): - """ - The blockchain state describes how the identity - was found : - VALIDATED means it was found in the blockchain - BUFFERED means it was found via a lookup but not in the - blockchain - NOT_FOUND means it could not be found anywhere - """ - NOT_FOUND = 0 - BUFFERED = 1 - VALIDATED = 2 - - -class Identity(QObject): - """ - A person with a uid and a pubkey - """ - def __init__(self, uid, pubkey, sigdate, local_state, blockchain_state): - """ - Initializing a person object. - - :param str uid: The identity uid, also known as its uid on the network - :param str pubkey: The identity pubkey - :parma BlockUID sig_date: The date of signature of the self certification - :param LocalState local_state: The local status of the identity - :param BlockchainState blockchain_state: The blockchain status of the identity - """ - if sigdate: - assert type(sigdate) is BlockUID - super().__init__() - self.uid = uid - self.pubkey = pubkey - self._sigdate = sigdate - self.local_state = local_state - self.blockchain_state = blockchain_state - - @classmethod - def empty(cls, pubkey): - return cls("", pubkey, None, LocalState.NOT_FOUND, BlockchainState.NOT_FOUND) - - @classmethod - def from_handled_data(cls, uid, pubkey, sigdate, blockchain_state): - return cls(uid, pubkey, sigdate, LocalState.COMPLETED, blockchain_state) - - @classmethod - def from_json(cls, json_data, version): - """ - Create a person from json data - - :param dict json_data: The person as a dict in json format - :return: A new person if pubkey wasn't known, else a new person instance. - """ - pubkey = json_data['pubkey'] - uid = json_data['uid'] - local_state = LocalState[json_data['local_state']] - blockchain_state = BlockchainState[json_data['blockchain_state']] - if version >= parse_version("0.20.0dev0") and json_data['sigdate']: - sigdate = BlockUID.from_str(json_data['sigdate']) - else: - sigdate = BlockUID.empty() - - return cls(uid, pubkey, sigdate, local_state, blockchain_state) - - @property - def sigdate(self): - return self._sigdate - - @sigdate.setter - def sigdate(self, sigdate): - assert type(sigdate) is BlockUID - self._sigdate = sigdate - - async def selfcert(self, community): - """ - Get the identity self certification. - This request is not cached in the person object. - - :param sakia.core.community.Community community: The community target to request the self certification - :return: A SelfCertification duniterpy object - :rtype: duniterpy.documents.certification.SelfCertification - """ - try: - timestamp = BlockUID.empty() - lookup_data = await community.bma_access.future_request(bma.wot.Lookup, - req_args={'search': self.pubkey}) - - for result in lookup_data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - for uid_data in uids: - # If the sigDate was written in the blockchain - if self._sigdate and BlockUID.from_str(uid_data["meta"]["timestamp"]) == self._sigdate: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - uid = uid_data["uid"] - signature = uid_data["self"] - # Else we choose the latest one found - elif BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - uid = uid_data["uid"] - signature = uid_data["self"] - - if not self.sigdate: - self.sigdate = timestamp - - return SelfCertification(PROTOCOL_VERSION, - community.currency, - self.pubkey, - uid, - timestamp, - signature) - except errors.DuniterError as e: - if e.ucode == errors.NO_MATCHING_IDENTITY: - raise LookupFailureError(self.pubkey, community) - except MalformedDocumentError: - raise LookupFailureError(self.pubkey, community) - except NoPeerAvailable: - logging.debug("No peer available") - - async def get_join_date(self, community): - """ - Get the person join date. - This request is not cached in the person object. - - :param sakia.core.community.Community community: The community target to request the join date - :return: A datetime object - """ - try: - search = await community.bma_access.future_request(bma.blockchain.Membership, - {'search': self.pubkey}) - if len(search['memberships']) > 0: - membership_data = search['memberships'][0] - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': membership_data['blockNumber']}) - return block['medianTime'] - except errors.DuniterError as e: - if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: - raise MembershipNotFoundError(self.pubkey, community.name) - except NoPeerAvailable as e: - logging.debug(str(e)) - raise MembershipNotFoundError(self.pubkey, community.name) - - async def get_expiration_date(self, community): - try: - membership = await self.membership(community) - join_block_number = membership['blockNumber'] - try: - join_block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': join_block_number}) - - parameters = await community.bma_access.future_request(bma.blockchain.Parameters) - join_date = join_block['medianTime'] - expiration_date = join_date + parameters['sigValidity'] - except NoPeerAvailable: - expiration_date = None - except errors.DuniterError as e: - logging.debug("Expiration date not found") - expiration_date = None - except MembershipNotFoundError: - expiration_date = None - return expiration_date - - -#TODO: Manage 'OUT' memberships ? Maybe ? - async def membership(self, community): - """ - Get the person last membership document. - - :param sakia.core.community.Community community: The community target to request the join date - :return: The membership data in BMA json format - :rtype: dict - """ - try: - search = await community.bma_access.future_request(bma.blockchain.Membership, - {'search': self.pubkey}) - block_number = -1 - membership_data = None - - for ms in search['memberships']: - if ms['blockNumber'] > block_number: - block_number = ms['blockNumber'] - if 'type' in ms: - if ms['type'] is 'IN': - membership_data = ms - else: - membership_data = ms - if membership_data: - return membership_data - else: - raise MembershipNotFoundError(self.pubkey, community.name) - - except errors.DuniterError as e: - if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: - raise MembershipNotFoundError(self.pubkey, community.name) - else: - logging.debug(str(e)) - raise MembershipNotFoundError(self.pubkey, community.name) - except NoPeerAvailable as e: - logging.debug(str(e)) - raise MembershipNotFoundError(self.pubkey, community.name) - - async def published_uid(self, community): - try: - data = await community.bma_access.future_request(bma.wot.Lookup, - req_args={'search': self.pubkey}) - timestamp = BlockUID.empty() - - for result in data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - person_uid = "" - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] - person_uid = uid_data["uid"] - if person_uid == self.uid: - return True - except errors.DuniterError as e: - logging.debug("Lookup error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return False - - async def uid_is_revokable(self, community): - published = await self.published_uid(community) - if published: - try: - await community.bma_access.future_request(bma.wot.CertifiersOf, - {'search': self.pubkey}) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Certifiers of error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return False - - async def is_member(self, community): - """ - Check if the person is a member of a community - - :param sakia.core.community.Community community: The community target to request the join date - :return: True if the person is a member of a community - """ - try: - certifiers = await community.bma_access.future_request(bma.wot.CertifiersOf, - {'search': self.pubkey}) - return certifiers['isMember'] - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - pass - except NoPeerAvailable as e: - logging.debug(str(e)) - return False - - async def certifiers_of(self, identities_registry, community): - """ - Get the list of this person certifiers - - :param sakia.core.registry.identities.IdentitiesRegistry identities_registry: The identities registry - :param sakia.core.community.Community community: The community target - :return: The list of the certifiers of this community - :rtype: list - """ - certifiers = list() - try: - data = await community.bma_access.future_request(bma.wot.CertifiersOf, - {'search': self.pubkey}) - - for certifier_data in data['certifications']: - certifier = {} - certifier['identity'] = identities_registry.from_handled_data(certifier_data['uid'], - certifier_data['pubkey'], - None, - BlockchainState.VALIDATED, - community) - certifier['cert_time'] = certifier_data['cert_time']['medianTime'] - if certifier_data['written']: - certifier['block_number'] = certifier_data['written']['number'] - else: - certifier['block_number'] = None - - certifiers.append(certifier) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Certifiers of error : {0}".format(str(e))) - else: - logging.debug(str(e)) - except NoPeerAvailable as e: - logging.debug(str(e)) - - try: - data = await community.bma_access.future_request(bma.wot.Lookup, {'search': self.pubkey}) - for result in data['results']: - if result["pubkey"] == self.pubkey: - self._refresh_uid(result['uids']) - for uid_data in result['uids']: - for certifier_data in uid_data['others']: - for uid in certifier_data['uids']: - # add a certifier - certifier = {} - certifier['identity'] = identities_registry.\ - from_handled_data(uid, - certifier_data['pubkey'], - None, - BlockchainState.BUFFERED, - community) - certifier['cert_time'] = await community.time(certifier_data['meta']['block_number']) - certifier['block_number'] = None - - certifiers.append(certifier) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Lookup error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return certifiers - - async def certified_by(self, identities_registry, community): - """ - Get the list of persons certified by this person - :param sakia.core.registry.IdentitiesRegistry identities_registry: The registry - :param sakia.core.community.Community community: The community target - :return: The list of the certified persons of this community in BMA json format - :rtype: list - """ - certified_list = list() - try: - data = await community.bma_access.future_request(bma.wot.CertifiedBy, {'search': self.pubkey}) - for certified_data in data['certifications']: - certified = {} - certified['identity'] = identities_registry.from_handled_data(certified_data['uid'], - certified_data['pubkey'], - None, - BlockchainState.VALIDATED, - community) - certified['cert_time'] = certified_data['cert_time']['medianTime'] - if certified_data['written']: - certified['block_number'] = certified_data['written']['number'] - else: - certified['block_number'] = None - certified_list.append(certified) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Certified by error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - - try: - data = await community.bma_access.future_request(bma.wot.Lookup, {'search': self.pubkey}) - for result in data['results']: - if result["pubkey"] == self.pubkey: - self._refresh_uid(result['uids']) - for certified_data in result['signed']: - certified = {} - certified['identity'] = identities_registry.from_handled_data(certified_data['uid'], - certified_data['pubkey'], - None, - BlockchainState.BUFFERED, - community) - timestamp = BlockUID.from_str(certified_data['meta']['timestamp']) - certified['cert_time'] = await community.time(timestamp.number) - certified['block_number'] = None - certified_list.append(certified) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Lookup error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return certified_list - - async def _unique_valid(self, cert_list, community): - """ - Get the certifications in the blockchain and in the pools - Get only unique and last certification for each pubkey - :param list cert_list: The certifications list to filter - :param sakia.core.community.Community community: The community target - :return: The list of the certifiers of this community - :rtype: list - """ - unique_valid = [] - #  add certifiers of uid - for certifier in tuple(cert_list): - # add only valid certification... - try: - cert_expired = await community.certification_expired(certifier['cert_time']) - except NoPeerAvailable: - logging.debug("No peer available") - cert_expired = True - - if not certifier['block_number']: - # add only valid certification... - try: - cert_writable = await community.certification_writable(certifier['cert_time']) - except NoPeerAvailable: - logging.debug("No peer available") - cert_writable = False - else: - cert_writable = True - - if not cert_expired and cert_writable: - # keep only the latest certification - already_found = [c['identity'].pubkey for c in unique_valid] - if certifier['identity'].pubkey in already_found: - index = already_found.index(certifier['identity'].pubkey) - if certifier['cert_time'] > unique_valid[index]['cert_time']: - unique_valid[index] = certifier - else: - unique_valid.append(certifier) - return unique_valid - - async def unique_valid_certifiers_of(self, identities_registry, community): - """ - Get the certifications in the blockchain and in the pools - Get only unique and last certification for each pubkey - :param sakia.core.registry.identities.IdentitiesRegistry identities_registry: The identities registry - :param sakia.core.community.Community community: The community target - :return: The list of the certifiers of this community - :rtype: list - """ - certifier_list = await self.certifiers_of(identities_registry, community) - return await self._unique_valid(certifier_list, community) - - async def unique_valid_certified_by(self, identities_registry, community): - """ - Get the list of persons certified by this person, filtered to get only unique - and valid certifications. - :param sakia.core.registry.IdentitiesRegistry identities_registry: The registry - :param sakia.core.community.Community community: The community target - :return: The list of the certified persons of this community in BMA json format - :rtype: list - """ - certified_list = await self.certified_by(identities_registry, community) - return await self._unique_valid(certified_list, community) - - async def identity_revocation_time(self, community): - """ - Get the remaining time before identity implicit revocation - :param sakia.core.Community community: the community - :return: the remaining time - :rtype: int - """ - membership = await self.membership(community) - join_block = membership['blockNumber'] - block = await community.get_block(join_block) - join_date = block['medianTime'] - parameters = await community.parameters() - # revocation date is join_date + 1 sigvalidity (expiration date) + 2*sigvalidity - revocation_date = join_date + 3*parameters['sigValidity'] - current_time = time.time() - return revocation_date - current_time - - async def membership_expiration_time(self, community): - """ - Get the remaining time before membership expiration - :param sakia.core.Community community: the community - :return: the remaining time - :rtype: int - """ - membership = await self.membership(community) - join_block = membership['blockNumber'] - block = await community.get_block(join_block) - join_date = block['medianTime'] - parameters = await community.parameters() - expiration_date = join_date + parameters['sigValidity'] - current_time = time.time() - return expiration_date - current_time - - async def cert_issuance_delay(self, identities_registry, community): - """ - Get the remaining time before being able to issue new certification. - :param sakia.core.Community community: the community - :return: the remaining time - :rtype: int - """ - certified = await self.certified_by(identities_registry, community) - if len(certified) > 0: - latest_time = max([c['cert_time'] for c in certified if c['cert_time']]) - parameters = await community.parameters() - if parameters and latest_time: - current_time = await community.time() - if current_time - latest_time < parameters['sigPeriod']: - return parameters['sigPeriod'] - (current_time - latest_time) - return 0 - - async def requirements(self, community): - """ - Get the current requirements data. - :param sakia.core.Community community: the community - :return: the requirements - :rtype: dict - """ - try: - requirements = await community.bma_access.future_request(bma.wot.Requirements, - {'search': self.pubkey}) - for req in requirements['identities']: - if req['pubkey'] == self.pubkey and req['uid'] == self.uid and \ - self._sigdate and \ - BlockUID.from_str(req['meta']['timestamp']) == self._sigdate: - return req - except errors.DuniterError as e: - logging.debug(str(e)) - return None - - def _refresh_uid(self, uids): - """ - Refresh UID from uids list, got from a successful lookup request - :param list uids: UIDs got from a lookup request - """ - timestamp = BlockUID.empty() - if self.local_state == LocalState.NOT_FOUND: - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - identity_uid = uid_data["uid"] - self.uid = identity_uid - self.blockchain_state = BlockchainState.BUFFERED - self.local_state = LocalState.PARTIAL - - def jsonify(self): - """ - Get the community as dict in json format. - :return: The community as a dict in json format - """ - data = {'uid': self.uid, - 'pubkey': self.pubkey, - 'sigdate': str(self._sigdate) if self._sigdate else None, - 'local_state': self.local_state.name, - 'blockchain_state': self.blockchain_state.name} - return data - - def __str__(self): - return "{uid} - {pubkey} - {sigdate} - {local} - {blockchain}".format(uid=self.uid, - pubkey=self.pubkey, - sigdate=self._sigdate, - local=self.local_state, - blockchain=self.blockchain_state) diff --git a/src/sakia/core/transfer.py b/src/sakia/core/transfer.py deleted file mode 100644 index b1cd3048ac8d02f013480656ed5c82a2e9285729..0000000000000000000000000000000000000000 --- a/src/sakia/core/transfer.py +++ /dev/null @@ -1,374 +0,0 @@ -""" -Created on 31 janv. 2015 - -@author: inso -""" -import logging -import time -from duniterpy.api import bma -from duniterpy.documents import Block, BlockUID -from PyQt5.QtCore import pyqtSignal, QObject -from enum import Enum - - -class TransferState(Enum): - """ - TO_SEND means the transaction wasn't sent yet - AWAITING means the transaction is waiting to reach K blockchain confrmation - VALIDATED means the transaction was validated locally and is considered present in the blockchain - REFUSED means the transaction took too long to be registered in the blockchain, - therefore it is considered as refused - DROPPED means the transaction was canceled locally. It can still be validated - in the blockchain if it was sent, if the guy is unlucky ;) - """ - TO_SEND = 0 - AWAITING = 1 - VALIDATING = 4 - VALIDATED = 2 - REFUSED = 3 - DROPPED = 5 - - -class Transfer(QObject): - """ - A transfer is the lifecycle of a transaction. - """ - transfer_broadcasted = pyqtSignal(str) - broadcast_error = pyqtSignal(int, str) - - def __init__(self, sha_hash, state, blockUID, metadata, locally_created): - """ - The constructor of a transfer. - Check for metadata keys which must be present : - - receiver - - block - - time - - issuer - - amount - - comment - - :param str sha_hash: The hash of the transaction - :param TransferState state: The state of the Transfer - :param duniterpy.documents.BlockUID blockUID: The blockUID of the transaction in the blockchain - :param dict metadata: The transfer metadata - """ - assert('receiver' in metadata) - assert('time' in metadata) - assert('issuer' in metadata) - assert('amount' in metadata) - assert('comment' in metadata) - assert('issuer_uid' in metadata) - assert('receiver_uid' in metadata) - assert('txid' in metadata) - super().__init__() - - self.sha_hash = sha_hash - self.state = state - self.blockUID = blockUID - self._locally_created = locally_created - self._metadata = metadata - - # Dict containing states of a transfer : - # keys are a tuple containg (current_state, transition_parameters) - # values are tuples containing (transition_test, transition_success, new_state) - self._table_states = { - (TransferState.TO_SEND, (list, Block)): - ( - (self._broadcast_success, lambda l, b: self._wait(b), TransferState.AWAITING), - (lambda l,b: self._broadcast_failure(l), None, TransferState.REFUSED), - ), - (TransferState.TO_SEND, ()): - ((self._is_locally_created, self._drop, TransferState.DROPPED),), - - (TransferState.AWAITING, (bool, Block)): - ((self._found_in_block, lambda r, b: self._be_validating(b), TransferState.VALIDATING),), - (TransferState.AWAITING, (bool, Block, int, int)): - ((self._not_found_in_blockchain, None, TransferState.REFUSED),), - - (TransferState.VALIDATING, (bool, Block, int)): - ((self._reached_enough_confrmation, None, TransferState.VALIDATED),), - (TransferState.VALIDATING, (bool, Block)): - ((self._rollback_and_removed, lambda r, b: self._drop(), TransferState.DROPPED),), - - (TransferState.VALIDATED, (bool, Block, int)): - ((self._rollback_in_fork_window, lambda r, b, i: self._be_validating(b), TransferState.VALIDATING),), - - (TransferState.VALIDATED, (bool, Block)): - ( - (self._rollback_and_removed, lambda r, b: self._drop(), TransferState.DROPPED), - (self._rollback_and_local, lambda r, b: self._wait(b), TransferState.AWAITING), - ), - - (TransferState.REFUSED, ()): - ((self._is_locally_created, self._drop, TransferState.DROPPED),) - } - - @classmethod - def initiate(cls, metadata): - """ - Create a new transfer in a "TO_SEND" state. - :param dict metadata: The computed metadata of the transfer - :return: A new transfer - :rtype: Transfer - """ - return cls(None, TransferState.TO_SEND, None, metadata, True) - - @classmethod - def create_from_blockchain(cls, hash, blockUID, metadata): - """ - Create a new transfer sent from another sakia instance - :param str hash: The transaction hash - :param duniterpy.documents.BlockUID blockUID: The block id were we found the tx - :param dict metadata: The computed metadata of the transaction - :return: A new transfer - :rtype: Transfer - """ - return cls(hash, TransferState.VALIDATING, blockUID, metadata, False) - - @classmethod - def load(cls, data): - """ - Create a new transfer from a dict in json format. - :param dict data: The loaded data - :return: A new transfer - :rtype: Transfer - """ - return cls(data['hash'], - TransferState[data['state']], - BlockUID.from_str(data['blockUID']) if data['blockUID'] else None, - data['metadata'], data['local']) - - def jsonify(self): - """ - :return: The transfer as a dict in json format - """ - return {'hash': self.sha_hash, - 'state': self.state.name, - 'blockUID': str(self.blockUID) if self.blockUID else None, - 'metadata': self._metadata, - 'local': self._locally_created} - - @property - def metadata(self): - """ - :return: this transfer metadata - """ - return self._metadata - - def _not_found_in_blockchain(self, rollback, block, mediantime_target, mediantime_blocks): - """ - Check if the transaction could not be found in the blockchain - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to look for the tx - :param int mediantime_target: The mediantime to mine a block in the community parameters - :param int mediantime_blocks: The number of block used to derive the mediantime - :return: True if the transaction could not be found in a given time - :rtype: bool - """ - if not rollback: - for tx in block.transactions: - if tx.sha_hash == self.sha_hash: - return False - if block.time > self.metadata['time'] + mediantime_target*mediantime_blocks: - return True - return False - - def _found_in_block(self, rollback, block): - """ - Check if the transaction can be found in the blockchain - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to check for the transaction - :return: True if the transaction was found - :rtype: bool - """ - if not rollback: - for tx in block.transactions: - if tx.sha_hash == self.sha_hash: - return True - return False - - def _broadcast_success(self, ret_codes, block): - """ - Check if the retcode is 200 after a POST - :param list ret_codes: The POST return codes of the broadcast - :param duniterpy.documents.Block block: The current block used for transition. - :return: True if the post was successful - :rtype: bool - """ - return 200 in ret_codes - - def _broadcast_failure(self, ret_codes): - """ - Check if no retcode is 200 after a POST - :param list ret_codes: The POST return codes of the broadcast - :return: True if the post was failed - :rtype: bool - """ - return 200 not in ret_codes - - def _reached_enough_confrmation(self, rollback, current_block, fork_window): - """ - Check if the transfer reached enough confrmation in the blockchain - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block current_block: The current block of the main blockchain - :param int fork_window: The number of confrmations needed on the network - :return: True if the transfer reached enough confrmations - :rtype: bool - """ - return not rollback and self.blockUID.number + fork_window <= current_block.number - - def _rollback_and_removed(self, rollback, block): - """ - Check if the transfer is not in the block anymore - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to check for the transaction - :return: True if the transfer is not found in the block - """ - if rollback: - if not block or block.blockUID != self.blockUID: - return True - else: - return self.sha_hash not in [t.sha_hash for t in block.transactions] - return False - - def _rollback_in_fork_window(self, rollback, current_block, fork_window): - """ - Check if the transfer is not in the block anymore - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block current_block: The block to check for the transaction - :return: True if the transfer is found in the block - """ - if rollback: - return self.blockUID.number + fork_window > current_block.number - return False - - def _rollback_and_local(self, rollback, block): - """ - Check if the transfer is not in the block anymore - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to check for the transaction - :return: True if the transfer is found in the block - """ - if rollback and self._locally_created and block.blockUID == self.blockUID: - return self.sha_hash not in [t.sha_hash for t in block.transactions] - return False - - def _is_locally_created(self): - """ - Check if we can send back the transaction if it was locally created - :return: True if the transaction was locally created - """ - return self._locally_created - - def _wait(self, current_block): - """ - Set the transfer as AWAITING confrmation. - :param duniterpy.documents.Block current_block: Current block of the main blockchain - """ - self.blockUID = current_block.blockUID - self._metadata['time'] = int(time.time()) - - def _be_validating(self, block): - """ - Action when the transfer ins found in a block - - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block checked - """ - self.blockUID = block.blockUID - self._metadata['time'] = block.mediantime - - def _drop(self): - """ - Cancel the transfer locally. - The transfer state becomes TransferState.DROPPED. - """ - self.blockUID = None - - def _try_transition(self, transition_key, inputs): - """ - Try the transition defined by the given transition_key - with inputs - :param tuple transition_key: The transition key in the table states - :param tuple inputs: The inputs - :return: True if the transition was applied - :rtype: bool - """ - if len(inputs) == len(transition_key[1]): - for i, input in enumerate(inputs): - if type(input) is not transition_key[1][i]: - return False - for transition in self._table_states[transition_key]: - if transition[0](*inputs): - if self.sha_hash: - logging.debug("{0} : {1} --> {2}".format(self.sha_hash[:5], self.state.name, - transition[2].name)) - else: - logging.debug("Unsent transfer : {0} --> {1}".format(self.state.name, - transition[2].name)) - - # If the transition changes data, apply changes - if transition[1]: - transition[1](*inputs) - self.state = transition[2] - return True - return False - - def run_state_transitions(self, inputs): - """ - Try all current state transitions with inputs - :param tuple inputs: The inputs passed to the transitions - :return: True if the transaction changed state - :rtype: bool - """ - transition_keys = [k for k in self._table_states.keys() if k[0] == self.state] - for key in transition_keys: - if self._try_transition(key, inputs): - return True - return False - - def cancel(self): - """ - Cancel a local transaction - """ - self.run_state_transitions(()) - - async def send(self, txdoc, community): - """ - Send a transaction and update the transfer state to AWAITING if accepted. - If the transaction was refused (return code != 200), state becomes REFUSED - The txdoc is saved as the transfer txdoc. - - :param txdoc: A transaction duniterpy object - :param community: The community target of the transaction - """ - self.sha_hash = txdoc.sha_hash - responses = await community.bma_access.broadcast(bma.tx.Process, - post_args={'transaction': txdoc.signed_raw()}) - blockUID = community.network.current_blockUID - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': blockUID.number}) - signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) - block_doc = Block.from_signed_raw(signed_raw) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.text() - self.run_state_transitions(([r.status for r in responses], block_doc)) - self.run_state_transitions(([r.status for r in responses], )) - return result - - async def get_raw_document(self, community): - """ - Get the raw documents of this transfer - """ - block = await community.get_block(self.blockUID.number) - if block: - block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) - for tx in block_doc.transactions: - if tx.sha_hash == self.sha_hash: - return tx diff --git a/src/sakia/core/txhistory.py b/src/sakia/core/txhistory.py deleted file mode 100644 index 4fd7b9a6c008f7f446026f670fa802602f6ab4bb..0000000000000000000000000000000000000000 --- a/src/sakia/core/txhistory.py +++ /dev/null @@ -1,426 +0,0 @@ -import asyncio -import logging -import hashlib -import math -from duniterpy.documents import SimpleTransaction, Block, MalformedDocumentError -from duniterpy.api import bma, errors -from .transfer import Transfer, TransferState -from .net.network import MAX_CONFIRMATIONS -from ..tools.exceptions import LookupFailureError, NoPeerAvailable - - -class TxHistory: - def __init__(self, app, wallet): - self._latest_block = 0 - self.wallet = wallet - self.app = app - self._stop_coroutines = False - self._running_refresh = [] - self._transfers = [] - self.available_sources = [] - self._dividends = [] - - @property - def latest_block(self): - return self._latest_block - - @latest_block.setter - def latest_block(self, value): - self._latest_block = value - - def load_from_json(self, data, version): - """ - Load the tx history cache from json data - - :param dict data: the data - :param version: the version parsed with pkg_resources.parse_version - :return: - """ - self._transfers = [] - - data_sent = data['transfers'] - for s in data_sent: - self._transfers.append(Transfer.load(s)) - - for s in data['sources']: - self.available_sources.append(s.copy()) - - for d in data['dividends']: - d['state'] = TransferState[d['state']] - self._dividends.append(d) - - self.latest_block = data['latest_block'] - - def jsonify(self): - data_transfer = [] - for s in self.transfers: - data_transfer.append(s.jsonify()) - - data_sources = [] - for s in self.available_sources: - data_sources.append(s) - - data_dividends = [] - for d in self._dividends: - dividend = { - 'block_number': d['block_number'], - "consumed": d['consumed'], - 'time': d['time'], - 'id': d['id'], - 'amount': d['amount'], - 'base': d['base'], - 'state': d['state'].name - } - data_dividends.append(dividend) - - return {'latest_block': self.latest_block, - 'transfers': data_transfer, - 'sources': data_sources, - 'dividends': data_dividends} - - @property - def transfers(self): - return [t for t in self._transfers if t.state != TransferState.DROPPED] - - @property - def dividends(self): - return self._dividends.copy() - - def stop_coroutines(self, closing=False): - self._stop_coroutines = True - - async def _get_block_doc(self, community, number): - """ - Retrieve the current block document - :param sakia.core.Community community: The community we look for a block - :param int number: The block number to retrieve - :return: the block doc or None if no block was found - """ - tries = 0 - block_doc = None - block = None - while block is None and tries < 3: - try: - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': number}) - signed_raw = "{0}{1}\n".format(block['raw'], - block['signature']) - try: - block_doc = Block.from_signed_raw(signed_raw) - except TypeError: - logging.debug("Error in {0}".format(number)) - block = None - tries += 1 - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - block = None - tries += 1 - return block_doc - - async def _parse_transaction(self, community, tx, blockUID, - mediantime, received_list, txid): - """ - Parse a transaction - :param sakia.core.Community community: The community - :param duniterpy.documents.Transaction tx: The tx json data - :param duniterpy.documents.BlockUID blockUID: The block id where we found the tx - :param int mediantime: Median time on the network - :param list received_list: The list of received transactions - :param int txid: The latest txid - :return: the found transaction - """ - receivers = [o.conditions.left.pubkey for o in tx.outputs - if o.conditions.left.pubkey != tx.issuers[0]] - - if len(receivers) == 0: - receivers = [tx.issuers[0]] - - try: - issuer = await self.wallet._identities_registry.future_find(tx.issuers[0], community) - issuer_uid = issuer.uid - except LookupFailureError: - issuer_uid = "" - - try: - receiver = await self.wallet._identities_registry.future_find(receivers[0], community) - receiver_uid = receiver.uid - except LookupFailureError: - receiver_uid = "" - - metadata = { - 'time': mediantime, - 'comment': tx.comment, - 'issuer': tx.issuers[0], - 'issuer_uid': issuer_uid, - 'receiver': receivers[0], - 'receiver_uid': receiver_uid, - 'txid': txid - } - - in_issuers = len([i for i in tx.issuers - if i == self.wallet.pubkey]) > 0 - in_outputs = len([o for o in tx.outputs - if o.conditions.left.pubkey == self.wallet.pubkey]) > 0 - - tx_hash = hashlib.sha256(tx.signed_raw().encode("ascii")).hexdigest().upper() - # If the wallet pubkey is in the issuers we sent this transaction - if in_issuers: - outputs = [o for o in tx.outputs - if o.conditions.left.pubkey != self.wallet.pubkey] - amount = 0 - for o in outputs: - amount += o.amount * math.pow(10, o.base) - metadata['amount'] = amount - transfer = Transfer.create_from_blockchain(tx_hash, - blockUID, - metadata.copy()) - return transfer - # If we are not in the issuers, - # maybe we are in the recipients of this transaction - elif in_outputs: - outputs = [o for o in tx.outputs - if o.conditions.left.pubkey == self.wallet.pubkey] - amount = 0 - for o in outputs: - amount += o.amount * math.pow(10, o.base) - metadata['amount'] = amount - - transfer = Transfer.create_from_blockchain(tx_hash, - blockUID, - metadata.copy()) - received_list.append(transfer) - return transfer - return None - - async def _parse_block(self, community, block_number, received_list, txmax): - """ - Parse a block - :param sakia.core.Community community: The community - :param int block_number: The block to request - :param list received_list: The list where we are appending transactions - :param int txmax: Latest tx id - :return: The list of transfers sent - """ - block_doc = await self._get_block_doc(community, block_number) - transfers = [] - if block_doc: - for transfer in [t for t in self._transfers if t.state == TransferState.AWAITING]: - transfer.run_state_transitions((False, block_doc)) - - new_tx = [t for t in block_doc.transactions - if t.sha_hash not in [trans.sha_hash for trans in self._transfers] - and SimpleTransaction.is_simple(t)] - - for (txid, tx) in enumerate(new_tx): - transfer = await self._parse_transaction(community, tx, block_doc.blockUID, - block_doc.mediantime, received_list, txid+txmax) - if transfer: - #logging.debug("Transfer amount : {0}".format(transfer.metadata['amount'])) - transfers.append(transfer) - else: - pass - #logging.debug("None transfer") - else: - logging.debug("Could not find or parse block {0}".format(block_number)) - return transfers - - async def request_dividends(self, community, parsed_block): - for i in range(0, 6): - try: - dividends_data = await community.bma_access.future_request(bma.ud.History, - req_args={'pubkey': self.wallet.pubkey}) - - dividends = dividends_data['history']['history'].copy() - - for d in dividends: - if d['block_number'] < parsed_block: - dividends.remove(d) - return dividends - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - pass - return {} - - async def _refresh(self, community, block_number_from, block_to, received_list): - """ - Refresh last transactions - - :param sakia.core.Community community: The community - :param list received_list: List of transactions received - """ - new_transfers = [] - new_dividends = [] - try: - logging.debug("Refresh from : {0} to {1}".format(block_number_from, block_to['number'])) - dividends = await self.request_dividends(community, block_number_from) - with_tx_data = await community.bma_access.future_request(bma.blockchain.TX) - blocks_with_tx = with_tx_data['result']['blocks'] - while block_number_from <= block_to['number']: - udid = 0 - for d in [ud for ud in dividends if ud['block_number'] == block_number_from]: - state = TransferState.VALIDATED if block_number_from + MAX_CONFIRMATIONS <= block_to['number'] \ - else TransferState.VALIDATING - - if d['block_number'] not in [ud['block_number'] for ud in self._dividends]: - d['id'] = udid - d['state'] = state - new_dividends.append(d) - - udid += 1 - else: - known_dividend = [ud for ud in self._dividends - if ud['block_number'] == d['block_number']][0] - known_dividend['state'] = state - - # We parse only blocks with transactions - if block_number_from in blocks_with_tx: - transfers = await self._parse_block(community, block_number_from, - received_list, - udid + len(new_transfers)) - new_transfers += transfers - - self.wallet.refresh_progressed.emit(block_number_from, block_to['number'], self.wallet.pubkey) - block_number_from += 1 - - signed_raw = "{0}{1}\n".format(block_to['raw'], - block_to['signature']) - block_to = Block.from_signed_raw(signed_raw) - for transfer in [t for t in self._transfers + new_transfers if t.state == TransferState.VALIDATING]: - transfer.run_state_transitions((False, block_to, MAX_CONFIRMATIONS)) - - # We check if latest parsed block_number is a new high number - if block_number_from > self.latest_block: - self.available_sources = await self.wallet.sources(community) - if self._stop_coroutines: - return - self.latest_block = block_number_from - - parameters = await community.parameters() - for transfer in [t for t in self._transfers if t.state == TransferState.AWAITING]: - transfer.run_state_transitions((False, block_to, - parameters['avgGenTime'], parameters['medianTimeBlocks'])) - except (MalformedDocumentError, NoPeerAvailable) as e: - logging.debug(str(e)) - self.wallet.refresh_finished.emit([]) - return - - self._transfers = self._transfers + new_transfers - self._dividends = self._dividends + new_dividends - - self.wallet.refresh_finished.emit(received_list) - - async def _check_block(self, community, block_number): - """ - Parse a block - :param sakia.core.Community community: The community - :param int block_number: The block to check for transfers - """ - block_doc = await self._get_block_doc(community, block_number) - if block_doc: - # We check the block dividend state - match = [d for d in self._dividends if d['block_number'] == block_number] - if len(match) > 0: - if block_doc.ud: - match[0]['amount'] = block_doc.ud - match[0]['base'] = block_doc.unit_base - else: - self._dividends.remove(match[0]) - - # We check if transactions are still present - for transfer in [t for t in self._transfers - if t.state in (TransferState.VALIDATING, TransferState.VALIDATED) and - t.blockUID.number == block_number]: - if transfer.blockUID.sha_hash == block_doc.blockUID.sha_hash: - return True - transfer.run_state_transitions((True, block_doc)) - else: - logging.debug("Could not get block document") - return False - - async def _rollback(self, community): - """ - Rollback last transactions until we find one still present - in the main blockchain - - :param sakia.core.Community community: The community - """ - try: - logging.debug("Rollback from : {0}".format(self.latest_block)) - # We look for the block goal to check for rollback, - # depending on validating and validated transfers... - tx_blocks = [tx.blockUID.number for tx in self._transfers - if tx.state in (TransferState.VALIDATED, TransferState.VALIDATING) and - tx.blockUID is not None] - ud_blocks = [ud['block_number'] for ud in self._dividends - if ud['state'] in (TransferState.AWAITING, TransferState.VALIDATING)] - blocks = tx_blocks + ud_blocks - blocks.reverse() - for i, block_number in enumerate(blocks): - self.wallet.refresh_progressed.emit(i, len(blocks), self.wallet.pubkey) - if await self._check_block(community, block_number): - break - - current_block = await self._get_block_doc(community, community.network.current_blockUID.number) - if current_block: - members_pubkeys = await community.members_pubkeys() - for transfer in [t for t in self._transfers - if t.state == TransferState.VALIDATED]: - transfer.run_state_transitions((True, current_block, MAX_CONFIRMATIONS)) - except NoPeerAvailable: - logging.debug("No peer available") - - async def refresh(self, community, received_list): - # We update the block goal - try: - current_block_number = community.network.current_blockUID.number - if current_block_number: - current_block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': current_block_number}) - members_pubkeys = await community.members_pubkeys() - # We look for the first block to parse, depending on awaiting and validating transfers and ud... - tx_blocks = [tx.blockUID.number for tx in self._transfers - if tx.state in (TransferState.AWAITING, TransferState.VALIDATING) \ - and tx.blockUID is not None] - ud_blocks = [ud['block_number'] for ud in self._dividends - if ud['state'] in (TransferState.AWAITING, TransferState.VALIDATING)] - blocks = tx_blocks + ud_blocks + \ - [max(0, self.latest_block - MAX_CONFIRMATIONS)] - block_from = min(set(blocks)) - - await self._wait_for_previous_refresh() - if block_from < current_block["number"]: - # Then we start a new one - logging.debug("Starts a new refresh") - task = asyncio.ensure_future(self._refresh(community, block_from, current_block, received_list)) - self._running_refresh.append(task) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - logging.debug("Block not found") - except NoPeerAvailable: - logging.debug("No peer available") - - async def rollback(self, community, received_list): - await self._wait_for_previous_refresh() - # Then we start a new one - logging.debug("Starts a new rollback") - task = asyncio.ensure_future(self._rollback(community)) - self._running_refresh.append(task) - - # Then we start a refresh to check for new transactions - await self.refresh(community, received_list) - - async def _wait_for_previous_refresh(self): - # We wait for current refresh coroutines - if len(self._running_refresh) > 0: - logging.debug("Wait for the end of previous refresh") - done, pending = await asyncio.wait(self._running_refresh) - for cor in done: - try: - self._running_refresh.remove(cor) - except ValueError: - logging.debug("Task already removed.") - for p in pending: - logging.debug("Still waiting for : {0}".format(p)) - logging.debug("Previous refresh finished") - else: - logging.debug("No previous refresh") diff --git a/src/sakia/core/wallet.py b/src/sakia/core/wallet.py deleted file mode 100644 index cc4a1f0f9e2fa2a9fe504a2c93db2f0dec8bb201..0000000000000000000000000000000000000000 --- a/src/sakia/core/wallet.py +++ /dev/null @@ -1,412 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" - -from duniterpy.documents.transaction import InputSource, OutputSource, Unlock, SIGParameter, Transaction, reduce_base -from duniterpy.grammars import output -from duniterpy.key import SigningKey - -from duniterpy.api import bma -from duniterpy.api.bma import PROTOCOL_VERSION -from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable, LookupFailureError -from .transfer import Transfer -from .txhistory import TxHistory -from .. import __version__ - -from pkg_resources import parse_version -from PyQt5.QtCore import QObject, pyqtSignal - -import logging -import asyncio - - -class Wallet(QObject): - """ - A wallet is used to manage money with a unique key. - """ - refresh_progressed = pyqtSignal(int, int, str) - refresh_finished = pyqtSignal(list) - - def __init__(self, walletid, pubkey, name, identities_registry): - """ - Constructor of a wallet object - - :param int walletid: The wallet number, unique between all wallets - :param str pubkey: The wallet pubkey - :param str name: The wallet name - """ - super().__init__() - self.coins = [] - self.walletid = walletid - self.pubkey = pubkey - self.name = name - self._identities_registry = identities_registry - self.caches = {} - - @classmethod - def create(cls, walletid, salt, password, scrypt_params, name, identities_registry): - """ - Factory method to create a new wallet - - :param int walletid: The wallet number, unique between all wallets - :param str salt: The account salt - :param str password: The account password - :param duniterpy.key.ScryptParams scrypt_params: the scrypt params - :param str name: The account name - """ - if walletid == 0: - key = SigningKey(salt, password, scrypt_params) - else: - key = SigningKey(b"{0}{1}".format(salt, walletid), password, scrypt_params) - return cls(walletid, key.pubkey, name, identities_registry) - - @classmethod - def load(cls, json_data, identities_registry): - """ - Factory method to load a saved wallet. - - :param dict json_data: The wallet as a dict in json format - """ - walletid = json_data['walletid'] - pubkey = json_data['pubkey'] - name = json_data['name'] - return cls(walletid, pubkey, name, identities_registry) - - def load_caches(self, app, json_data): - """ - Load this wallet caches. - Each cache correspond to one different community. - - :param dict json_data: The caches as a dict in json format - """ - version = parse_version(json_data['version']) - for currency in json_data: - if currency != 'version': - self.caches[currency] = TxHistory(app, self) - if version >= parse_version("0.20.dev0"): - self.caches[currency].load_from_json(json_data[currency], version) - - def jsonify_caches(self): - """ - Get this wallet caches as json. - - :return: The wallet caches as a dict in json format - """ - data = {} - for currency in self.caches: - data[currency] = self.caches[currency].jsonify() - return data - - def init_cache(self, app, community): - """ - Init the cache of this wallet for the specified community. - - :param community: The community to refresh its cache - """ - if community.currency not in self.caches: - self.caches[community.currency] = TxHistory(app, self) - - def refresh_transactions(self, community, received_list): - """ - Refresh the cache of this wallet for the specified community. - - :param community: The community to refresh its cache - """ - logging.debug("Refresh transactions for {0}".format(self.pubkey)) - asyncio.ensure_future(self.caches[community.currency].refresh(community, received_list)) - - def rollback_transactions(self, community, received_list): - """ - Rollback the transactions of this wallet for the specified community. - - :param community: The community to refresh its cache - """ - logging.debug("Refresh transactions for {0}".format(self.pubkey)) - asyncio.ensure_future(self.caches[community.currency].rollback(community, received_list)) - - async def relative_value(self, community): - """ - Get wallet value relative to last generated UD - - :param community: The community to get value - :return: The wallet relative value - """ - value = await self.value(community) - ud = community.dividend - relative_value = value / float(ud) - return relative_value - - async def value(self, community): - """ - Get wallet absolute value - - :param community: The community to get value - :return: The wallet absolute value - """ - value = 0 - sources = await self.sources(community) - for s in sources: - value += s['amount'] * pow(10, s['base']) - return value - - def tx_sources(self, amount, community): - """ - Get inputs to generate a transaction with a given amount of money - - :param int amount: The amount target value - :param community: The community target of the transaction - - :return: The list of inputs to use in the transaction document - """ - - # such a dirty algorithmm - # everything should be done again from scratch - # in future versions - - def current_value(inputs, overhs): - i = 0 - for s in inputs: - i += s['amount'] * (10**s['base']) - for o in overhs: - i -= o[0] * (10**o[1]) - return i - - amount, amount_base = reduce_base(amount, 0) - cache = self.caches[community.currency] - if cache.available_sources: - current_base = max([src['base'] for src in cache.available_sources]) - value = 0 - sources = [] - outputs = [] - overheads = [] - buf_sources = list(cache.available_sources) - while current_base >= 0: - for s in [src for src in cache.available_sources if src['base'] == current_base]: - test_sources = sources + [s] - val = current_value(test_sources, overheads) - # if we have to compute an overhead - if current_value(test_sources, overheads) > amount * (10**amount_base): - overhead = current_value(test_sources, overheads) - int(amount) * (10**amount_base) - # we round the overhead in the current base - # exemple : 12 in base 1 -> 1*10^1 - overhead = int(round(float(overhead) / (10**current_base))) - source_value = s['amount'] * (10**s['base']) - out = int((source_value - (overhead * (10**current_base)))/(10**current_base)) - if out * (10**current_base) <= amount * (10**amount_base): - sources.append(s) - buf_sources.remove(s) - overheads.append((overhead, current_base)) - outputs.append((out, current_base)) - # else just add the output - else: - sources.append(s) - buf_sources.remove(s) - outputs.append((s['amount'] , s['base'])) - if current_value(sources, overheads) == amount * (10 ** amount_base): - return sources, outputs, overheads, buf_sources - - current_base -= 1 - - raise NotEnoughMoneyError(value, community.currency, - len(sources), amount * pow(10, amount_base)) - - def tx_inputs(self, sources): - """ - Get inputs to generate a transaction with a given amount of money - - :param list sources: The sources used to send the given amount of money - - :return: The list of inputs to use in the transaction document - """ - inputs = [] - for s in sources: - inputs.append(InputSource(s['amount'], s['base'], s['type'], s['identifier'], s['noffset'])) - return inputs - - def tx_unlocks(self, sources): - """ - Get unlocks to generate a transaction with a given amount of money - - :param list sources: The sources used to send the given amount of money - - :return: The list of unlocks to use in the transaction document - """ - unlocks = [] - for i, s in enumerate(sources): - unlocks.append(Unlock(i, [SIGParameter(0)])) - return unlocks - - def tx_outputs(self, pubkey, outputs, overheads): - """ - Get outputs to generate a transaction with a given amount of money - - :param str pubkey: The target pubkey of the transaction - :param list outputs: The amount to send - :param list inputs: The inputs used to send the given amount of money - :param list overheads: The overheads used to send the given amount of money - - :return: The list of outputs to use in the transaction document - """ - total = [] - outputs_bases = set(o[1] for o in outputs) - for base in outputs_bases: - output_sum = 0 - for o in outputs: - if o[1] == base: - output_sum += o[0] - total.append(OutputSource(output_sum, base, output.Condition.token(output.SIG.token(pubkey)))) - - overheads_bases = set(o[1] for o in overheads) - for base in overheads_bases: - overheads_sum = 0 - for o in overheads: - if o[1] == base: - overheads_sum += o[0] - total.append(OutputSource(overheads_sum, base, output.Condition.token(output.SIG.token(self.pubkey)))) - - return total - - def prepare_tx(self, pubkey, blockstamp, amount, message, community): - """ - Prepare a simple Transaction document - :param str pubkey: the target of the transaction - :param duniterpy.documents.BlockUID blockstamp: the blockstamp - :param int amount: the amount sent to the receiver - :param Community community: the target community - :return: the transaction document - :rtype: duniterpy.documents.Transaction - """ - result = self.tx_sources(int(amount), community) - sources = result[0] - computed_outputs = result[1] - overheads = result[2] - self.caches[community.currency].available_sources = result[3][1:] - logging.debug("Inputs : {0}".format(sources)) - - inputs = self.tx_inputs(sources) - unlocks = self.tx_unlocks(sources) - outputs = self.tx_outputs(pubkey, computed_outputs, overheads) - logging.debug("Outputs : {0}".format(outputs)) - tx = Transaction(3, community.currency, blockstamp, 0, - [self.pubkey], inputs, unlocks, - outputs, message, None) - return tx - - async def send_money(self, salt, password, scrypt_params, community, - recipient, amount, message): - """ - Send money to a given recipient in a specified community - - :param str salt: The account salt - :param str password: The account password - :param community: The community target of the transfer - :param str recipient: The pubkey of the recipient - :param int amount: The amount of money to transfer - :param str message: The message to send with the transfer - """ - try: - blockUID = community.network.current_blockUID - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': blockUID.number}) - except ValueError as e: - if '404' in str(e): - return False, "Could not send transfer with null blockchain" - - time = block['medianTime'] - txid = len(block['transactions']) - if self.walletid == 0: - key = SigningKey(salt, password, scrypt_params) - else: - key = SigningKey("{0}{1}".format(salt, self.walletid), password, scrypt_params) - logging.debug("Sender pubkey:{0}".format(key.pubkey)) - - try: - issuer = await self._identities_registry.future_find(key.pubkey, community) - issuer_uid = issuer.uid - except LookupFailureError as e: - issuer_uid = "" - - try: - receiver = await self._identities_registry.future_find(recipient, community) - receiver_uid = receiver.uid - except LookupFailureError as e: - receiver_uid = "" - - metadata = {'block': None, - 'time': time, - 'amount': amount, - 'issuer': key.pubkey, - 'issuer_uid': issuer_uid, - 'receiver': recipient, - 'receiver_uid': receiver_uid, - 'comment': message, - 'txid': txid - } - transfer = Transfer.initiate(metadata) - self.caches[community.currency]._transfers.append(transfer) - try: - tx = self.prepare_tx(recipient, blockUID, amount, message, community) - logging.debug("TX : {0}".format(tx.raw())) - - tx.sign([key]) - logging.debug("Transaction : [{0}]".format(tx.signed_raw())) - return await transfer.send(tx, community) - except NotEnoughMoneyError as e: - return (False, str(e)) - - async def sources(self, community): - """ - Get available sources in a given community - - :param sakia.core.community.Community community: The community where we want available sources - :return: List of bma sources - """ - sources = [] - try: - data = await community.bma_access.future_request(bma.tx.Sources, - req_args={'pubkey': self.pubkey}) - return data['sources'].copy() - except NoPeerAvailable as e: - logging.debug(str(e)) - return sources - - def transfers(self, community): - """ - Get all transfers objects of this wallet - - :param community: The community we want to get the executed transfers - :return: A list of Transfer objects - """ - if community.currency in self.caches: - return self.caches[community.currency].transfers - else: - return [] - - def dividends(self, community): - """ - Get all the dividends received by this wallet - - :param community: The community we want to get received dividends - :return: Result of udhistory request - """ - if community.currency in self.caches: - return self.caches[community.currency].dividends - else: - return [] - - def stop_coroutines(self, closing=False): - for c in self.caches.values(): - c.stop_coroutines(closing) - - def jsonify(self): - """ - Get the wallet as json format. - - :return: The wallet as a dict in json format. - """ - return {'walletid': self.walletid, - 'pubkey': self.pubkey, - 'name': self.name, - 'version': __version__} diff --git a/src/sakia/core/net/api/__init__.py b/src/sakia/data/__init__.py similarity index 100% rename from src/sakia/core/net/api/__init__.py rename to src/sakia/data/__init__.py diff --git a/src/sakia/data/connectors/__init__.py b/src/sakia/data/connectors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..50431678702ffa85b431cf7b1cab28a519252f7d --- /dev/null +++ b/src/sakia/data/connectors/__init__.py @@ -0,0 +1,2 @@ +from .node import NodeConnector +from .bma import BmaConnector diff --git a/src/sakia/data/connectors/bma.py b/src/sakia/data/connectors/bma.py new file mode 100644 index 0000000000000000000000000000000000000000..241b4e742628a654163641491002b6ad57538895 --- /dev/null +++ b/src/sakia/data/connectors/bma.py @@ -0,0 +1,230 @@ +import logging +import aiohttp +from aiohttp.errors import ClientError, ServerDisconnectedError +from duniterpy.api import bma, errors +from duniterpy.documents import BMAEndpoint, SecuredBMAEndpoint +from sakia.errors import NoPeerAvailable +from pkg_resources import parse_version +from socket import gaierror +import asyncio +import random +import jsonschema +import attr +import copy + + +def filter_endpoints(request, nodes): + def compare_versions(node, version): + if node.version and node.version != '': + try: + return parse_version(node.version) >= parse_version(version) + except TypeError: + return False + else: + return True + filters = { + bma.ud.history: lambda n: compare_versions(n, "0.11.0"), + bma.tx.history: lambda n: compare_versions(n, "0.11.0"), + bma.blockchain.membership: lambda n: compare_versions(n, "0.14") + } + if request in filters: + nodes = [n for n in nodes if filters[request](n)] + endpoints = [] + for n in nodes: + endpoints += [e for e in n.endpoints if type(e) in (BMAEndpoint, SecuredBMAEndpoint)] + return endpoints + + +def make_hash(o): + """ + Makes a hash from a dictionary, list, tuple or set to any level, that contains + only other hashable types (including any lists, tuples, sets, and + dictionaries). + """ + + if isinstance(o, (set, tuple, list)): + return tuple([make_hash(e) for e in o]) + elif not isinstance(o, dict): + return hash(o) + + new_o = copy.deepcopy(o) + for k, v in new_o.items(): + new_o[k] = make_hash(v) + + return hash(tuple(frozenset(sorted(new_o.items())))) + + +def _compare_json(first, second): + """ + Compare two json dicts + :param first: the first dictionnary + :param second: the second dictionnary + :return: True if the json dicts are the same + :rtype: bool + """ + + def ordered(obj): + if isinstance(obj, dict): + try: + return sorted((k, ordered(v)) for k, v in obj.items()) + except TypeError: + return obj + if isinstance(obj, list): + try: + return sorted(ordered(x) for x in obj) + except TypeError: + return obj + else: + return obj + + return ordered(first) == ordered(second) + + +def _filter_data(request, data): + filtered = data + if request is bma.wot.lookup: + filtered = copy.deepcopy(data) + filtered.pop("results") + elif request is bma.tx.history: + filtered = copy.deepcopy(data) + filtered["history"].pop("sending") + filtered["history"].pop("receiving") + filtered["history"].pop("pending") + elif request is bma.wot.requirements: + filtered = copy.deepcopy(data) + for idty in filtered["identities"]: + for c in idty["certifications"]: + c.pop("expiresIn") + + return filtered + + +@attr.s() +class BmaConnector: + """ + This class is used to access BMA API. + """ + _nodes_processor = attr.ib() + _user_parameters = attr.ib() + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + async def verified_get(self, currency, request, req_args): + synced_nodes = self._nodes_processor.synced_members_nodes(currency) + if not synced_nodes: + # If no node is known as a member, lookup synced nodes as a fallback + synced_nodes = self._nodes_processor.synced_nodes(currency) + nodes_generator = (n for n in synced_nodes) + answers = {} + answers_data = {} + nb_verification = min(max(1, 0.66 * len(synced_nodes)), 10) + # We try to find agreeing nodes from one 1 to 66% of nodes, max 10 + session = aiohttp.ClientSession() + try: + while max([len(nodes) for nodes in answers.values()] + [0]) <= nb_verification: + futures = [] + + try: + for i in range(0, int(nb_verification)+1): + node = next(nodes_generator) + endpoints = filter_endpoints(request, [node]) + endpoint = random.choice(endpoints) + self._logger.debug( + "Requesting {0} on endpoint {1}".format(str(request.__name__), str(endpoint))) + futures.append(request( + endpoint.conn_handler(session, proxy=self._user_parameters.proxy()), + **req_args)) + except StopIteration: + # When no more node is available, we go out of the while loop + break + finally: + # Everytime we go out of the while loop, we gather the futures + if futures: + responses = await asyncio.gather(*futures, return_exceptions=True) + for r in responses: + if isinstance(r, errors.DuniterError): + data_hash = hash(r.ucode) + elif isinstance(r, BaseException): + self._logger.debug("Exception in responses : " + str(r)) + continue + else: + filtered_data = _filter_data(request, r) + data_hash = make_hash(filtered_data) + answers_data[data_hash] = r + if data_hash not in answers: + answers[data_hash] = [node] + else: + answers[data_hash].append(node) + finally: + session.close() + + for dict_hash in answers: + if len(answers[dict_hash]) >= nb_verification: + if isinstance(answers_data[dict_hash], errors.DuniterError): + raise answers_data[dict_hash] + else: + return answers_data[dict_hash] + + raise NoPeerAvailable("", len(synced_nodes)) + + async def simple_get(self, currency, request, req_args): + endpoints = filter_endpoints(request, self._nodes_processor.synced_nodes(currency)) + tries = 0 + while tries < 3 and endpoints: + endpoint = random.choice(endpoints) + endpoints.remove(endpoint) + try: + self._logger.debug("Requesting {0} on endpoint {1}".format(str(request.__name__), str(endpoint))) + async with aiohttp.ClientSession() as session: + json_data = await request(endpoint.conn_handler(session), **req_args) + return json_data + except (ClientError, ServerDisconnectedError, gaierror, + asyncio.TimeoutError, ValueError, jsonschema.ValidationError) as e: + self._logger.debug(str(e)) + tries += 1 + raise NoPeerAvailable("", len(endpoints)) + + async def get(self, currency, request, req_args={}, verify=True): + """ + :param str currency: the currency requested + :param class request: A bma request class calling for data + :param dict req_args: Arguments to pass to the request constructor + :param bool verify: Verify returned value against multiple nodes + :return: The returned data + """ + if verify: + return await self.verified_get(currency, request, req_args) + else: + return await self.simple_get(currency, request, req_args) + + async def broadcast(self, currency, request, req_args={}): + """ + Broadcast data to a network. + Sends the data to all knew nodes. + + :param str currency: the currency target + :param request: A duniterpy bma request class + :param req_args: Arguments to pass to the request constructor + :return: All nodes replies + :rtype: tuple of aiohttp replies + + .. note:: If one node accept the requests (returns 200), + the broadcast should be considered accepted by the network. + """ + filtered_endpoints = filter_endpoints(request, self._nodes_processor.synced_nodes(currency)) + endpoints = random.sample(filtered_endpoints, 6) if len(filtered_endpoints) > 6 else filtered_endpoints + replies = [] + + if len(endpoints) > 0: + with aiohttp.ClientSession() as session: + for endpoint in endpoints: + self._logger.debug("Trying to connect to : " + str(endpoint)) + reply = asyncio.ensure_future(request(endpoint.conn_handler(session, + proxy=self._user_parameters.proxy()), + **req_args)) + replies.append(reply) + + result = await asyncio.gather(*replies, return_exceptions=True) + return tuple(result) + return () + else: + raise NoPeerAvailable("", len(endpoints)) diff --git a/src/sakia/data/connectors/node.py b/src/sakia/data/connectors/node.py new file mode 100644 index 0000000000000000000000000000000000000000..8e59bc5b9f80a535a2ee7c07dabebc82ba2a5014 --- /dev/null +++ b/src/sakia/data/connectors/node.py @@ -0,0 +1,379 @@ +import asyncio +import logging +from asyncio import TimeoutError +from socket import gaierror + +import aiohttp +import jsonschema +from PyQt5.QtCore import QObject, pyqtSignal +from aiohttp.errors import ClientError, DisconnectedError +from aiohttp.errors import WSServerHandshakeError, ClientResponseError + +from duniterpy.api import bma, errors +from duniterpy.documents import BlockUID, MalformedDocumentError, BMAEndpoint +from duniterpy.documents.peer import Peer, ConnectionHandler +from sakia.decorators import asyncify +from sakia.errors import InvalidNodeCurrency +from ..entities.node import Node + + +class NodeConnector(QObject): + """ + A node is a peer send from the client point of view. + """ + changed = pyqtSignal() + error = pyqtSignal() + identity_changed = pyqtSignal() + neighbour_found = pyqtSignal(Peer) + + def __init__(self, node, user_parameters, session=None): + """ + Constructor + """ + super().__init__() + self.node = node + self._ws_tasks = {'block': None, + 'peer': None} + self._connected = {'block': False, + 'peer': False} + self._user_parameters = user_parameters + self.session = session + self._refresh_counter = 1 + self._logger = logging.getLogger('sakia') + + def __del__(self): + for ws in self._ws_tasks.values(): + if ws: + ws.cancel() + + @classmethod + async def from_address(cls, currency, secured, address, port, user_parameters): + """ + Factory method to get a node from a given address + :param str currency: The node currency. None if we don't know\ + the currency it should have, for example if its the first one we add + :param bool secured: True if the node uses https + :param str address: The node address + :param int port: The node port + :return: A new node + :rtype: sakia.core.net.Node + """ + http_scheme = "https" if secured else "http" + ws_scheme = "ws" if secured else "wss" + session = aiohttp.ClientSession() + peer_data = await bma.network.peering(ConnectionHandler(http_scheme, ws_scheme, address, port, + proxy=user_parameters.proxy(), session=session)) + + peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['raw'], + peer_data['signature'])) + + if currency and peer.currency != currency: + raise InvalidNodeCurrency(peer.currency, currency) + + node = Node(peer.currency, peer.pubkey, peer.endpoints, peer.blockUID) + logging.getLogger('sakia').debug("Node from address : {:}".format(str(node))) + + return cls(node, user_parameters, session=session) + + @classmethod + def from_peer(cls, currency, peer, user_parameters): + """ + Factory method to get a node from a peer document. + :param str currency: The node currency. None if we don't know\ + the currency it should have, for example if its the first one we add + :param peer: The peer document + :return: A new node + :rtype: sakia.core.net.Node + """ + if currency and peer.currency != currency: + raise InvalidNodeCurrency(peer.currency, currency) + + node = Node(peer.currency, peer.pubkey, peer.endpoints, peer.blockUID) + logging.getLogger('sakia').debug("Node from peer : {:}".format(str(node))) + + return cls(node, user_parameters, session=None) + + async def safe_request(self, endpoint, request, proxy, req_args={}): + try: + conn_handler = endpoint.conn_handler(self.session, proxy=proxy) + data = await request(conn_handler, **req_args) + return data + except (ClientError, gaierror, TimeoutError, ConnectionRefusedError, DisconnectedError, ValueError) as e: + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + except jsonschema.ValidationError as e: + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.CORRUPTED + + async def init_session(self): + if not self.session: + self.session = aiohttp.ClientSession() + + async def close_ws(self): + for ws in self._ws_tasks.values(): + if ws: + ws.cancel() + await asyncio.sleep(0) + closed = False + while not closed: + for ws in self._ws_tasks.values(): + if ws: + closed = False + break + else: + closed = True + await asyncio.sleep(0) + await self.session.close() + await asyncio.sleep(0) + + def refresh(self, manual=False): + """ + Refresh all data of this node + :param bool manual: True if the refresh was manually initiated + """ + if not self._ws_tasks['block']: + self._ws_tasks['block'] = asyncio.ensure_future(self.connect_current_block()) + + if not self._ws_tasks['peer']: + self._ws_tasks['peer'] = asyncio.ensure_future(self.connect_peers()) + + if manual: + asyncio.ensure_future(self.request_peers()) + + if self._refresh_counter % 20 == 0 or manual: + self.refresh_summary() + self._refresh_counter = self._refresh_counter if manual else 1 + else: + self._refresh_counter += 1 + + async def connect_current_block(self): + """ + Connects to the websocket entry point of the node + If the connection fails, it tries the fallback mode on HTTP GET + """ + for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: + if not self._connected['block']: + try: + conn_handler = endpoint.conn_handler(self.session, proxy=self._user_parameters.proxy()) + ws_connection = bma.ws.block(conn_handler) + async with ws_connection as ws: + self._connected['block'] = True + self._logger.debug("Connected successfully to block ws : {0}" + .format(self.node.pubkey[:5])) + async for msg in ws: + if msg.tp == aiohttp.MsgType.text: + self._logger.debug("Received a block : {0}".format(self.node.pubkey[:5])) + block_data = bma.parse_text(msg.data, bma.ws.WS_BLOCk_SCHEMA) + await self.refresh_block(block_data) + elif msg.tp == aiohttp.MsgType.closed: + break + elif msg.tp == aiohttp.MsgType.error: + break + except (WSServerHandshakeError, + ClientResponseError, ValueError) as e: + self._logger.debug("Websocket block {0} : {1} - {2}" + .format(type(e).__name__, str(e), self.node.pubkey[:5])) + await self.request_current_block() + except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + except jsonschema.ValidationError as e: + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.CORRUPTED + self.changed.emit() + finally: + self._connected['block'] = False + self._ws_tasks['block'] = None + + async def request_current_block(self): + """ + Request a node on the HTTP GET interface + If an error occurs, the node is considered offline + """ + for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: + try: + block_data = await self.safe_request(endpoint, bma.blockchain.current, + proxy=self._user_parameters.proxy()) + if not block_data: + continue + await self.refresh_block(block_data) + return # Do not try any more endpoint + except errors.DuniterError as e: + if e.ucode == errors.BLOCK_NOT_FOUND: + self.node.previous_buid = BlockUID.empty() + self.changed.emit() + else: + self.node.state = Node.OFFLINE + self.changed.emit() + self._logger.debug("Error in block reply of {0} : {1}}".format(self.node.pubkey[:5], str(e))) + else: + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + + async def refresh_block(self, block_data): + """ + Refresh the blocks of this node + :param dict block_data: The block data in json format + """ + self.node.state = Node.ONLINE + if not self.node.current_buid or self.node.current_buid.sha_hash != block_data['hash']: + for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: + conn_handler = endpoint.conn_handler(self.session, + proxy=self._user_parameters.proxy()) + self._logger.debug("Requesting {0}".format(conn_handler)) + try: + previous_block = await self.safe_request(endpoint, bma.blockchain.block, + proxy=self._user_parameters.proxy(), + req_args={'number': self.node.current_buid.number}) + if not previous_block: + continue + self.node.previous_buid = BlockUID(previous_block['number'], previous_block['hash']) + return # Do not try any more endpoint + except errors.DuniterError as e: + if e.ucode == errors.BLOCK_NOT_FOUND: + self.node.previous_buid = BlockUID.empty() + self.node.current_buid = BlockUID.empty() + else: + self.node.state = Node.OFFLINE + self._logger.debug("Error in previous block reply of {0} : {1}".format(self.node.pubkey[:5], str(e))) + finally: + if self.node.current_buid != BlockUID(block_data['number'], block_data['hash']): + self.node.current_buid = BlockUID(block_data['number'], block_data['hash']) + self.node.current_ts = block_data['medianTime'] + self._logger.debug("Changed block {0} -> {1}".format(self.node.current_buid.number, + block_data['number'])) + self.changed.emit() + else: + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + + @asyncify + async def refresh_summary(self): + """ + Refresh the summary of this node + """ + for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: + try: + summary_data = await self.safe_request(endpoint, bma.node.summary, + proxy=self._user_parameters.proxy()) + if not summary_data: + continue + self.node.software = summary_data["duniter"]["software"] + self.node.version = summary_data["duniter"]["version"] + self.node.state = Node.ONLINE + self.changed.emit() + return # Break endpoints loop + except errors.DuniterError as e: + self.node.state = Node.OFFLINE + self._logger.debug("Error in summary of {0} : {1}".format(self.node.pubkey[:5], str(e))) + self.changed.emit() + else: + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + + async def connect_peers(self): + """ + Connects to the peer websocket entry point + If the connection fails, it tries the fallback mode on HTTP GET + """ + for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: + if not self._connected['peer']: + try: + conn_handler = endpoint.conn_handler(self.session, + proxy=self._user_parameters.proxy()) + ws_connection = bma.ws.peer(conn_handler) + async with ws_connection as ws: + self._connected['peer'] = True + self._logger.debug("Connected successfully to peer ws : {0}".format(self.node.pubkey[:5])) + async for msg in ws: + if msg.tp == aiohttp.MsgType.text: + self._logger.debug("Received a peer : {0}".format(self.node.pubkey[:5])) + peer_data = bma.parse_text(msg.data, bma.ws.WS_PEER_SCHEMA) + self.refresh_peer_data(peer_data) + elif msg.tp == aiohttp.MsgType.closed: + break + elif msg.tp == aiohttp.MsgType.error: + break + except (WSServerHandshakeError, + ClientResponseError, ValueError) as e: + self._logger.debug("Websocket peer {0} : {1} - {2}" + .format(type(e).__name__, str(e), self.node.pubkey[:5])) + await self.request_peers() + except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + except jsonschema.ValidationError as e: + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.CORRUPTED + self.changed.emit() + finally: + self._connected['peer'] = False + self._ws_tasks['peer'] = None + else: + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + + async def request_peers(self): + """ + Refresh the list of peers knew by this node + """ + for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: + try: + peers_data = await self.safe_request(endpoint, bma.network.peers, + req_args={'leaves': 'true'}, + proxy=self._user_parameters.proxy()) + if not peers_data: + continue + self.node.state = Node.ONLINE + if peers_data['root'] != self.node.merkle_peers_root: + leaves = [leaf for leaf in peers_data['leaves'] + if leaf not in self.node.merkle_peers_leaves] + for leaf_hash in leaves: + try: + leaf_data = await self.safe_request(endpoint, + bma.network.peers, + proxy=self._user_parameters.proxy(), + req_args={'leaf': leaf_hash}) + if not leaf_data: + break + self.refresh_peer_data(leaf_data['leaf']['value']) + except (AttributeError, ValueError, errors.DuniterError) as e: + self._logger.debug("{pubkey} : Incorrect peer data in {leaf}" + .format(pubkey=self.node.pubkey[:5], + leaf=leaf_hash)) + self.node.state = Node.OFFLINE + finally: + self.changed.emit() + else: + self.node.merkle_peers_root = peers_data['root'] + self.node.merkle_peers_leaves = tuple(peers_data['leaves']) + return # Break endpoints loop + except errors.DuniterError as e: + self._logger.debug("Error in peers reply : {0}".format(str(e))) + self.node.state = Node.OFFLINE + self.changed.emit() + else: + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + self.changed.emit() + + def refresh_peer_data(self, peer_data): + if "raw" in peer_data: + try: + str_doc = "{0}{1}\n".format(peer_data['raw'], + peer_data['signature']) + peer_doc = Peer.from_signed_raw(str_doc) + self.neighbour_found.emit(peer_doc) + except MalformedDocumentError as e: + self._logger.debug(str(e)) + else: + self._logger.debug("Incorrect leaf reply") diff --git a/src/sakia/data/entities/__init__.py b/src/sakia/data/entities/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7c3eb79e32d6b493c85e1e229ecceefd3a6f013e --- /dev/null +++ b/src/sakia/data/entities/__init__.py @@ -0,0 +1,10 @@ +from .identity import Identity +from .blockchain import Blockchain, BlockchainParameters +from .certification import Certification +from .transaction import Transaction +from .node import Node +from .connection import Connection +from .user_parameters import UserParameters +from .app_data import AppData +from .source import Source +from .dividend import Dividend \ No newline at end of file diff --git a/src/sakia/data/entities/app_data.py b/src/sakia/data/entities/app_data.py new file mode 100644 index 0000000000000000000000000000000000000000..a3ed41ad1bac72fabd2dc19b3813addaac5b6127 --- /dev/null +++ b/src/sakia/data/entities/app_data.py @@ -0,0 +1,7 @@ +import attr + + +@attr.s() +class AppData: + profiles = attr.ib(default=attr.Factory(list)) + default = attr.ib(convert=str, default="Default Profile") diff --git a/src/sakia/data/entities/blockchain.py b/src/sakia/data/entities/blockchain.py new file mode 100644 index 0000000000000000000000000000000000000000..4ff30d4fefb3b5761b582e11cc4ac6f5b0ca033c --- /dev/null +++ b/src/sakia/data/entities/blockchain.py @@ -0,0 +1,74 @@ +import attr +from duniterpy.documents import block_uid, BlockUID + + +@attr.s() +class BlockchainParameters: + # The decimal percent growth of the UD every [dt] period + c = attr.ib(convert=float, default=0, cmp=False, hash=False) + # Time period between two UD in seconds + dt = attr.ib(convert=int, default=0, cmp=False, hash=False) + # UD(0), i.e. initial Universal Dividend + ud0 = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Minimum delay between 2 certifications of a same issuer, in seconds. Must be positive or zero + sig_period = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Maximum quantity of active certifications made by member + sig_stock = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Maximum age of a active signature (in seconds) + sig_validity = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Minimum quantity of signatures to be part of the WoT + sig_qty = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Maximum delay in seconds a certification can wait before being expired for non-writing + sig_window = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Maximum delay in seconds an identity can wait before being expired for non-writing + idty_window = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Maximum delay in seconds a membership can wait before being expired for non-writing + ms_window = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Minimum decimal percent of sentries to reach to match the distance rule + xpercent = attr.ib(convert=float, default=0, cmp=False, hash=False) + # Maximum age of an active membership( in seconds) + ms_validity = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Maximum distance between each WoT member and a newcomer + step_max = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Number of blocks used for calculating median time + median_time_blocks = attr.ib(convert=int, default=0, cmp=False, hash=False) + # The average time for writing 1 block (wished time) in seconds + avg_gen_time = attr.ib(convert=int, default=0, cmp=False, hash=False) + # The number of blocks required to evaluate again PoWMin value + dt_diff_eval = attr.ib(convert=int, default=0, cmp=False, hash=False) + # The decimal percent of previous issuers to reach for personalized difficulty + percent_rot = attr.ib(convert=float, default=0, cmp=False, hash=False) + + +@attr.s() +class Blockchain: + # Parameters in block 0 + parameters = attr.ib(default=BlockchainParameters()) + # block number and hash + current_buid = attr.ib(convert=block_uid, default=BlockUID.empty()) + # Number of members + current_members_count = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Current monetary mass in units + current_mass = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Median time in seconds + median_time = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Last members count + last_members_count = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Last UD amount in units (multiply by 10^base) + last_ud = attr.ib(convert=int, default=1, cmp=False, hash=False) + # Last UD base + last_ud_base = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Last UD base + last_ud_time = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Previous monetary mass in units + previous_mass = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Previous members count + previous_members_count = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Previous UD amount in units (multiply by 10^base) + previous_ud = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Previous UD base + previous_ud_base = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Previous UD base + previous_ud_time = attr.ib(convert=int, default=0, cmp=False, hash=False) + # Currency name + currency = attr.ib(convert=str, default="", cmp=False, hash=False) diff --git a/src/sakia/data/entities/certification.py b/src/sakia/data/entities/certification.py new file mode 100644 index 0000000000000000000000000000000000000000..d59b7ff789e31ccdc2ab773bb8d4237173e613f3 --- /dev/null +++ b/src/sakia/data/entities/certification.py @@ -0,0 +1,13 @@ +import attr +from duniterpy.documents import block_uid, BlockUID + + +@attr.s() +class Certification: + currency = attr.ib(convert=str) + certifier = attr.ib(convert=str) + certified = attr.ib(convert=str) + block = attr.ib(convert=int) + timestamp = attr.ib(convert=int) + signature = attr.ib(convert=str, cmp=False, hash=False) + written_on = attr.ib(convert=int, default=0, cmp=False, hash=False) diff --git a/src/sakia/data/entities/connection.py b/src/sakia/data/entities/connection.py new file mode 100644 index 0000000000000000000000000000000000000000..09b453740b106506ddad0520368884072731aad8 --- /dev/null +++ b/src/sakia/data/entities/connection.py @@ -0,0 +1,28 @@ +import attr +from duniterpy.documents import block_uid, BlockUID +from duniterpy.key import ScryptParams + + +@attr.s() +class Connection: + """ + A connection represents a connection to a currency's network + It is defined by the currency name, and the key informations + used to connect to it. If the user is using an identity, it is defined here too. + """ + currency = attr.ib(convert=str) + pubkey = attr.ib(convert=str) + salt = attr.ib(convert=str) + uid = attr.ib(convert=str, default="", cmp=False, hash=False) + scrypt_N = attr.ib(convert=int, default=4096) + scrypt_r = attr.ib(convert=int, default=16) + scrypt_p = attr.ib(convert=int, default=1) + blockstamp = attr.ib(convert=block_uid, default=BlockUID.empty(), cmp=False, hash=False) + password = attr.ib(init=False, convert=str, default="", cmp=False, hash=False) + + def title(self): + return "@".join([self.uid, self.pubkey[:11]]) + + @property + def scrypt_params(self): + return ScryptParams(self.scrypt_N, self.scrypt_r, self.scrypt_p) diff --git a/src/sakia/data/entities/dividend.py b/src/sakia/data/entities/dividend.py new file mode 100644 index 0000000000000000000000000000000000000000..75c70b4e8721643b7924c05a3298b5c5f04164a8 --- /dev/null +++ b/src/sakia/data/entities/dividend.py @@ -0,0 +1,11 @@ +import attr + + +@attr.s() +class Dividend: + currency = attr.ib(convert=str) + pubkey = attr.ib(convert=str) + block_number = attr.ib(convert=int) + timestamp = attr.ib(convert=int) + amount = attr.ib(convert=int, cmp=False, hash=False) + base = attr.ib(convert=int, cmp=False, hash=False) diff --git a/src/sakia/data/entities/identity.py b/src/sakia/data/entities/identity.py new file mode 100644 index 0000000000000000000000000000000000000000..91a8e06d74ea04737128a038906e6577e8c7cef4 --- /dev/null +++ b/src/sakia/data/entities/identity.py @@ -0,0 +1,31 @@ +import attr +from duniterpy.documents import block_uid, BlockUID +from duniterpy.documents import Identity as IdentityDoc + + +@attr.s() +class Identity: + currency = attr.ib(convert=str) + pubkey = attr.ib(convert=str) + uid = attr.ib(convert=str, default="") + blockstamp = attr.ib(convert=block_uid, default=BlockUID.empty()) + signature = attr.ib(convert=str, default="", cmp=False, hash=False) + # Mediantime of the block referenced by blockstamp + timestamp = attr.ib(convert=int, default=0, cmp=False, hash=False) + written_on = attr.ib(convert=int, default=0, cmp=False, hash=False) + revoked_on = attr.ib(convert=int, default=0, cmp=False, hash=False) + outdistanced = attr.ib(convert=bool, default=True, cmp=False, hash=False) + member = attr.ib(validator=attr.validators.instance_of(bool), default=False, cmp=False, hash=False) + membership_buid = attr.ib(convert=block_uid, default=BlockUID.empty(), cmp=False, hash=False) + membership_timestamp = attr.ib(convert=int, default=0, cmp=False, hash=False) + membership_type = attr.ib(convert=str, default='', validator=lambda s, a, t: t in ('', 'IN', 'OUT'), cmp=False, hash=False) + membership_written_on = attr.ib(convert=int, default=0, cmp=False, hash=False) + + def document(self): + """ + Creates a self cert document for a given identity + :param sakia.data.entities.Identity identity: + :return: the document + :rtype: duniterpy.documents.Identity + """ + return IdentityDoc(10, self.currency, self.pubkey, self.uid, self.blockstamp, self.signature) diff --git a/src/sakia/data/entities/node.py b/src/sakia/data/entities/node.py new file mode 100644 index 0000000000000000000000000000000000000000..e3f9cac2887ecb82fc14b1fe27edf93e499b5061 --- /dev/null +++ b/src/sakia/data/entities/node.py @@ -0,0 +1,81 @@ +import attr +from duniterpy.documents import block_uid, endpoint + + +def _tuple_of_endpoints(value): + if isinstance(value, tuple): + return value + elif isinstance(value, list): + l = [endpoint(e) for e in value] + return tuple(l) + elif isinstance(value, str): + list_of_str = value.split('\n') + conv = [] + for s in list_of_str: + conv.append(endpoint(s)) + return conv + else: + raise TypeError("Can't convert {0} to list of endpoints".format(value)) + + +def _tuple_of_hashes(ls): + if isinstance(ls, tuple): + return ls + elif isinstance(ls, list): + return tuple([str(a) for a in ls]) + elif isinstance(ls, str): + if ls: # if string is not empty + return tuple([str(a) for a in ls.split('\n')]) + else: + return tuple() + + +@attr.s() +class Node: + """ + + A node can have multiple states : + - ONLINE : The node is available for requests + - OFFLINE: The node is disconnected + - DESYNCED : The node is online but is desynced from the network + - CORRUPTED : The node is corrupted, some weird behaviour is going on + """ + MERKLE_EMPTY_ROOT = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + + ONLINE = 1 + OFFLINE = 2 + DESYNCED = 3 + CORRUPTED = 4 + + # The currency handled by this node + currency = attr.ib(convert=str) + # The pubkey of the node + pubkey = attr.ib(convert=str) + # The endpoints of the node, in a list of Endpoint objects format + endpoints = attr.ib(convert=_tuple_of_endpoints, cmp=False) + # The previous block uid in /blockchain/current + peer_blockstamp = attr.ib(convert=block_uid, cmp=False) + # The uid of the owner of node + uid = attr.ib(convert=str, cmp=False, default="") + # The current block uid in /blockchain/current + current_buid = attr.ib(convert=block_uid, cmp=False, default=None) + # The current block time in /blockchain/current + current_ts = attr.ib(convert=int, cmp=False, default=0) + # The previous block uid in /blockchain/current + previous_buid = attr.ib(convert=block_uid, cmp=False, default=None) + # The state of the node in Sakia + state = attr.ib(convert=int, cmp=False, default=OFFLINE) + # The version of the software in /node/summary + software = attr.ib(convert=str, cmp=False, default="") + # The version of the software in /node/summary + version = attr.ib(convert=str, cmp=False, default="") + # Root of the merkle peers tree, default = sha256 of empty string + merkle_peers_root = attr.ib(convert=str, cmp=False, + default=MERKLE_EMPTY_ROOT) + # Leaves of the merkle peers tree + merkle_peers_leaves = attr.ib(convert=_tuple_of_hashes, cmp=False, default=tuple()) + # Define if this node is a root node in Sakia + root = attr.ib(convert=bool, cmp=False, default=False) + # If this node is a member or not + member = attr.ib(convert=bool, cmp=False, default=False) + diff --git a/src/sakia/data/entities/source.py b/src/sakia/data/entities/source.py new file mode 100644 index 0000000000000000000000000000000000000000..96beeb2cf4a37e695d24f4269675ceefc6f030a4 --- /dev/null +++ b/src/sakia/data/entities/source.py @@ -0,0 +1,12 @@ +import attr + + +@attr.s() +class Source: + currency = attr.ib(convert=str) + pubkey = attr.ib(convert=str) + identifier = attr.ib(convert=str) + noffset = attr.ib(convert=int) + type = attr.ib(convert=str, validator=lambda i, a, s: s == 'T' or s == 'D') + amount = attr.ib(convert=int, cmp=False, hash=False) + base = attr.ib(convert=int, cmp=False, hash=False) diff --git a/src/sakia/data/entities/transaction.py b/src/sakia/data/entities/transaction.py new file mode 100644 index 0000000000000000000000000000000000000000..66b5a30b9af5c811e417c00ce6760e333bae02a8 --- /dev/null +++ b/src/sakia/data/entities/transaction.py @@ -0,0 +1,103 @@ +import attr +from duniterpy.documents import block_uid +from duniterpy.documents.transaction import reduce_base +import math + + +def parse_transaction_doc(tx_doc, pubkey, block_number, mediantime, txid): + """ + Parse a transaction + :param duniterpy.documents.Transaction tx_doc: The tx json data + :param str pubkey: The pubkey of the transaction to parse, to know if its a receiver or issuer + :param int block_number: The block number where we found the tx + :param int mediantime: Median time on the network + :param int txid: The latest txid + :return: the found transaction + """ + receivers = [o.conditions.left.pubkey for o in tx_doc.outputs + if o.conditions.left.pubkey != tx_doc.issuers[0]] + + if len(receivers) == 0: + receivers = [tx_doc.issuers[0]] + + in_issuers = len([i for i in tx_doc.issuers + if i == pubkey]) > 0 + in_outputs = len([o for o in tx_doc.outputs + if o.conditions.left.pubkey == pubkey]) > 0 + + if in_issuers or in_outputs: + # If the wallet pubkey is in the issuers we sent this transaction + if in_issuers: + outputs = [o for o in tx_doc.outputs + if o.conditions.left.pubkey != pubkey] + amount = 0 + for o in outputs: + amount += o.amount * math.pow(10, o.base) + # If we are not in the issuers, + # maybe we are in the recipients of this transaction + else: + outputs = [o for o in tx_doc.outputs + if o.conditions.left.pubkey == pubkey] + amount = 0 + for o in outputs: + amount += o.amount * math.pow(10, o.base) + amount, amount_base = reduce_base(amount, 0) + + transaction = Transaction(currency=tx_doc.currency, + sha_hash=tx_doc.sha_hash, + written_block=block_number, + blockstamp=tx_doc.blockstamp, + timestamp=mediantime, + signature=tx_doc.signatures[0], + issuer=tx_doc.issuers[0], + receiver=receivers[0], + amount=amount, + amount_base=amount_base, + comment=tx_doc.comment, + txid=txid, + state=Transaction.VALIDATED, + raw=tx_doc.signed_raw()) + return transaction + return None + + +@attr.s() +class Transaction: + """ + Transaction entity + + :param str currency: the currency of the transaction + :param str sha_hash: the hash of the transaction + :param int written_block: the number of the block + :param str blockstamp: the blockstamp of the transaction + :param int timestamp: the timestamp of the transaction + :param str signature: the signature + :param str issuer: the pubkey of the issuer + :param str receiver: the pubkey of the receiver + :param int amount: the amount + :param int amount_base: the amount base + :param str comment: a comment + :param str txid: the transaction id to sort transctions + :param int state: the state of the transaction + """ + TO_SEND = 0 + AWAITING = 1 + VALIDATED = 4 + REFUSED = 8 + DROPPED = 16 + + currency = attr.ib(convert=str, cmp=False) + sha_hash = attr.ib(convert=str) + written_block = attr.ib(convert=int, cmp=False) + blockstamp = attr.ib(convert=block_uid, cmp=False) + timestamp = attr.ib(convert=int, cmp=False) + signature = attr.ib(convert=str, cmp=False) + issuer = attr.ib(convert=str, cmp=False) + receiver = attr.ib(convert=str, cmp=False) + amount = attr.ib(convert=int, cmp=False) + amount_base = attr.ib(convert=int, cmp=False) + comment = attr.ib(convert=str, cmp=False) + txid = attr.ib(convert=int, cmp=False) + state = attr.ib(convert=int, cmp=False) + local = attr.ib(convert=bool, cmp=False, default=False) + raw = attr.ib(convert=str, cmp=False, default="") diff --git a/src/sakia/data/entities/user_parameters.py b/src/sakia/data/entities/user_parameters.py new file mode 100644 index 0000000000000000000000000000000000000000..b6e79736a2bd7d2d3b307877fc5adbfc70801f78 --- /dev/null +++ b/src/sakia/data/entities/user_parameters.py @@ -0,0 +1,23 @@ +import attr + + +@attr.s() +class UserParameters: + """ + The user parameters entity + """ + profile_name = attr.ib(convert=str, default="Default Profile") + lang = attr.ib(convert=str, default="en_US") + referential = attr.ib(convert=int, default=0) + expert_mode = attr.ib(convert=bool, default=False) + digits_after_comma = attr.ib(convert=int, default=2) + maximized = attr.ib(convert=bool, default=False) + notifications = attr.ib(convert=bool, default=True) + enable_proxy = attr.ib(convert=bool, default=False) + proxy_type = attr.ib(convert=int, default=0) + proxy_address = attr.ib(convert=str, default="") + proxy_port = attr.ib(convert=int, default=8080) + + def proxy(self): + if self.enable_proxy is True: + return "http://{0}:{1}".format(self.proxy_address, self.proxy_port) diff --git a/src/sakia/data/files/__init__.py b/src/sakia/data/files/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cc136098a6d0981bebaaf8a3b1fca6fc76655beb --- /dev/null +++ b/src/sakia/data/files/__init__.py @@ -0,0 +1,2 @@ +from .user_parameters import UserParametersFile +from .app_data import AppDataFile diff --git a/src/sakia/data/files/app_data.py b/src/sakia/data/files/app_data.py new file mode 100644 index 0000000000000000000000000000000000000000..921c8f24787488618833750a93e6972c5747a655 --- /dev/null +++ b/src/sakia/data/files/app_data.py @@ -0,0 +1,39 @@ +import attr +import json +import os +import logging +from ..entities import AppData + + +@attr.s(frozen=True) +class AppDataFile: + """ + The repository for AppData + """ + _file = attr.ib() + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + filename = "appdata.json" + + @classmethod + def in_config_path(cls, config_path): + return cls(os.path.join(config_path, AppDataFile.filename)) + + def save(self, app_data): + """ + Commit a app_data to the database + :param sakia.data.entities.AppData app_data: the app_data to commit + """ + with open(self._file, 'w') as outfile: + json.dump(attr.asdict(app_data), outfile, indent=4) + + def load_or_init(self): + """ + Update an existing app_data in the database + :param sakia.data.entities.AppData app_data: the app_data to update + """ + try: + with open(self._file, 'r') as json_data: + app_data = AppData(**json.load(json_data)) + except OSError: + app_data = AppData() + return app_data diff --git a/src/sakia/data/files/user_parameters.py b/src/sakia/data/files/user_parameters.py new file mode 100644 index 0000000000000000000000000000000000000000..f00116060ce20954552163d7682e7d06c8283cc8 --- /dev/null +++ b/src/sakia/data/files/user_parameters.py @@ -0,0 +1,44 @@ +import attr +import json +import os +import logging +from ..entities import UserParameters + + +@attr.s(frozen=True) +class UserParametersFile: + """ + The repository for UserParameters + """ + _file = attr.ib() + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + filename = "parameters.json" + + @classmethod + def in_config_path(cls, config_path, profile_name): + if not os.path.exists(os.path.join(config_path, profile_name)): + os.makedirs(os.path.join(config_path, profile_name)) + return cls(os.path.join(config_path, profile_name, UserParametersFile.filename)) + + def save(self, user_parameters): + """ + Commit a user_parameters to the database + :param sakia.data.entities.UserParameters user_parameters: the user_parameters to commit + """ + if not os.path.exists(os.path.abspath(os.path.join(self._file, os.pardir))): + os.makedirs(os.path.abspath(os.path.join(self._file, os.pardir))) + with open(self._file, 'w') as outfile: + json.dump(attr.asdict(user_parameters), outfile, indent=4) + return user_parameters + + def load_or_init(self): + """ + Update an existing user_parameters in the database + :param sakia.data.entities.UserParameters user_parameters: the user_parameters to update + """ + try: + with open(self._file, 'r') as json_data: + user_parameters = UserParameters(**json.load(json_data)) + except OSError: + user_parameters = UserParameters() + return user_parameters diff --git a/src/sakia/data/graphs/__init__.py b/src/sakia/data/graphs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ed8f7ac56cb500a3e4a7cc3dd2ecfdd1d2388629 --- /dev/null +++ b/src/sakia/data/graphs/__init__.py @@ -0,0 +1,2 @@ +from .base_graph import BaseGraph +from .wot_graph import WoTGraph \ No newline at end of file diff --git a/src/sakia/data/graphs/base_graph.py b/src/sakia/data/graphs/base_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..0c4436d355687d20fa4c25ad4fd04d9c43214f14 --- /dev/null +++ b/src/sakia/data/graphs/base_graph.py @@ -0,0 +1,242 @@ +import logging +import time +import networkx +from PyQt5.QtCore import QLocale, QDateTime, QObject +from sakia.errors import NoPeerAvailable +from .constants import EdgeStatus, NodeStatus +from sakia.constants import MAX_CONFIRMATIONS + + +class BaseGraph(QObject): + def __init__(self, app, blockchain_service, identities_service, nx_graph): + """ + Init Graph instance + :param sakia.app.Application app: Application instance + :param sakia.services.BlockchainService blockchain_service: Blockchain service instance + :param sakia.services.IdentitiesService identities_service: Identities service instance + :param networkx.Graph nx_graph: The networkx graph + :return: + """ + super().__init__() + self.app = app + self.identities_service = identities_service + self.blockchain_service = blockchain_service + # graph empty if None parameter + self.nx_graph = nx_graph if nx_graph else networkx.DiGraph() + + def arc_status(self, cert_time): + """ + Get arc status of a certification + :param int cert_time: the timestamp of the certification + :return: the certification time + """ + parameters = self.blockchain_service.parameters() + # arc considered strong during 75% of signature validity time + arc_strong = int(parameters.sig_validity * 0.75) + # display validity status + if (time.time() - cert_time) > arc_strong: + return EdgeStatus.WEAK + else: + return EdgeStatus.STRONG + + async def node_status(self, node_identity, account_identity): + """ + Return the status of the node depending + :param sakia.core.registry.Identity node_identity: The identity of the node + :param sakia.core.registry.Identity account_identity: The identity of the account displayed + :return: HIGHLIGHTED if node_identity is account_identity and OUT if the node_identity is not a member + :rtype: sakia.core.graph.constants.NodeStatus + """ + # new node + node_status = NodeStatus.NEUTRAL + node_identity = await self.identities_service.load_requirements(node_identity) + if node_identity.pubkey == account_identity.pubkey: + node_status += NodeStatus.HIGHLIGHTED + if node_identity.member is False: + node_status += NodeStatus.OUT + return node_status + + def offline_node_status(self, node_identity, account_identity): + """ + Return the status of the node depending on its requirements. No network request. + :param sakia.core.registry.Identity node_identity: The identity of the node + :param sakia.core.registry.Identity account_identity: The identity of the account displayed + :return: HIGHLIGHTED if node_identity is account_identity and OUT if the node_identity is not a member + :rtype: sakia.core.graph.constants.NodeStatus + """ + # new node + node_status = NodeStatus.NEUTRAL + if node_identity.pubkey == account_identity.pubkey: + node_status += NodeStatus.HIGHLIGHTED + if node_identity.member is False: + node_status += NodeStatus.OUT + return node_status + + def confirmation_text(self, block_number): + """ + Build confirmation text of an arc + :param int block_number: the block number of the certification + :return: the confirmation text + :rtype: str + """ + try: + current_confirmations = min(max(self.blockchain_service.current_buid().number - block_number, 0), 6) + + if MAX_CONFIRMATIONS > current_confirmations: + if self.app.parameters.expert_mode: + return "{0}/{1}".format(current_confirmations, MAX_CONFIRMATIONS) + else: + confirmation = current_confirmations / MAX_CONFIRMATIONS * 100 + return "{0} %".format(QLocale().toString(float(confirmation), 'f', 0)) + except ValueError: + pass + return None + + def is_sentry(self, nb_certs, nb_members): + """ + Check if it is a sentry or not + :param int nb_certs: the number of certs + :param int nb_members: the number of members + :return: True if a sentry + """ + Y = { + 10: 2, + 100: 4, + 1000: 6, + 10000: 12, + 100000: 20 + } + for k in reversed(sorted(Y.keys())): + if nb_members >= k: + return nb_certs >= Y[k] + return False + + def add_certifier_node(self, certifier, identity, certification, node_status): + metadata = { + 'text': certifier.uid, + 'tooltip': certifier.pubkey, + 'identity': certifier, + 'status': node_status + } + self.nx_graph.add_node(certifier.pubkey, attr_dict=metadata) + + arc_status = self.arc_status(certification.timestamp) + sig_validity = self.blockchain_service.parameters().sig_validity + arc = { + 'status': arc_status, + 'tooltip': QLocale.toString( + QLocale(), + QDateTime.fromTime_t(certification.timestamp + sig_validity).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ), + 'cert_time': certification.timestamp, + 'confirmation_text': self.confirmation_text(certification.block) + } + self.nx_graph.add_edge(certifier.pubkey, identity.pubkey, attr_dict=arc) + + def add_certified_node(self, identity, certified, certification, node_status): + metadata = { + 'text': certified.uid, + 'tooltip': certified.pubkey, + 'identity': certified, + 'status': node_status + } + self.nx_graph.add_node(certified.pubkey, attr_dict=metadata) + + arc_status = self.arc_status(certification.timestamp) + sig_validity = self.blockchain_service.parameters().sig_validity + arc = { + 'status': arc_status, + 'tooltip': QLocale.toString( + QLocale(), + QDateTime.fromTime_t(certification.timestamp + sig_validity).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ), + 'cert_time': certification.timestamp, + 'confirmation_text': self.confirmation_text(certification.block) + } + + self.nx_graph.add_edge(identity.pubkey, certified.pubkey, attr_dict=arc) + + def add_offline_certifier_list(self, certifier_list, identity, account_identity): + """ + Add list of certifiers to graph + :param List[sakia.data.entities.Certification] certifier_list: List of certified from api + :param sakia.data.entities.Identity identity: identity instance which is certified + :param sakia.data.entities.Identity account_identity: Account identity instance + :return: + """ + # add certifiers of uid + for certification in tuple(certifier_list): + certifier = self.identities_service.get_identity(certification.certifier) + node_status = self.offline_node_status(certifier, account_identity) + self.add_certifier_node(certifier, identity, certification, node_status) + + def add_offline_certified_list(self, certified_list, identity, account_identity): + """ + Add list of certified from api to graph + :param List[sakia.data.entities.Certification] certified_list: List of certified from api + :param identity identity: identity instance which is certifier + :param identity account_identity: Account identity instance + :return: + """ + # add certified by uid + for certification in tuple(certified_list): + certified = self.identities_service.get_identity(certification.certified) + node_status = self.offline_node_status(certified, account_identity) + self.add_certified_node(identity, certified, certification, node_status) + + async def add_certifier_list(self, certifier_list, identity, account_identity): + """ + Add list of certifiers to graph + :param List[sakia.data.entities.Certification] certifier_list: List of certified from api + :param sakia.data.entities.Identity identity: identity instance which is certified + :param sakia.data.entities.Identity account_identity: Account identity instance + :return: + """ + try: + # add certifiers of uid + for certification in tuple(certifier_list): + certifier = self.identities_service.get_identity(certification.certifier) + if not certifier: + certifier = await self.identities_service.find_from_pubkey(certification.certifier) + node_status = await self.node_status(certifier, account_identity) + self.add_certifier_node(certifier, identity, certification, node_status) + except NoPeerAvailable as e: + logging.debug(str(e)) + + async def add_certified_list(self, certified_list, identity, account_identity): + """ + Add list of certified from api to graph + :param List[sakia.data.entities.Certification] certified_list: List of certified from api + :param identity identity: identity instance which is certifier + :param identity account_identity: Account identity instance + :return: + """ + try: + # add certified by uid + for certification in tuple(certified_list): + certified = self.identities_service.get_identity(certification.certified) + if not certified: + certified = await self.identities_service.find_from_pubkey(certification.certified) + node_status = await self.node_status(certified, account_identity) + self.add_certified_node(identity, certified, certification, node_status) + + except NoPeerAvailable as e: + logging.debug(str(e)) + + def add_identity(self, identity, status): + """ + Add identity as a new node in graph + :param identity identity: identity instance + :param int status: Optional, default=None, Node status (see sakia.gui.views.wot) + :param list edges: Optional, default=None, List of edges (certified by identity) + :param list connected: Optional, default=None, Public key list of the connected nodes around the identity + """ + metadata = { + 'text': identity.uid, + 'tooltip': identity.pubkey, + 'status': status, + 'identity': identity + } + self.nx_graph.add_node(identity.pubkey, attr_dict=metadata) diff --git a/src/sakia/core/graph/constants.py b/src/sakia/data/graphs/constants.py similarity index 100% rename from src/sakia/core/graph/constants.py rename to src/sakia/data/graphs/constants.py diff --git a/src/sakia/data/graphs/wot_graph.py b/src/sakia/data/graphs/wot_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..d93d0d57fda62de4375aa20e37622c6a6e073f07 --- /dev/null +++ b/src/sakia/data/graphs/wot_graph.py @@ -0,0 +1,51 @@ +import asyncio +import networkx +from .base_graph import BaseGraph + + +class WoTGraph(BaseGraph): + def __init__(self, app, blockchain_service, identities_service, nx_graph=None): + """ + Init WoTGraph instance + :param sakia.app.Application app: the app + :param sakia.data.entities.Connection connection: the connection + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + :param networkx.Graph nx_graph: The networkx graph + :return: + """ + super().__init__(app, blockchain_service, identities_service, nx_graph) + + async def initialize(self, center_identity, connection_identity): + self.nx_graph.clear() + node_status = await self.node_status(center_identity, connection_identity) + + self.add_identity(center_identity, node_status) + + # create Identity from node metadata + certifier_coro = asyncio.ensure_future(self.identities_service.load_certifiers_of(center_identity)) + certified_coro = asyncio.ensure_future(self.identities_service.load_certified_by(center_identity)) + + certifier_list, certified_list = await asyncio.gather(*[certifier_coro, certified_coro]) + + # populate graph with certifiers-of + certifier_coro = asyncio.ensure_future(self.add_certifier_list(certifier_list, + center_identity, connection_identity)) + # populate graph with certified-by + certified_coro = asyncio.ensure_future(self.add_certified_list(certified_list, + center_identity, connection_identity)) + + await asyncio.gather(*[certifier_coro, certified_coro], return_exceptions=True) + await asyncio.sleep(0) + + def offline_init(self, center_identity, connection_identity): + node_status = self.offline_node_status(center_identity, connection_identity) + + self.add_identity(center_identity, node_status) + + # populate graph with certifiers-of + certifier_list = self.identities_service.certifications_received(center_identity.pubkey) + self.add_offline_certifier_list(certifier_list, center_identity, connection_identity) + # populate graph with certified-by + certified_list = self.identities_service.certifications_sent(center_identity.pubkey) + self.add_offline_certified_list(certified_list, center_identity, connection_identity) diff --git a/src/sakia/data/processors/__init__.py b/src/sakia/data/processors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..764f624b174de3e07a52f55d3b6db2fd81594156 --- /dev/null +++ b/src/sakia/data/processors/__init__.py @@ -0,0 +1,9 @@ +from .nodes import NodesProcessor +from .identities import IdentitiesProcessor +from .certifications import CertificationsProcessor +from .blockchain import BlockchainProcessor +from .connections import ConnectionsProcessor +from .sources import SourcesProcessor +from .transactions import TransactionsProcessor +from .dividends import DividendsProcessor + diff --git a/src/sakia/data/processors/blockchain.py b/src/sakia/data/processors/blockchain.py new file mode 100644 index 0000000000000000000000000000000000000000..037a1255235ed2bdf4d996b429ca77a6e7411cef --- /dev/null +++ b/src/sakia/data/processors/blockchain.py @@ -0,0 +1,308 @@ +import attr +import logging +from sakia.errors import NoPeerAvailable +from ..entities import Blockchain, BlockchainParameters +from .nodes import NodesProcessor +from ..connectors import BmaConnector +from duniterpy.api import bma, errors +from duniterpy.documents import Block, BMAEndpoint +import asyncio + + +@attr.s +class BlockchainProcessor: + _repo = attr.ib() # :type sakia.data.repositories.CertificationsRepo + _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + :rtype: sakia.data.processors.BlockchainProcessor + """ + return cls(app.db.blockchains_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters)) + + async def timestamp(self, currency, blockstamp): + try: + block = await self._bma_connector.get(currency, bma.blockchain.block, {'number': blockstamp.number}) + if block: + return block['medianTime'] + except NoPeerAvailable as e: + self._logger.debug(str(e)) + return 0 + + def current_buid(self, currency): + """ + Get the local current blockuid + :rtype: duniterpy.documents.BlockUID + """ + blockchain = self._repo.get_one(currency=currency) + return blockchain.current_buid + + def time(self, currency): + """ + Get the local current median time + :rtype: int + """ + return self._repo.get_one(currency=currency).median_time + + def parameters(self, currency): + """ + Get the parameters of the blockchain + :rtype: sakia.data.entities.BlockchainParameters + """ + return self._repo.get_one(currency=currency).parameters + + def current_mass(self, currency): + """ + Get the local current monetary mass + :rtype: int + """ + return self._repo.get_one(currency=currency).current_mass + + def current_members_count(self, currency): + """ + Get the number of members in the blockchain + :rtype: int + """ + return self._repo.get_one(currency=currency).current_members_count + + def last_members_count(self, currency): + """ + Get the last ud value and base + :rtype: int, int + """ + return self._repo.get_one(currency=currency).last_members_count + + def last_ud(self, currency): + """ + Get the last ud value and base + :rtype: int, int + """ + blockchain = self._repo.get_one(currency=currency) + try: + return blockchain.last_ud, blockchain.last_ud_base + except AttributeError: + pass + + def last_ud_time(self, currency): + """ + Get the last ud time + :rtype: int + """ + blockchain = self._repo.get_one(currency=currency) + return blockchain.last_ud_time + + def previous_monetary_mass(self, currency): + """ + Get the local current monetary mass + :rtype: int + """ + return self._repo.get_one(currency=currency).previous_mass + + def previous_members_count(self, currency): + """ + Get the local current monetary mass + :rtype: int + """ + return self._repo.get_one(currency=currency).previous_members_count + + def previous_ud(self, currency): + """ + Get the previous ud value and base + :rtype: int, int + """ + blockchain = self._repo.get_one(currency=currency) + return blockchain.previous_ud, blockchain.previous_ud_base + + def previous_ud_time(self, currency): + """ + Get the previous ud time + :rtype: int + """ + blockchain = self._repo.get_one(currency=currency) + return blockchain.previous_ud_time + + async def get_block(self, currency, number): + """ + Get block documen at a given number + :param str currency: + :param int number: + :rtype: duniterpy.documents.Block + """ + block = await self._bma_connector.get(currency, bma.blockchain.block, req_args={'number': number}) + if block: + block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) + return block_doc + + + async def new_blocks_with_identities(self, currency): + """ + Get blocks more recent than local blockuid + with identities + """ + with_identities = [] + future_requests = [] + for req in (bma.blockchain.joiners, + bma.blockchain.leavers, + bma.blockchain.actives, + bma.blockchain.excluded, + bma.blockchain.newcomers): + future_requests.append(self._bma_connector.get(currency, req)) + results = await asyncio.gather(*future_requests) + + for res in results: + with_identities += res["result"]["blocks"] + + local_current_buid = self.current_buid(currency) + return sorted([b for b in with_identities if b > local_current_buid.number]) + + async def new_blocks_with_money(self, currency): + """ + Get blocks more recent than local block uid + with money data (tx and uds) + """ + with_money = [] + future_requests = [] + for req in (bma.blockchain.ud, bma.blockchain.tx): + future_requests.append(self._bma_connector.get(currency, req)) + results = await asyncio.gather(*future_requests) + + for res in results: + with_money += res["result"]["blocks"] + + local_current_buid = self.current_buid(currency) + return sorted([b for b in with_money if b > local_current_buid.number]) + + async def blocks(self, numbers, currency): + """ + Get blocks from the network + :param List[int] numbers: list of blocks numbers to get + :return: the list of block documents + :rtype: List[duniterpy.documents.Block] + """ + if numbers: + from_block = min(numbers) + to_block = max(numbers) + count = to_block - from_block + + blocks_data = await self._bma_connector.get(currency, bma.blockchain.blocks, req_args={'count': count, + 'start': from_block}) + blocks = [] + for data in blocks_data: + if data['number'] in numbers: + blocks.append(Block.from_signed_raw(data["raw"] + data["signature"] + "\n")) + + return blocks + return [] + + async def initialize_blockchain(self, currency, log_stream): + """ + Initialize blockchain for a given currency if no source exists locally + """ + blockchain = self._repo.get_one(currency=currency) + if not blockchain: + blockchain = Blockchain(currency=currency) + log_stream("Requesting blockchain parameters") + try: + parameters = await self._bma_connector.get(currency, bma.blockchain.parameters, verify=False) + blockchain.parameters.ms_validity = parameters['msValidity'] + blockchain.parameters.avg_gen_time = parameters['avgGenTime'] + blockchain.parameters.c = parameters['c'] + blockchain.parameters.dt = parameters['dt'] + blockchain.parameters.dt_diff_eval = parameters['dtDiffEval'] + blockchain.parameters.median_time_blocks = parameters['medianTimeBlocks'] + blockchain.parameters.percent_rot = parameters['percentRot'] + blockchain.parameters.idty_window = parameters['idtyWindow'] + blockchain.parameters.ms_window = parameters['msWindow'] + blockchain.parameters.sig_window = parameters['sigWindow'] + blockchain.parameters.sig_period = parameters['sigPeriod'] + blockchain.parameters.sig_qty = parameters['sigQty'] + blockchain.parameters.sig_stock = parameters['sigStock'] + blockchain.parameters.sig_validity = parameters['sigValidity'] + blockchain.parameters.sig_qty = parameters['sigQty'] + blockchain.parameters.sig_period = parameters['sigPeriod'] + blockchain.parameters.ud0 = parameters['ud0'] + blockchain.parameters.xpercent = parameters['xpercent'] + except errors.DuniterError as e: + raise + + log_stream("Requesting current block") + try: + current_block = await self._bma_connector.get(currency, bma.blockchain.current, verify=False) + signed_raw = "{0}{1}\n".format(current_block['raw'], current_block['signature']) + block = Block.from_signed_raw(signed_raw) + blockchain.current_buid = block.blockUID + blockchain.median_time = block.mediantime + blockchain.current_members_count = block.members_count + except errors.DuniterError as e: + if e.ucode != errors.NO_CURRENT_BLOCK: + raise + + log_stream("Requesting blocks with dividend") + with_ud = await self._bma_connector.get(currency, bma.blockchain.ud, verify=False) + blocks_with_ud = with_ud['result']['blocks'] + + if len(blocks_with_ud) > 0: + log_stream("Requesting last block with dividend") + try: + index = max(len(blocks_with_ud) - 1, 0) + block_number = blocks_with_ud[index] + block_with_ud = await self._bma_connector.get(currency, bma.blockchain.block, + req_args={'number': block_number}, verify=False) + if block_with_ud: + blockchain.last_members_count = block_with_ud['membersCount'] + blockchain.last_ud = block_with_ud['dividend'] + blockchain.last_ud_base = block_with_ud['unitbase'] + blockchain.last_ud_time = block_with_ud['medianTime'] + blockchain.current_mass = block_with_ud['monetaryMass'] + except errors.DuniterError as e: + if e.ucode != errors.NO_CURRENT_BLOCK: + raise + + log_stream("Requesting previous block with dividend") + try: + index = max(len(blocks_with_ud) - 2, 0) + block_number = blocks_with_ud[index] + block_with_ud = await self._bma_connector.get(currency, bma.blockchain.block, + req_args={'number': block_number}, verify=False) + blockchain.previous_mass = block_with_ud['monetaryMass'] + blockchain.previous_members_count = block_with_ud['membersCount'] + blockchain.previous_ud = block_with_ud['dividend'] + blockchain.previous_ud_base = block_with_ud['unitbase'] + blockchain.previous_ud_time = block_with_ud['medianTime'] + except errors.DuniterError as e: + if e.ucode != errors.NO_CURRENT_BLOCK: + raise + + self._repo.insert(blockchain) + + def handle_new_blocks(self, currency, blocks): + """ + Initialize blockchain for a given currency if no source exists locally + :param List[duniterpy.documents.Block] blocks + """ + blockchain = self._repo.get_one(currency=currency) + for block in sorted(blocks): + blockchain.current_buid = block.blockUID + blockchain.median_time = block.mediantime + blockchain.current_members_count = block.members_count + if block.ud: + blockchain.previous_mass = blockchain.current_mass + blockchain.previous_members_count = blockchain.last_members_count + blockchain.previous_ud = blockchain.last_ud + blockchain.previous_ud_base = blockchain.last_ud_base + blockchain.previous_ud_time = blockchain.last_ud_time + blockchain.current_mass = blockchain.current_mass + block.ud * block.members_count + blockchain.last_members_count = block.members_count + blockchain.last_ud = block.ud + blockchain.last_ud_base = block.unit_base + blockchain.last_ud_time = block.mediantime + self._repo.update(blockchain) + + def remove_blockchain(self, currency): + self._repo.drop(self._repo.get_one(currency=currency)) + diff --git a/src/sakia/data/processors/certifications.py b/src/sakia/data/processors/certifications.py new file mode 100644 index 0000000000000000000000000000000000000000..e9fc98773973e6d41115e6bfe884fc8764ff95ca --- /dev/null +++ b/src/sakia/data/processors/certifications.py @@ -0,0 +1,186 @@ +import attr +import asyncio +from duniterpy.api import bma, errors +from duniterpy.documents import block_uid +from ..connectors import BmaConnector +from ..processors import NodesProcessor +from ..entities import Certification, Identity +import sqlite3 +import logging +from sakia.errors import NoPeerAvailable + + +@attr.s +class CertificationsProcessor: + _certifications_repo = attr.ib() # :type sakia.data.repositories.CertificationsRepo + _identities_repo = attr.ib() # :type sakia.data.repositories.IdentitiesRepo + _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.certifications_repo, app.db.identities_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters)) + + def certifications_sent(self, currency, pubkey): + """ + Get the list of certifications sent for a given pubkey + :param str currency: + :param str pubkey: + :rtype: List[sakia.data.entities.Certification] + """ + return self._certifications_repo.get_all(currency=currency, certifier=pubkey) + + def certifications_received(self, currency, pubkey): + """ + Get the list of certifications sent for a given pubkey + :param str currency: + :param str pubkey: + :rtype: List[sakia.data.entities.Certification] + """ + return self._certifications_repo.get_all(currency=currency, certified=pubkey) + + def cert_issuance_delay(self, currency, pubkey, parameters, blockchain_time): + """ + Get the remaining time before being able to issue new certification. + :param str currency: the currency of the certifications + :param str pubkey: the pubkey of the certifications + :param sakia.data.entities.BlockchainParameters parameters: the parameters of the blockchain + :param int blockchain_time: the current time of the blockchain + :return: the remaining time + :rtype: int + """ + certified = self._certifications_repo.get_latest_sent(currency=currency, pubkey=pubkey) + if certified and blockchain_time - certified.timestamp < parameters.sig_period: + return parameters.sig_period - (blockchain_time - certified.timestamp) + return 0 + + def create_certification(self, currency, cert, blockstamp): + """ + Creates a certification and insert it in the db + :param duniterpy.documents.Certification cert: + :param duniterpy.documents.BlockUID blockstamp: + :return: the instanciated certification + :rtype: sakia.data.entities.Certification + """ + cert = Certification(currency, cert.pubkey_from, cert.pubkey_to, cert.timestamp.number, + 0, cert.signatures[0], blockstamp) + self._certifications_repo.insert(cert) + return cert + + def insert_or_update_certification(self, cert): + """ + Commits a certification to the DB + :param sakia.data.entities.Certification cert: + :return: + """ + try: + self._certifications_repo.insert(cert) + except sqlite3.IntegrityError: + self._certifications_repo.update(cert) + + async def initialize_certifications(self, identity, log_stream): + """ + Initialize certifications to and from a given identity + :param sakia.data.entities.Identity identity: + :param function log_stream: + """ + log_stream("Requesting certifiers of data") + identities = list() + certifiers = list() + try: + data = await self._bma_connector.get(identity.currency, bma.wot.certifiers_of, + req_args={'search': identity.pubkey}, verify=False) + + for certifier_data in data['certifications']: + certification = Certification(currency=identity.currency, + certified=identity.pubkey, + certifier=certifier_data['pubkey'], + block=certifier_data['cert_time']['block'], + timestamp=certifier_data['cert_time']['medianTime'], + signature=certifier_data['signature']) + other_identity = Identity(currency=identity.currency, + pubkey=certifier_data['pubkey'], + uid=certifier_data['uid'], + blockstamp=certifier_data['sigDate'], + member=certifier_data['isMember']) + if certifier_data['written']: + certification.written_on = certifier_data['written']['number'] + + certifiers.append(certification) + identities.append(other_identity) + except errors.DuniterError as e: + if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): + logging.debug("Certifiers of error : {0}".format(str(e))) + else: + raise + + log_stream("Requesting certified by data") + certified = list() + try: + data = await self._bma_connector.get(identity.currency, bma.wot.certified_by, + req_args={'search': identity.pubkey}, verify=False) + for certified_data in data['certifications']: + certification = Certification(currency=identity.currency, + certifier=identity.pubkey, + certified=certified_data['pubkey'], + block=certified_data['cert_time']['block'], + timestamp=certified_data['cert_time']['medianTime'], + signature=certified_data['signature']) + other_identity = Identity(currency=identity.currency, + pubkey=certified_data['pubkey'], + uid=certified_data['uid'], + blockstamp=certified_data['sigDate'], + member=certified_data['isMember']) + if certified_data['written']: + certification.written_on = certified_data['written']['number'] + + certified.append(certification) + identities.append(other_identity) + except errors.DuniterError as e: + if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): + logging.debug("Certified by error : {0}".format(str(e))) + else: + raise + + log_stream('Commiting certifications...') + for i, cert in enumerate(certifiers + certified): + log_stream('Certification {0}/{1}'.format(i, len(certifiers + certified))) + self.insert_or_update_certification(cert) + await asyncio.sleep(0) + + log_stream('Commiting identities...') + for i, idty in enumerate(identities): + log_stream('Identity {0}/{1}'.format(i, len(identities))) + try: + self._identities_repo.insert(idty) + except sqlite3.IntegrityError: + self._identities_repo.update(idty) + await asyncio.sleep(0) + + def cleanup_connection(self, connection, connections_pubkeys): + """ + Cleanup connections data after removal + :param sakia.data.entities.Connection connection: removed connection + :param List[str] connections_pubkeys: pubkeys of existing connections + :return: + """ + certifiers = self._certifications_repo.get_all(currency=connection.currency, certifier=connection.pubkey) + for c in certifiers: + self._certifications_repo.drop(c) + if c.certified not in connections_pubkeys: + idty = self._identities_repo.get_one(currency=connection.currency, pubkey=c.certified) + if idty: + self._identities_repo.drop(idty) + + certified = self._certifications_repo.get_all(currency=connection.currency, certified=connection.pubkey) + for c in certified: + self._certifications_repo.drop(c) + if c.certifier not in connections_pubkeys: + idty = self._identities_repo.get_one(currency=connection.currency, pubkey=c.certifier) + if idty: + self._identities_repo.drop(idty) \ No newline at end of file diff --git a/src/sakia/data/processors/connections.py b/src/sakia/data/processors/connections.py new file mode 100644 index 0000000000000000000000000000000000000000..879cc69c6d966f311a0ef1608fd33385841dcce4 --- /dev/null +++ b/src/sakia/data/processors/connections.py @@ -0,0 +1,42 @@ +import attr +import sqlite3 +import logging + + +@attr.s +class ConnectionsProcessor: + _connections_repo = attr.ib() # :type sakia.data.repositories.ConnectionsRepo + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.connections_repo) + + def commit_connection(self, connection): + """ + Saves a connection state in the db + :param sakia.data.entities.Connection connection: the connection updated + """ + try: + self._connections_repo.insert(connection) + except sqlite3.IntegrityError: + self._connections_repo.update(connection) + + def remove_connections(self, connection): + self._connections_repo.drop(connection) + + def pubkeys(self): + return self._connections_repo.get_pubkeys() + + def connections(self): + return self._connections_repo.get_all() + + def connections_to(self, currency): + return self._connections_repo.get_all(currency=currency) + + def currencies(self): + return self._connections_repo.get_currencies() diff --git a/src/sakia/data/processors/dividends.py b/src/sakia/data/processors/dividends.py new file mode 100644 index 0000000000000000000000000000000000000000..5c10168face779efd62ff8a91e905a3a0a092616 --- /dev/null +++ b/src/sakia/data/processors/dividends.py @@ -0,0 +1,96 @@ +import attr +import logging +from ..entities import Dividend +from .nodes import NodesProcessor +from ..connectors import BmaConnector +from duniterpy.api import bma +from duniterpy.documents import Transaction +import sqlite3 +import asyncio + + +@attr.s +class DividendsProcessor: + """ + :param sakia.data.repositories.DividendsRepo _repo: the repository of the sources + :param sakia.data.connectors.bma.BmaConnector _bma_connector: the bma connector + """ + _repo = attr.ib() + _bma_connector = attr.ib() + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.dividends_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters)) + + def commit(self, dividend): + try: + self._repo.insert(dividend) + return True + except sqlite3.IntegrityError: + self._logger.debug("Dividend already in db") + return False + + async def initialize_dividends(self, connection, transactions, log_stream): + """ + Request transactions from the network to initialize data for a given pubkey + :param sakia.data.entities.Connection connection: + :param List[sakia.data.entities.Transaction] transactions: the list of transactions found by tx processor + :param function log_stream: + """ + history_data = await self._bma_connector.get(connection.currency, bma.ud.history, + req_args={'pubkey': connection.pubkey}, verify=False) + log_stream("Found {0} available dividends".format(len(history_data["history"]["history"]))) + block_numbers = [] + for ud_data in history_data["history"]["history"]: + dividend = Dividend(currency=connection.currency, + pubkey=connection.pubkey, + block_number=ud_data["block_number"], + timestamp=ud_data["time"], + amount=ud_data["amount"], + base=ud_data["base"]) + log_stream("Dividend of block {0}".format(dividend.block_number)) + block_numbers.append(dividend.block_number) + try: + self._repo.insert(dividend) + except sqlite3.IntegrityError: + log_stream("Dividend already registered in database") + + for tx in transactions: + txdoc = Transaction.from_signed_raw(tx.raw) + for input in txdoc.inputs: + if input.source == "D" and input.origin_id == connection.pubkey and input.index not in block_numbers: + block = await self._bma_connector.get(connection.currency, + bma.blockchain.block, req_args={'number': input.index}, + verify=False) + await asyncio.sleep(0.5) + + dividend = Dividend(currency=connection.currency, + pubkey=connection.pubkey, + block_number=input.index, + timestamp=block["medianTime"], + amount=block["dividend"], + base=block["unitbase"]) + log_stream("Dividend of block {0}".format(dividend.block_number)) + try: + self._repo.insert(dividend) + except sqlite3.IntegrityError: + log_stream("Dividend already registered in database") + + def dividends(self, currency, pubkey): + return self._repo.get_all(currency=currency, pubkey=pubkey) + + def cleanup_connection(self, connection): + """ + Cleanup connection after removal + :param sakia.data.entities.Connection connection: + :return: + """ + dividends = self._repo.get_all(currency=connection.currency, pubkey=connection.pubkey) + for d in dividends: + self._repo.drop(d) diff --git a/src/sakia/data/processors/identities.py b/src/sakia/data/processors/identities.py new file mode 100644 index 0000000000000000000000000000000000000000..a9099ddf507da65bc29756148b5142ee1f5e437e --- /dev/null +++ b/src/sakia/data/processors/identities.py @@ -0,0 +1,177 @@ +import attr +import sqlite3 +import logging +import asyncio +from ..entities import Identity +from ..connectors import BmaConnector +from ..processors import NodesProcessor +from duniterpy.api import bma, errors +from duniterpy.key import SigningKey +from duniterpy.documents import BlockUID, block_uid +from duniterpy.documents import Identity as IdentityDoc +from aiohttp.errors import ClientError +from sakia.errors import NoPeerAvailable + + +@attr.s +class IdentitiesProcessor: + _identities_repo = attr.ib() # :type sakia.data.repositories.IdentitiesRepo + _blockchain_repo = attr.ib() # :type sakia.data.repositories.BlockchainRepo + _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.identities_repo, app.db.blockchains_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters)) + + async def find_from_pubkey(self, currency, pubkey): + """ + Get the most recent identity corresponding to a pubkey + from the network and the local db + :param currency: + :param pubkey: + :rtype: sakia.data.entities.Identity + """ + found_identity = Identity(currency=currency, pubkey=pubkey) + identities = self._identities_repo.get_all(currency=currency, pubkey=pubkey) + for idty in identities: + if idty.blockstamp > found_identity.blockstamp: + found_identity = idty + if not found_identity: + tries = 0 + while tries < 3: + try: + data = await self._bma_connector.get(currency, bma.wot.lookup, req_args={'search': pubkey}) + found_identity = None + for result in data['results']: + if result["pubkey"] == pubkey: + uids = result['uids'] + for uid_data in uids: + identity = Identity(currency, pubkey) + identity.uid = uid_data['uid'] + identity.blockstamp = block_uid(uid_data['meta']['timestamp']) + identity.signature = uid_data['self'] + if identity.blockstamp > found_identity.blockstamp: + found_identity = identity + except (errors.DuniterError, asyncio.TimeoutError, ClientError) as e: + tries += 1 + self._logger.debug(str(e)) + except NoPeerAvailable: + self._logger.debug(str(e)) + return found_identity + + async def lookup(self, currency, text): + """ + Get the list of identities corresponding to a pubkey + from the network and the local db + :param str currency: + :param str text: the text to lookup + :rtype: list[sakia.data.entities.Identity] + """ + identities = self._identities_repo.find_all(currency=currency, text=text) + tries = 0 + while tries < 3: + try: + data = await self._bma_connector.get(currency, bma.wot.lookup, req_args={'search': text}) + for result in data['results']: + pubkey = result['pubkey'] + for uid_data in result['uids']: + if not uid_data['revoked']: + identity = Identity(currency=currency, + pubkey=pubkey, + uid=uid_data['uid'], + blockstamp=uid_data['meta']['timestamp'], + signature=uid_data['self']) + if identity not in identities: + identities.append(identity) + break + except (errors.DuniterError, asyncio.TimeoutError, ClientError) as e: + tries += 1 + self._logger.debug(str(e)) + return identities + + def get_identity(self, currency, pubkey, uid=""): + """ + Return the identity corresponding to a given pubkey, uid and currency + If no UID is given, o + :param str currency: + :param str pubkey: + :param str uid: optionally, specify an uid to lookup + + :rtype: sakia.data.entities.Identity + """ + identities = self._identities_repo.get_all(currency=currency, pubkey=pubkey) + if identities: + recent = identities[0] + for i in identities: + if i.blockstamp > recent.blockstamp: + if uid and i.uid == uid: + recent = i + elif not uid: + recent = i + return recent + + def insert_or_update_identity(self, identity): + """ + Saves an identity state in the db + :param sakia.data.entities.Identity identity: the identity updated + """ + try: + self._identities_repo.insert(identity) + except sqlite3.IntegrityError: + self._identities_repo.update(identity) + + async def initialize_identity(self, identity, log_stream): + """ + Initialize memberships and other data for given identity + :param sakia.data.entities.Identity identity: + :param function log_stream: + """ + log_stream("Requesting membership data") + try: + memberships_data = await self._bma_connector.get(identity.currency, bma.blockchain.memberships, + req_args={'search': identity.pubkey}, verify=False) + if block_uid(memberships_data['sigDate']) == identity.blockstamp \ + and memberships_data['uid'] == identity.uid: + for ms in memberships_data['memberships']: + if ms['written'] > identity.membership_written_on: + identity.membership_buid = BlockUID(ms['blockNumber'], ms['blockHash']) + identity.membership_type = ms['membership'] + + if identity.membership_buid: + log_stream("Requesting membership timestamp") + ms_block_data = await self._bma_connector.get(identity.currency, bma.blockchain.block, + req_args={'number': identity.membership_buid.number}, + verify=False) + if ms_block_data: + identity.membership_timestamp = ms_block_data['medianTime'] + + log_stream("Requesting identity requirements status") + + requirements_data = await self._bma_connector.get(identity.currency, bma.wot.requirements, + req_args={'search': identity.pubkey}, verify=False) + identity_data = next((data for data in requirements_data["identities"] + if data["pubkey"] == identity.pubkey)) + identity.member = identity_data['membershipExpiresIn'] > 0 and not identity_data['outdistanced'] + identity.outdistanced = identity_data['outdistanced'] + self.insert_or_update_identity(identity) + except errors.DuniterError as e: + if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: + pass + else: + raise + + def cleanup_connection(self, connection): + """ + Cleanup after connection removal + :param sakia.data.entities.Connectionb connection: + :return: + """ + identities = self._identities_repo.get_all(currency=connection.currency, pubkey=connection.pubkey) + for idty in identities: + self._identities_repo.drop(idty) diff --git a/src/sakia/data/processors/nodes.py b/src/sakia/data/processors/nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..d5d92bf0e321b66a6326f1530bed03a2c1cba92a --- /dev/null +++ b/src/sakia/data/processors/nodes.py @@ -0,0 +1,136 @@ +import attr +import sqlite3 +from ..entities import Node +from duniterpy.documents import BlockUID, endpoint +import logging + + +@attr.s +class NodesProcessor: + _repo = attr.ib() # :type sakia.data.repositories.NodesRepo + + @classmethod + def instanciate(cls, app): + return cls(app.db.nodes_repo) + + def current_buid(self, currency): + """ + Get current buid + :param str currency: + """ + synced_node = self._repo.get_one(currency=currency, state=Node.ONLINE) + return synced_node.current_buid + + def synced_nodes(self, currency): + """ + Get nodes which are in the ONLINE state. + """ + return self._repo.get_all(currency=currency, state=Node.ONLINE) + + def synced_members_nodes(self, currency): + """ + Get nodes which are in the ONLINE state. + """ + return self._repo.get_all(currency=currency, state=Node.ONLINE, member=True) + + def online_nodes(self, currency): + """ + Get nodes which are in the ONLINE state. + """ + return self._repo.get_all(currency=currency, state=Node.ONLINE) + \ + self._repo.get_all(currency=currency, state=Node.DESYNCED) + + def update_node(self, node): + """ + Update node in the repository. + First involves basic checks about pubkey and primary key constraints. + + :param sakia.data.entities.Node node: the node to update + """ + other_node = self._repo.get_one(currency=node.currency, pubkey=node.pubkey) + if other_node: + self._repo.update(node) + else: + self._repo.insert(node) + + def insert_node(self, node): + """ + Update node in the repository. + First involves basic checks about pubkey and primary key constraints. + + :param sakia.data.entities.Node node: the node to update + """ + self._repo.insert(node) + + def commit_node(self, node): + """ + Saves a node state in the db + :param sakia.data.entities.Node node: the node updated + """ + try: + self._repo.insert(node) + except sqlite3.IntegrityError: + self._repo.update(node) + + def unknown_node(self, currency, pubkey): + """ + Search for pubkey in the repository. + :param str pubkey: the pubkey to lookup + """ + other_node = self._repo.get_one(currency=currency, pubkey=pubkey) + return other_node is None + + def nodes(self, currency): + """ + Get all knew nodes. + """ + return self._repo.get_all(currency=currency) + + def root_nodes(self, currency): + """ + Get root nodes. + """ + return self._repo.get_all(currency=currency, root=True) + + def current_buid(self, currency): + """ + Get the latest block considered valid + It is the most frequent last block of every known nodes + """ + blocks_uids = [n.current_buid for n in self.synced_nodes(currency)] + if len(blocks_uids) > 0: + return blocks_uids[0] + else: + return BlockUID.empty() + + def quality(self, currency): + """ + Get a ratio of the synced nodes vs the rest + """ + synced = len(self.synced_nodes(currency)) + total = len(self.nodes(currency)) + if total == 0: + ratio_synced = 0 + else: + ratio_synced = synced / total + return ratio_synced + + def update_peer(self, currency, peer): + """ + Update the peer of a node + :param str currency: the currency of the peer + :param peer: + :return: + """ + node = self._repo.get_one(pubkey=peer.pubkey, currency=currency) + if node and node.peer_blockstamp < peer.blockUID: + logging.debug("Update node : {0}".format(peer.pubkey[:5])) + node.endpoints = tuple(peer.endpoints) + node.peer_blockstamp = peer.blockUID + self._repo.update(node) + return node + + def drop_all(self): + nodes = self._repo.get_all() + for n in nodes: + self._repo.drop(n) \ No newline at end of file diff --git a/src/sakia/data/processors/sources.py b/src/sakia/data/processors/sources.py new file mode 100644 index 0000000000000000000000000000000000000000..36055a5ac27e305f80d96316f790a6c3e3da7e2f --- /dev/null +++ b/src/sakia/data/processors/sources.py @@ -0,0 +1,83 @@ +import attr +import sqlite3 +from ..entities import Source +from .nodes import NodesProcessor +from ..connectors import BmaConnector +from duniterpy.api import bma, errors + + +@attr.s +class SourcesProcessor: + """ + :param sakia.data.repositories.SourcesRepo _repo: the repository of the sources + :param sakia.data.connectors.bma.BmaConnector _bma_connector: the bma connector + """ + _repo = attr.ib() + _bma_connector = attr.ib() + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.sources_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters)) + + def commit(self, source): + try: + self._repo.insert(source) + except sqlite3.IntegrityError: + self._repo.update(source) + + async def initialize_sources(self, currency, pubkey, log_stream): + """ + Initialize sources for a given pubkey if no source exists locally + """ + log_stream("Requesting sources") + try: + sources_data = await self._bma_connector.get(currency, bma.tx.sources, + req_args={'pubkey': pubkey}, verify=False) + + log_stream("Found {0} sources".format(len(sources_data['sources']))) + for i, s in enumerate(sources_data['sources']): + source = Source(currency=currency, pubkey=pubkey, + identifier=s['identifier'], + type=s['type'], + noffset=s['noffset'], + amount=s['amount'], + base=s['base']) + self.commit(source) + log_stream("{0}/{1} sources".format(i, len(sources_data['sources']))) + except errors.DuniterError as e: + raise + + def amount(self, currency, pubkey): + """ + Get the amount value of the sources for a given pubkey + :param str currency: the currency of the sources + :param str pubkey: the pubkey owning the sources + :return: + """ + sources = self._repo.get_all(currency=currency, pubkey=pubkey) + return sum([s.amount * (10**s.base) for s in sources]) + + def available(self, currency): + """" + :param str currency: the currency of the sources + :rtype: list[sakia.data.entities.Source] + """ + return self._repo.get_all(currency=currency) + + def consume(self, sources): + """ + + :param currency: + :param sources: + :return: + """ + for s in sources: + self._repo.drop(s) + + def drop_all_of(self, currency, pubkey): + self._repo.drop_all(currency=currency, pubkey=pubkey) diff --git a/src/sakia/data/processors/transactions.py b/src/sakia/data/processors/transactions.py new file mode 100644 index 0000000000000000000000000000000000000000..35435d2f2b0996969c03e5991e81002fd8a8a2c4 --- /dev/null +++ b/src/sakia/data/processors/transactions.py @@ -0,0 +1,179 @@ +import logging +import attr +import asyncio +import sqlite3 +from ..entities import Transaction +from ..entities.transaction import parse_transaction_doc +from .nodes import NodesProcessor +from . import tx_lifecycle +from ..connectors import BmaConnector +from duniterpy.api import bma +from duniterpy.documents import Transaction as TransactionDoc + + +@attr.s +class TransactionsProcessor: + _repo = attr.ib() # :type sakia.data.repositories.SourcesRepo + _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector + _table_states = attr.ib(default=attr.Factory(dict)) + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.transactions_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters)) + + def next_txid(self, currency, block_number): + """ + :param str currency: + :param str block_number: + :rtype: int + """ + transfers = self._repo.get_all(currency=currency, written_on=block_number) + return max([tx.txid for tx in transfers]) if transfers else 0 + + def transfers(self, currency, pubkey): + """ + Get all transfers from or to a given pubkey + :param str currency: + :param str pubkey: + :return: the list of Transaction entities + :rtype: List[sakia.data.entities.Transaction] + """ + return self._repo.get_transfers(currency, pubkey) + + def _try_transition(self, tx, transition_key, *inputs): + """ + Try the transition defined by the given transition_key + with inputs + :param sakia.data.entities.Transaction tx: the transaction + :param tuple transition_key: The transition key in the table states + :param tuple inputs: The inputs + :return: True if the transition was applied + :rtype: bool + """ + if len(inputs) == len(transition_key[1]): + for i, input in enumerate(inputs): + if not isinstance(input, transition_key[1][i]): + return False + for transition in tx_lifecycle.states[transition_key]: + if transition[0](tx, *inputs): + if tx.sha_hash: + self._logger.debug("{0} : {1} --> {2}".format(tx.sha_hash[:5], tx.state, + transition[2])) + else: + self._logger.debug("Unsent transfer : {0} --> {1}".format(tx.state, + transition[2])) + + # If the transition changes data, apply changes + if transition[1]: + transition[1](tx, *inputs) + tx.state = transition[2] + return True + return False + + def commit(self, tx): + try: + self._repo.insert(tx) + except sqlite3.IntegrityError: + self._repo.update(tx) + + def find_by_hash(self, sha_hash): + return self._repo.get_one(sha_hash=sha_hash) + + def awaiting(self, currency): + return self._repo.get_all(currency=currency, state=Transaction.AWAITING) + + def run_state_transitions(self, tx, *inputs): + """ + Try all current state transitions with inputs + :param sakia.data.entities.Transaction tx: the transaction + :param tuple inputs: The inputs passed to the transitions + :return: True if the transaction changed state + :rtype: bool + """ + transition_keys = [k for k in tx_lifecycle.states.keys() if k[0] == tx.state] + for key in transition_keys: + if self._try_transition(tx, key, *inputs): + self._repo.update(tx) + return True + return False + + def cancel(self, tx): + """ + Cancel a local transaction + :param sakia.data.entities.Transaction tx: the transaction + """ + self.run_state_transitions(tx, ()) + + async def send(self, tx, txdoc, currency): + """ + Send a transaction and update the transfer state to AWAITING if accepted. + If the transaction was refused (return code != 200), state becomes REFUSED + The txdoc is saved as the transfer txdoc. + + :param sakia.data.entities.Transaction tx: the transaction + :param txdoc: A transaction duniterpy object + :param currency: The community target of the transaction + """ + self._logger.debug(txdoc.signed_raw()) + self._repo.insert(tx) + responses = await self._bma_connector.broadcast(currency, bma.tx.process, req_args={'transaction': txdoc.signed_raw()}) + result = (False, "") + for r in responses: + if isinstance(r, BaseException): + result = (False, str(r)) + elif r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.text() + self.run_state_transitions(tx, [r.status for r in responses if not isinstance(r, BaseException)]) + return result, tx + + async def initialize_transactions(self, connection, log_stream): + """ + Request transactions from the network to initialize data for a given pubkey + :param sakia.data.entities.Connection connection: + :param function log_stream: + """ + history_data = await self._bma_connector.get(connection.currency, bma.tx.history, + req_args={'pubkey': connection.pubkey}, verify=False) + txid = 0 + nb_tx = len(history_data["history"]["sent"]) + len(history_data["history"]["received"]) + log_stream("Found {0} transactions".format(nb_tx)) + transactions = [] + for sent_data in history_data["history"]["sent"] + history_data["history"]["received"]: + sent = TransactionDoc.from_bma_history(history_data["currency"], sent_data) + log_stream("{0}/{1} transactions".format(txid, nb_tx)) + try: + tx = parse_transaction_doc(sent, connection.pubkey, sent_data["block_number"], + sent_data["time"], txid) + self._repo.insert(tx) + transactions.append(tx) + except sqlite3.IntegrityError: + log_stream("Transaction already registered in database") + await asyncio.sleep(0) + txid += 1 + return transactions + + def cleanup_connection(self, connection, connections_pubkeys): + """ + Cleanup connections data after removal + :param sakia.data.entities.Connection connection: removed connection + :param List[str] connections_pubkeys: pubkeys of existing connections + :return: + """ + sent = self._repo.get_all(currency=connection.currency, issuer=connection.pubkey) + for tx in sent: + if tx.receiver not in connections_pubkeys: + self._repo.drop(tx) + received = self._repo.get_all(currency=connection.currency, receiver=connection.pubkey) + for tx in received: + if tx.issuer not in connections_pubkeys: + self._repo.drop(tx) diff --git a/src/sakia/data/processors/tx_lifecycle.py b/src/sakia/data/processors/tx_lifecycle.py new file mode 100644 index 0000000000000000000000000000000000000000..37b89567dcc9102a846434ce3eafc568b02ebe24 --- /dev/null +++ b/src/sakia/data/processors/tx_lifecycle.py @@ -0,0 +1,127 @@ +import time +from sakia.data.entities import Transaction +from duniterpy.documents import Block + + +def _found_in_block(tx, block): + """ + Check if the transaction can be found in the blockchain + :param sakia.data.entities.Transaction tx: the transaction + :param duniterpy.documents.Block block: The block to check for the transaction + :return: True if the transaction was found + :rtype: bool + """ + for block_tx in block.transactions: + if block_tx.sha_hash == tx.sha_hash: + return True + + +def _broadcast_success(tx, ret_codes): + """ + Check if the retcode is 200 after a POST + :param sakia.data.entities.Transaction tx: the transaction + :param list ret_codes: The POST return codes of the broadcast + :param duniterpy.documents.Block block: The current block used for transition. + :return: True if the post was successful + :rtype: bool + """ + return 200 in ret_codes + + +def _broadcast_failure(tx, ret_codes): + """ + Check if no retcode is 200 after a POST + :param sakia.data.entities.Transaction tx: the transaction + :param list ret_codes: The POST return codes of the broadcast + :return: True if the post was failed + :rtype: bool + """ + return 200 not in ret_codes + + +def _rollback_and_removed(tx, rollback, block): + """ + Check if the transfer is not in the block anymore + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is not found in the block + """ + if rollback: + if not block or block.blockUID != tx.blockstamp: + return True + else: + return tx.sha_hash not in [t.sha_hash for t in block.transactions] + return False + + +def _rollback_and_local(tx, rollback, block): + """ + Check if the transfer is not in the block anymore + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is found in the block + """ + if rollback and tx.local and block.blockUID == tx.blockstamp: + return tx.sha_hash not in [t.sha_hash for t in block.transactions] + return False + + +def _is_locally_created(tx): + """ + Check if we can send back the transaction if it was locally created + + :param sakia.data.entities.Transaction tx: the transaction + :return: True if the transaction was locally created + """ + return tx.local + + +def _be_validated(tx, block): + """ + Action when the transfer ins found in a block + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block checked + """ + tx.blockstamp = block.blockUID + tx.timestamp = block.mediantime + + +def _drop(tx): + """ + Cancel the transfer locally. + The transfer state becomes TransferState.DROPPED. + :param sakia.data.entities.Transaction tx: the transaction + """ + tx.blockstamp = None + + +# Dict containing states of a transfer : +# keys are a tuple containg (current_state, transition_parameters) +# values are tuples containing (transition_test, transition_success, new_state) +states = { + (Transaction.TO_SEND, (list,)): + ( + (_broadcast_success, None, Transaction.AWAITING), + (lambda tx, l: _broadcast_failure(tx, l), None, Transaction.REFUSED), + ), + (Transaction.TO_SEND, ()): + ((_is_locally_created, _drop, Transaction.DROPPED),), + + (Transaction.AWAITING, (Block,)): + ((_found_in_block, _be_validated, Transaction.VALIDATED),), + + (Transaction.VALIDATED, (bool,)): + ( + (_rollback_and_removed, lambda tx, r: _drop(tx), Transaction.DROPPED), + (_rollback_and_local, None, Transaction.AWAITING), + ), + + (Transaction.REFUSED, ()): + ((_is_locally_created, _drop, Transaction.DROPPED),) + } diff --git a/src/sakia/data/repositories/__init__.py b/src/sakia/data/repositories/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0a2eca6386d2ef45810e49dc76ff53328f5c1191 --- /dev/null +++ b/src/sakia/data/repositories/__init__.py @@ -0,0 +1,9 @@ +from .identities import IdentitiesRepo +from .blockchains import BlockchainsRepo +from .meta import SakiaDatabase +from .certifications import CertificationsRepo +from .transactions import TransactionsRepo +from .nodes import NodesRepo +from .connections import ConnectionsRepo +from .sources import SourcesRepo +from .dividends import DividendsRepo diff --git a/src/sakia/data/repositories/blockchains.py b/src/sakia/data/repositories/blockchains.py new file mode 100644 index 0000000000000000000000000000000000000000..c6dd9fb4edc88a7cce688536fd1e31747843723d --- /dev/null +++ b/src/sakia/data/repositories/blockchains.py @@ -0,0 +1,121 @@ +from typing import List + +import attr + +from ..entities import Blockchain, BlockchainParameters + + +@attr.s(frozen=True) +class BlockchainsRepo: + """The repository for Blockchain entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Blockchain.currency,) + + def insert(self, blockchain): + """ + Commit a blockchain to the database + :param sakia.data.entities.Blockchain blockchain: the blockchain to commit + """ + blockchain_tuple = attr.astuple(blockchain.parameters) \ + + attr.astuple(blockchain, filter=attr.filters.exclude(Blockchain.parameters)) + values = ",".join(['?'] * len(blockchain_tuple)) + self._conn.execute("INSERT INTO blockchains VALUES ({0})".format(values), blockchain_tuple) + + def update(self, blockchain): + """ + Update an existing blockchain in the database + :param sakia.data.entities.Blockchain blockchain: the blockchain to update + """ + updated_fields = attr.astuple(blockchain, filter=attr.filters.exclude( + Blockchain.parameters, *BlockchainsRepo._primary_keys)) + where_fields = attr.astuple(blockchain, filter=attr.filters.include(*BlockchainsRepo._primary_keys)) + self._conn.execute("""UPDATE blockchains SET + current_buid=?, + current_members_count=?, + current_mass=?, + median_time=?, + last_members_count=?, + last_ud=?, + last_ud_base=?, + last_ud_time=?, + previous_mass=?, + previous_members_count=?, + previous_ud=?, + previous_ud_base=?, + previous_ud_time=? + WHERE + currency=?""", + updated_fields + where_fields) + + def get_one(self, **search): + """ + Get an existing blockchain in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Blockchain + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + if filters: + request = "SELECT * FROM blockchains WHERE {filters}".format(filters=" AND ".join(filters)) + else: + request = "SELECT * FROM blockchains" + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Blockchain(BlockchainParameters(*data[:16]), *data[17:]) + + def get_all(self, offset=0, limit=1000, sort_by="currency", sort_order="ASC", **search) -> List[Blockchain]: + """ + Get all existing blockchain in the database corresponding to the search + :param int offset: offset in results to paginate + :param int limit: limit results to paginate + :param str sort_by: column name to sort by + :param str sort_order: sort order ASC or DESC + :param dict search: the criterions of the lookup + :rtype: [sakia.data.entities.Blockchain] + """ + filters = [] + values = [] + if search: + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = """SELECT * FROM blockchains WHERE {filters} + ORDER BY {sort_by} {sort_order} + LIMIT {limit} OFFSET {offset}""".format( + filters=" AND ".join(filters), + offset=offset, + limit=limit, + sort_by=sort_by, + sort_order=sort_order + ) + c = self._conn.execute(request, tuple(values)) + else: + request = """SELECT * FROM blockchains + ORDER BY {sort_by} {sort_order} + LIMIT {limit} OFFSET {offset}""".format( + offset=offset, + limit=limit, + sort_by=sort_by, + sort_order=sort_order + ) + c = self._conn.execute(request) + datas = c.fetchall() + if datas: + return [Blockchain(BlockchainParameters(*data[:16]), *data[17:]) for data in datas] + return [] + + def drop(self, blockchain): + """ + Drop an existing blockchain from the database + :param sakia.data.entities.Blockchain blockchain: the blockchain to update + """ + where_fields = attr.astuple(blockchain, filter=attr.filters.include(*BlockchainsRepo._primary_keys)) + self._conn.execute("DELETE FROM blockchains WHERE currency=?", where_fields) diff --git a/src/sakia/data/repositories/certifications.py b/src/sakia/data/repositories/certifications.py new file mode 100644 index 0000000000000000000000000000000000000000..1ef13cdde7549ec37e08eeeef6552ca70fe09c11 --- /dev/null +++ b/src/sakia/data/repositories/certifications.py @@ -0,0 +1,109 @@ +import attr + +from ..entities import Certification + + +@attr.s(frozen=True) +class CertificationsRepo: + """The repository for Communities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Certification.currency, Certification.certified, + Certification.certifier, Certification.block,) + + def insert(self, certification): + """ + Commit a certification to the database + :param sakia.data.entities.Certification certification: the certification to commit + """ + certification_tuple = attr.astuple(certification) + values = ",".join(['?'] * len(certification_tuple)) + self._conn.execute("INSERT INTO certifications VALUES ({0})".format(values), certification_tuple) + + def update(self, certification): + """ + Update an existing certification in the database + :param sakia.data.entities.Certification certification: the certification to update + """ + updated_fields = attr.astuple(certification, filter=attr.filters.exclude(*CertificationsRepo._primary_keys)) + where_fields = attr.astuple(certification, filter=attr.filters.include(*CertificationsRepo._primary_keys)) + self._conn.execute("""UPDATE certifications SET + ts=?, + signature=?, + written_on=? + WHERE + currency=? AND + certifier=? AND + certified=? AND + block=?""", + updated_fields + where_fields) + + def get_one(self, **search): + """ + Get an existing certification in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Certification + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM certifications WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Certification(*data) + + def get_all(self, **search): + """ + Get all existing certification in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Certification + """ + filters = [] + values = [] + for k, v in search.items(): + value = v + filters.append("{key} = ?".format(key=k)) + values.append(value) + + request = "SELECT * FROM certifications WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Certification(*data) for data in datas] + return [] + + def get_latest_sent(self, currency, pubkey): + """ + Get latest sent certification + :param str currency: + :param str pubkey: + :return: + :rtype: sakia.data.entities.Certification + """ + request = """SELECT * FROM certifications + WHERE currency=? AND certifier=? + ORDER BY ts DESC + LIMIT 1""" + c = self._conn.execute(request, (currency, pubkey)) + data = c.fetchone() + if data: + return Certification(*data) + + def drop(self, certification): + """ + Drop an existing certification from the database + :param sakia.data.entities.Certification certification: the certification to update + """ + where_fields = attr.astuple(certification, filter=attr.filters.include(*CertificationsRepo._primary_keys)) + self._conn.execute("""DELETE FROM certifications + WHERE + currency=? AND + certifier=? AND + certified=? AND + block=?""", where_fields) diff --git a/src/sakia/data/repositories/connections.py b/src/sakia/data/repositories/connections.py new file mode 100644 index 0000000000000000000000000000000000000000..1c24fad895c86d783be23e527a290f7acb638c3f --- /dev/null +++ b/src/sakia/data/repositories/connections.py @@ -0,0 +1,121 @@ +import attr + +from ..entities import Connection + + +@attr.s(frozen=True) +class ConnectionsRepo: + """ + The repository for Connections entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Connection.currency, Connection.pubkey) + + def insert(self, connection): + """ + Commit a connection to the database + :param sakia.data.entities.Connection connection: the connection to commit + """ + connection_tuple = attr.astuple(connection, filter=attr.filters.exclude(Connection.password)) + values = ",".join(['?'] * len(connection_tuple)) + self._conn.execute("INSERT INTO connections VALUES ({0})".format(values), connection_tuple) + + def update(self, connection): + """ + Update an existing connection in the database + :param sakia.data.entities.Connection connection: the certification to update + """ + updated_fields = attr.astuple(connection, filter=attr.filters.exclude(Connection.password, + *ConnectionsRepo._primary_keys)) + where_fields = attr.astuple(connection, filter=attr.filters.include(*ConnectionsRepo._primary_keys)) + + self._conn.execute("""UPDATE connections SET + salt=?, + uid=?, + scrypt_N=?, + scrypt_p=?, + scrypt_r=?, + blockstamp=? + WHERE + currency=? AND + pubkey=? + """, updated_fields + where_fields) + + def get_one(self, **search): + """ + Get an existing connection in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Connection + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM connections WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Connection(*data) + + def get_all(self, **search): + """ + Get all existing connection in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Connection + """ + filters = [] + values = [] + for k, v in search.items(): + value = v + filters.append("{connection} = ?".format(connection=k)) + values.append(value) + + request = "SELECT * FROM connections" + if filters: + request += " WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Connection(*data) for data in datas] + return [] + + def get_currencies(self): + """ + Get all existing connection in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: List[str] + """ + request = "SELECT DISTINCT currency FROM connections" + c = self._conn.execute(request) + datas = c.fetchall() + if datas: + return [data[0] for data in datas] + return [] + + def get_pubkeys(self): + """ + Get all existing connection in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: List[str] + """ + request = "SELECT DISTINCT pubkey FROM connections" + c = self._conn.execute(request) + datas = c.fetchall() + if datas: + return [data[0] for data in datas] + return [] + + def drop(self, connection): + """ + Drop an existing connection from the database + :param sakia.data.entities.Connection connection: the connection to update + """ + where_fields = attr.astuple(connection, filter=attr.filters.include(*ConnectionsRepo._primary_keys)) + self._conn.execute("""DELETE FROM connections + WHERE + currency=? AND + pubkey=?""", where_fields) diff --git a/src/sakia/data/repositories/dividends.py b/src/sakia/data/repositories/dividends.py new file mode 100644 index 0000000000000000000000000000000000000000..41ba667f732dc38bb73745be09ed7f8ceb2fc93b --- /dev/null +++ b/src/sakia/data/repositories/dividends.py @@ -0,0 +1,92 @@ +import attr + +from ..entities import Dividend + + +@attr.s(frozen=True) +class DividendsRepo: + """The repository for Communities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Dividend.currency, Dividend.pubkey, Dividend.block_number) + + def insert(self, dividend): + """ + Commit a dividend to the database + :param sakia.data.entities.Dividend dividend: the dividend to commit + """ + dividend_tuple = attr.astuple(dividend) + values = ",".join(['?'] * len(dividend_tuple)) + self._conn.execute("INSERT INTO dividends VALUES ({0})".format(values), dividend_tuple) + + def get_one(self, **search): + """ + Get an existing dividend in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Dividend + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM dividends WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Dividend(*data) + + def get_all(self, **search): + """ + Get all existing dividend in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Dividend + """ + filters = [] + values = [] + for k, v in search.items(): + value = v + filters.append("{key} = ?".format(key=k)) + values.append(value) + + request = "SELECT * FROM dividends WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Dividend(*data) for data in datas] + return [] + + def get_dividends(self, currency, pubkey, offset=0, limit=1000, sort_by="currency", sort_order="ASC"): + """ + Get all transfers in the database on a given currency from or to a pubkey + + :param str pubkey: the criterions of the lookup + :rtype: List[sakia.data.entities.Dividend] + """ + request = """SELECT * FROM dividends + WHERE currency=? AND pubkey=? + ORDER BY {sort_by} {sort_order} + LIMIT {limit} OFFSET {offset}""" \ + .format(offset=offset, + limit=limit, + sort_by=sort_by, + sort_order=sort_order + ) + c = self._conn.execute(request, (currency, pubkey, pubkey)) + datas = c.fetchall() + if datas: + return [Dividend(*data) for data in datas] + return [] + + def drop(self, dividend): + """ + Drop an existing dividend from the database + :param sakia.data.entities.Dividend dividend: the dividend to update + """ + where_fields = attr.astuple(dividend, filter=attr.filters.include(*DividendsRepo._primary_keys)) + self._conn.execute("""DELETE FROM dividends + WHERE + currency=? AND pubkey=? AND block_number=? """, where_fields) diff --git a/src/sakia/data/repositories/identities.py b/src/sakia/data/repositories/identities.py new file mode 100644 index 0000000000000000000000000000000000000000..9e432205249859aaa00a02d1b08d6a63c790acbe --- /dev/null +++ b/src/sakia/data/repositories/identities.py @@ -0,0 +1,114 @@ +import attr + +from duniterpy.documents.block import BlockUID + +from ..entities import Identity + + +@attr.s(frozen=True) +class IdentitiesRepo: + """The repository for Identities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Identity.currency, Identity.pubkey, Identity.uid, Identity.blockstamp) + + def insert(self, identity): + """ + Commit an identity to the database + :param sakia.data.entities.Identity identity: the identity to commit + """ + identity_tuple = attr.astuple(identity) + values = ",".join(['?'] * len(identity_tuple)) + self._conn.execute("INSERT INTO identities VALUES ({0})".format(values), identity_tuple) + + def update(self, identity): + """ + Update an existing identity in the database + :param sakia.data.entities.Identity identity: the identity to update + """ + updated_fields = attr.astuple(identity, filter=attr.filters.exclude(*IdentitiesRepo._primary_keys)) + where_fields = attr.astuple(identity, filter=attr.filters.include(*IdentitiesRepo._primary_keys)) + self._conn.execute("""UPDATE identities SET + signature=?, + timestamp=?, + written_on=?, + revoked_on=?, + outdistanced=?, + member=?, + ms_buid=?, + ms_timestamp=?, + ms_written_on=?, + ms_type=? + WHERE + currency=? AND + pubkey=? AND + uid=? AND + blockstamp=?""", updated_fields + where_fields + ) + + def get_one(self, **search): + """ + Get an existing identity in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Identity + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM identities WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Identity(*data) + + def get_all(self, **search): + """ + Get all existing identity in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Identity + """ + filters = [] + values = [] + for k, v in search.items(): + if isinstance(v, bool): + v = int(v) + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM identities WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Identity(*data) for data in datas] + return [] + + def find_all(self, currency, text): + """ + Get all existing identity in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Identity + """ + request = "SELECT * FROM identities WHERE currency=? AND (UID LIKE ? or PUBKEY LIKE ?)" + + c = self._conn.execute(request, (currency, "%{0}%".format(text), "%{0}%".format(text))) + datas = c.fetchall() + if datas: + return [Identity(*data) for data in datas] + return [] + + def drop(self, identity): + """ + Drop an existing identity from the database + :param sakia.data.entities.Identity identity: the identity to update + """ + where_fields = attr.astuple(identity, filter=attr.filters.include(*IdentitiesRepo._primary_keys)) + self._conn.execute("""DELETE FROM identities WHERE + currency=? AND + pubkey=? AND + uid=? AND + blockstamp=?""", where_fields) diff --git a/src/sakia/data/repositories/meta.py b/src/sakia/data/repositories/meta.py new file mode 100644 index 0000000000000000000000000000000000000000..4bae0a53f13a2ccc60d80cf6a69e855a24c0e661 --- /dev/null +++ b/src/sakia/data/repositories/meta.py @@ -0,0 +1,100 @@ +import attr +import os +import logging +import sqlite3 +from duniterpy.documents import BlockUID +from .connections import ConnectionsRepo +from .identities import IdentitiesRepo +from .blockchains import BlockchainsRepo +from .certifications import CertificationsRepo +from .transactions import TransactionsRepo +from .dividends import DividendsRepo +from .nodes import NodesRepo +from .sources import SourcesRepo + + +@attr.s(frozen=True) +class SakiaDatabase: + """ + This is Sakia unique SQLite database. + """ + conn = attr.ib() # :type sqlite3.Connection + connections_repo = attr.ib(default=None) + identities_repo = attr.ib(default=None) + blockchains_repo = attr.ib(default=None) + certifications_repo = attr.ib(default=None) + transactions_repo = attr.ib(default=None) + nodes_repo = attr.ib(default=None) + sources_repo = attr.ib(default=None) + dividends_repo = attr.ib(default=None) + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def load_or_init(cls, options, profile_name): + sqlite3.register_adapter(BlockUID, str) + sqlite3.register_adapter(bool, int) + sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) + con = sqlite3.connect(os.path.join(options.config_path, profile_name, options.database + ".db"), + detect_types=sqlite3.PARSE_DECLTYPES) + meta = SakiaDatabase(con, ConnectionsRepo(con), IdentitiesRepo(con), + BlockchainsRepo(con), CertificationsRepo(con), TransactionsRepo(con), + NodesRepo(con), SourcesRepo(con), DividendsRepo(con)) + meta.prepare() + meta.upgrade_database() + return meta + + def prepare(self): + """ + Prepares the database if the table is missing + """ + with self.conn: + self._logger.debug("Initializing meta database") + self.conn.execute("""CREATE TABLE IF NOT EXISTS meta( + id INTEGER NOT NULL, + version INTEGER NOT NULL, + PRIMARY KEY (id) + )""" + ) + + @property + def upgrades(self): + return [ + self.create_all_tables, + ] + + def upgrade_database(self): + """ + Execute the migrations + """ + self._logger.debug("Begin upgrade of database...") + version = self.version() + nb_versions = len(self.upgrades) + for v in range(version, nb_versions): + self._logger.debug("Upgrading to version {0}...".format(v)) + self.upgrades[v]() + with self.conn: + self.conn.execute("UPDATE meta SET version=? WHERE id=1", (version + 1,)) + self._logger.debug("End upgrade of database...") + + def create_all_tables(self): + """ + Init all the tables + :return: + """ + self._logger.debug("Initialiazing all databases") + sql_file = open(os.path.join(os.path.dirname(__file__), 'meta.sql'), 'r') + with self.conn: + self.conn.executescript(sql_file.read()) + + def version(self): + with self.conn: + c = self.conn.execute("SELECT * FROM meta WHERE id=1") + data = c.fetchone() + if data: + return data[1] + else: + self.conn.execute("INSERT INTO meta VALUES (1, 0)") + return 0 + + def commit(self): + self.conn.commit() diff --git a/src/sakia/data/repositories/meta.sql b/src/sakia/data/repositories/meta.sql new file mode 100644 index 0000000000000000000000000000000000000000..d53558f529b4f09223ac3a982316e427c2bdeebe --- /dev/null +++ b/src/sakia/data/repositories/meta.sql @@ -0,0 +1,143 @@ +-- IDENTITY TABLE +CREATE TABLE IF NOT EXISTS identities( + currency VARCHAR(30), + pubkey VARCHAR(50), + uid VARCHAR(255), + blockstamp VARCHAR(100), + signature VARCHAR(100), + timestamp INT, + written_on INT, + revoked_on INT, + outdistanced BOOLEAN, + member BOOLEAN, + ms_buid VARCHAR(100), + ms_timestamp INT, + ms_written_on INT, + ms_type VARCHAR(5), + PRIMARY KEY (currency, pubkey, uid, blockstamp) + ); + +-- BLOCKCHAIN TABLE +CREATE TABLE IF NOT EXISTS blockchains ( + c FLOAT(1, 6), + dt INT, + ud0 INT, + sig_period INT, + sig_stock INT, + sig_window INT, + idty_window INT, + ms_window INT, + sig_validity INT, + sig_qty INT, + xpercent FLOAT(1, 6), + ms_validity INT, + step_max INT, + median_time_blocks INT, + avg_gen_time INT, + dt_diff_eval INT, + percent_rot FLOAT(1, 6), + current_buid INT, + current_members_count INT, + current_mass INT, + median_time INT, + last_members_count INT, + last_ud INT, + last_ud_base INT, + last_ud_time INT, + previous_mass INT, + previous_members_count INT, + previous_ud INT, + previous_ud_base INT, + previous_ud_time INT, + currency VARCHAR(30), + PRIMARY KEY (currency) +); + + +-- CERTIFICATIONS TABLE +CREATE TABLE IF NOT EXISTS certifications( + currency VARCHAR(30), + certifier VARCHAR(50), + certified VARCHAR(50), + block INT, + ts INT, + signature VARCHAR(100), + written_on INT, + PRIMARY KEY (currency, certifier, certified, block) + ); + +-- TRANSACTIONS TABLE +CREATE TABLE IF NOT EXISTS transactions( + currency VARCHAR(30), + sha_hash VARCHAR(50), + written_on INT, + blockstamp VARCHAR(100), + ts INT, + signature VARCHAR(100), + issuer VARCHAR(50), + receiver VARCHAR(50), + amount INT, + amountbase INT, + comment VARCHAR(255), + txid INT, + state INT, + local BOOLEAN, + raw TEXT, + PRIMARY KEY (sha_hash) + ); + +-- NODES TABLE +CREATE TABLE IF NOT EXISTS nodes( + currency VARCHAR(30), + pubkey VARCHAR(50), + endpoints TEXT, + peer_buid VARCHAR(100), + uid VARCHAR(50), + current_buid VARCHAR(100), + current_ts INT, + previous_buid VARCHAR(100), + state INT, + software VARCHAR(100), + version VARCHAR(50), + merkle_peers_root VARCHAR(50), + merkle_peers_leaves TEXT, + root BOOLEAN, + member BOOLEAN, + PRIMARY KEY (currency, pubkey) + ); + +-- Cnnections TABLE +CREATE TABLE IF NOT EXISTS connections( + currency VARCHAR(30), + pubkey VARCHAR(50), + salt VARCHAR(50), + uid VARCHAR(255), + scrypt_N INT, + scrypt_p INT, + scrypt_r INT, + blockstamp VARCHAR(100), + PRIMARY KEY (currency, pubkey) + ); + +-- Cnnections TABLE +CREATE TABLE IF NOT EXISTS sources( + currency VARCHAR(30), + pubkey VARCHAR(50), + identifier VARCHAR(255), + noffset INT, + type VARCHAR(8), + amount INT, + base INT, + PRIMARY KEY (currency, pubkey, identifier, noffset) + ); + +CREATE TABLE IF NOT EXISTS dividends( + currency VARCHAR(30), + pubkey VARCHAR(50), + block_number VARCHAR(255), + timestamp INT, + amount INT, + base INT, + PRIMARY KEY (currency, pubkey, block_number) +); + diff --git a/src/sakia/data/repositories/nodes.py b/src/sakia/data/repositories/nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..4500574e3346f3c374d3827ad2e64831209ae61d --- /dev/null +++ b/src/sakia/data/repositories/nodes.py @@ -0,0 +1,110 @@ +import attr + +from ..entities import Node + + +@attr.s(frozen=True) +class NodesRepo: + """The repository for Communities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Node.currency, Node.pubkey) + + def insert(self, node): + """ + Commit a node to the database + :param sakia.data.entities.Node node: the node to commit + """ + node_tuple = attr.astuple(node, tuple_factory=list) + node_tuple[2] = "\n".join([str(n) for n in node_tuple[2]]) + node_tuple[12] = "\n".join([str(n) for n in node_tuple[12]]) + values = ",".join(['?'] * len(node_tuple)) + self._conn.execute("INSERT INTO nodes VALUES ({0})".format(values), node_tuple) + + def update(self, node): + """ + Update an existing node in the database + :param sakia.data.entities.Node node: the node to update + """ + updated_fields = attr.astuple(node, tuple_factory=list, + filter=attr.filters.exclude(*NodesRepo._primary_keys)) + updated_fields[0] = "\n".join([str(n) for n in updated_fields[0]]) + updated_fields[10] = "\n".join([str(n) for n in updated_fields[9]]) + where_fields = attr.astuple(node, tuple_factory=list, + filter=attr.filters.include(*NodesRepo._primary_keys)) + self._conn.execute("""UPDATE nodes SET + endpoints=?, + peer_buid=?, + uid=?, + current_buid=?, + current_ts=?, + previous_buid=?, + state=?, + software=?, + version=?, + merkle_peers_root=?, + merkle_peers_leaves=?, + root=?, + member=? + WHERE + currency=? AND + pubkey=?""", + updated_fields + where_fields) + + def get_one(self, **search): + """ + Get an existing node in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Node + """ + filters = [] + values = [] + for k, v in search.items(): + if isinstance(v, bool): + v = int(v) + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM nodes WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Node(*data) + + def get_all(self, **search): + """ + Get all existing node in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Node + """ + filters = [] + values = [] + for k, v in search.items(): + if isinstance(v, bool): + value = int(v) + else: + value = v + filters.append("{key} = ?".format(key=k)) + values.append(value) + + if filters: + request = "SELECT * FROM nodes WHERE {filters}".format(filters=" AND ".join(filters)) + else: + request = "SELECT * FROM nodes" + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Node(*data) for data in datas] + return [] + + def drop(self, node): + """ + Drop an existing node from the database + :param sakia.data.entities.Node node: the node to update + """ + where_fields = attr.astuple(node, filter=attr.filters.include(*NodesRepo._primary_keys)) + self._conn.execute("""DELETE FROM nodes + WHERE + currency=? AND pubkey=?""", where_fields) diff --git a/src/sakia/data/repositories/sources.py b/src/sakia/data/repositories/sources.py new file mode 100644 index 0000000000000000000000000000000000000000..bd5266cf27688cc0810caddc8c4b8e746a0a5b9a --- /dev/null +++ b/src/sakia/data/repositories/sources.py @@ -0,0 +1,81 @@ +import attr + +from ..entities import Source + + +@attr.s(frozen=True) +class SourcesRepo: + """The repository for Communities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Source.identifier,) + + def insert(self, source): + """ + Commit a source to the database + :param sakia.data.entities.Source source: the source to commit + """ + source_tuple = attr.astuple(source) + values = ",".join(['?'] * len(source_tuple)) + self._conn.execute("INSERT INTO sources VALUES ({0})".format(values), source_tuple) + + def get_one(self, **search): + """ + Get an existing source in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Source + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM sources WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Source(*data) + + def get_all(self, **search): + """ + Get all existing source in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Source + """ + filters = [] + values = [] + for k, v in search.items(): + value = v + filters.append("{key} = ?".format(key=k)) + values.append(value) + + request = "SELECT * FROM sources WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Source(*data) for data in datas] + return [] + + def drop(self, source): + """ + Drop an existing source from the database + :param sakia.data.entities.Source source: the source to update + """ + where_fields = attr.astuple(source, filter=attr.filters.include(*SourcesRepo._primary_keys)) + self._conn.execute("""DELETE FROM sources + WHERE + identifier=?""", where_fields) + + def drop_all(self, **filter): + filters = [] + values = [] + for k, v in filter.items(): + value = v + filters.append("{key} = ?".format(key=k)) + values.append(value) + + request = "DELETE FROM sources WHERE {filters}".format(filters=" AND ".join(filters)) + self._conn.execute(request, tuple(values)) diff --git a/src/sakia/data/repositories/transactions.py b/src/sakia/data/repositories/transactions.py new file mode 100644 index 0000000000000000000000000000000000000000..5f735b6b64507f1fcbd331464fbe285c64f5874a --- /dev/null +++ b/src/sakia/data/repositories/transactions.py @@ -0,0 +1,118 @@ +import attr + +from ..entities import Transaction + + +@attr.s(frozen=True) +class TransactionsRepo: + """The repository for Communities entities. + """ + _conn = attr.ib() # :type sqlite3.Connection + _primary_keys = (Transaction.sha_hash,) + + def insert(self, transaction): + """ + Commit a transaction to the database + :param sakia.data.entities.Transaction transaction: the transaction to commit + """ + transaction_tuple = attr.astuple(transaction) + values = ",".join(['?'] * len(transaction_tuple)) + self._conn.execute("INSERT INTO transactions VALUES ({0})".format(values), transaction_tuple) + + def update(self, transaction): + """ + Update an existing transaction in the database + :param sakia.data.entities.Transaction transaction: the transaction to update + """ + updated_fields = attr.astuple(transaction, filter=attr.filters.exclude(*TransactionsRepo._primary_keys)) + where_fields = attr.astuple(transaction, filter=attr.filters.include(*TransactionsRepo._primary_keys)) + self._conn.execute("""UPDATE transactions SET + currency=?, + written_on=?, + blockstamp=?, + ts=?, + signature=?, + issuer = ?, + receiver = ?, + amount = ?, + amountbase = ?, + comment = ?, + txid = ?, + state = ?, + local = ?, + raw = ? + WHERE + sha_hash=?""", + updated_fields + where_fields) + + def get_one(self, **search): + """ + Get an existing transaction in the database + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Transaction + """ + filters = [] + values = [] + for k, v in search.items(): + filters.append("{k}=?".format(k=k)) + values.append(v) + + request = "SELECT * FROM transactions WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + data = c.fetchone() + if data: + return Transaction(*data) + + def get_all(self, **search): + """ + Get all existing transaction in the database corresponding to the search + :param dict search: the criterions of the lookup + :rtype: sakia.data.entities.Transaction + """ + filters = [] + values = [] + for k, v in search.items(): + value = v + filters.append("{key} = ?".format(key=k)) + values.append(value) + + request = "SELECT * FROM transactions WHERE {filters}".format(filters=" AND ".join(filters)) + + c = self._conn.execute(request, tuple(values)) + datas = c.fetchall() + if datas: + return [Transaction(*data) for data in datas] + return [] + + def get_transfers(self, currency, pubkey, offset=0, limit=1000, sort_by="currency", sort_order="ASC"): + """ + Get all transfers in the database on a given currency from or to a pubkey + + :param str pubkey: the criterions of the lookup + :rtype: List[sakia.data.entities.Transaction] + """ + request = """SELECT * FROM transactions + WHERE currency=? AND (issuer=? or receiver=?) + ORDER BY {sort_by} {sort_order} + LIMIT {limit} OFFSET {offset}""" \ + .format(offset=offset, + limit=limit, + sort_by=sort_by, + sort_order=sort_order + ) + c = self._conn.execute(request, (currency, pubkey, pubkey)) + datas = c.fetchall() + if datas: + return [Transaction(*data) for data in datas] + return [] + + def drop(self, transaction): + """ + Drop an existing transaction from the database + :param sakia.data.entities.Transaction transaction: the transaction to update + """ + where_fields = attr.astuple(transaction, filter=attr.filters.include(*TransactionsRepo._primary_keys)) + self._conn.execute("""DELETE FROM transactions + WHERE + sha_hash=?""", where_fields) diff --git a/src/sakia/tools/decorators.py b/src/sakia/decorators.py similarity index 75% rename from src/sakia/tools/decorators.py rename to src/sakia/decorators.py index 7648502ebeb53d64180a81e1c5b04e40aa330538..f9dc5f2103c04299d0d010141f8ab759f13d8c5e 100644 --- a/src/sakia/tools/decorators.py +++ b/src/sakia/decorators.py @@ -19,24 +19,24 @@ def once_at_a_time(fn): func_call = args[0].__tasks[fn.__name__] args[0].__tasks.pop(fn.__name__) if getattr(func_call, "_next_task", None): - func_call._next_task._start() + start_task(func_call._next_task[0], + *func_call._next_task[1], + **func_call._next_task[2]) except KeyError: logging.debug("Task {0} already removed".format(fn.__name__)) - def start_task(): - args[0].__tasks[fn.__name__] = fn(*args, **kwargs) - args[0].__tasks[fn.__name__].add_done_callback(task_done) + def start_task(f, *a, **k): + args[0].__tasks[f.__name__] = f(*a, **k) + args[0].__tasks[f.__name__].add_done_callback(task_done) if getattr(args[0], "__tasks", None) is None: setattr(args[0], "__tasks", {}) - fn._start = lambda: start_task() - if fn.__name__ in args[0].__tasks: - args[0].__tasks[fn.__name__]._next_task = fn + args[0].__tasks[fn.__name__]._next_task = (fn, args, kwargs) args[0].__tasks[fn.__name__].cancel() else: - fn._start() + start_task(fn, *args, **kwargs) return args[0].__tasks[fn.__name__] return wrapper diff --git a/src/sakia/errors.py b/src/sakia/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..801e47dc5764ae7167520f919eb3383debe150ba --- /dev/null +++ b/src/sakia/errors.py @@ -0,0 +1,63 @@ +""" +Created on 9 févr. 2014 + +@author: inso +""" + + +class Error(Exception): + + def __init__(self, message): + """ + Constructor + """ + self.message = "Error : " + message + + def __str__(self): + return self.message + + +class NotEnoughChangeError(Error): + + """ + Exception raised when trying to send money but user + is missing change + """ + + def __init__(self, available, currency, nb_inputs, requested): + """ + Constructor + """ + super() .__init__( + "Only {0} {1} available in {2} sources, needs {3}" + .format(available, + currency, + nb_inputs, + requested)) + + +class NoPeerAvailable(Error): + """ + Exception raised when a community doesn't have any + peer available. + """ + def __init__(self, currency, nbpeers): + """ + Constructor + """ + super() .__init__( + "No peer answered in {0} community ({1} peers available)" + .format(currency, nbpeers)) + + +class InvalidNodeCurrency(Error): + """ + Exception raised when a node doesn't use the intended currency + """ + def __init__(self, currency, node_currency): + """ + Constructor + """ + super() .__init__( + "Node is working for {0} currency, but should be {1}" + .format(node_currency, currency)) diff --git a/src/sakia/gui/certification.py b/src/sakia/gui/certification.py deleted file mode 100644 index f19a2840f8edd3c566ff446b065525db639b87ae..0000000000000000000000000000000000000000 --- a/src/sakia/gui/certification.py +++ /dev/null @@ -1,263 +0,0 @@ -""" -Created on 24 dec. 2014 - -@author: inso -""" -import asyncio -import logging -from duniterpy.api import errors -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QApplication, QMessageBox -from PyQt5.QtCore import Qt, QObject, QLocale, QDateTime - -from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox -from .member import MemberDialog -from ..tools.decorators import asyncify, once_at_a_time -from ..tools.exceptions import NoPeerAvailable -from ..gen_resources.certification_uic import Ui_CertificationDialog - -class CertificationDialog(QObject): - """ - A dialog to certify individuals - """ - - def __init__(self, app, account, password_asker, widget, ui): - """ - Constructor if a certification dialog - - :param sakia.core.Application app: - :param sakia.core.Account account: - :param sakia.gui.password_asker.PasswordAsker password_asker: - :param PyQt5.QtWidgets widget: the widget of the dialog - :param sakia.gen_resources.certification_uic.Ui_CertificationDialog view: the view of the certification dialog - :return: - """ - super().__init__() - self.widget = widget - self.ui = ui - self.ui.setupUi(self.widget) - self.app = app - self.account = account - self.password_asker = password_asker - self.community = self.account.communities[0] - - self.ui.radio_contact.toggled.connect(lambda c, radio="contact": self.recipient_mode_changed(radio)) - self.ui.radio_pubkey.toggled.connect(lambda c, radio="pubkey": self.recipient_mode_changed(radio)) - self.ui.radio_search.toggled.connect(lambda c, radio="search": self.recipient_mode_changed(radio)) - self.ui.button_box.accepted.connect(self.accept) - self.ui.button_box.rejected.connect(self.widget.reject) - - for community in self.account.communities: - self.ui.combo_community.addItem(community.currency) - - for contact_name in sorted([c['name'] for c in account.contacts], key=str.lower): - self.ui.combo_contact.addItem(contact_name) - - if len(account.contacts) == 0: - self.ui.radio_pubkey.setChecked(True) - self.ui.radio_contact.setEnabled(False) - - self.ui.member_widget = MemberDialog.as_widget(self.ui.groupBox, self.app, self.account, self.community, None) - self.ui.horizontalLayout_5.addWidget(self.ui.member_widget.widget) - - self.ui.search_user.button_reset.hide() - self.ui.search_user.init(self.app) - self.ui.search_user.change_account(self.account) - self.ui.search_user.change_community(self.community) - self.ui.combo_contact.currentIndexChanged.connect(self.refresh_member) - self.ui.edit_pubkey.textChanged.connect(self.refresh_member) - self.ui.search_user.identity_selected.connect(self.refresh_member) - self.ui.radio_contact.toggled.connect(self.refresh_member) - self.ui.radio_search.toggled.connect(self.refresh_member) - self.ui.radio_pubkey.toggled.connect(self.refresh_member) - self.ui.combo_community.currentIndexChanged.connect(self.change_current_community) - - @classmethod - def open_dialog(cls, app, account, community, password_asker): - """ - Certify and identity - :param sakia.core.Application app: the application - :param sakia.core.Account account: the account certifying the identity - :param sakia.core.Community community: the community - :param sakia.gui.password_asker.PasswordAsker password_asker: the password asker - :return: - """ - dialog = cls(app, account, password_asker, QDialog(), Ui_CertificationDialog()) - if community: - dialog.ui.combo_community.setCurrentText(community.name) - dialog.refresh() - return dialog.exec() - - @classmethod - async def certify_identity(cls, app, account, password_asker, community, identity): - """ - Certify and identity - :param sakia.core.Application app: the application - :param sakia.core.Account account: the account certifying the identity - :param sakia.gui.password_asker.PasswordAsker password_asker: the password asker - :param sakia.core.Community community: the community - :param sakia.core.registry.Identity identity: the identity certified - :return: - """ - dialog = cls(app, account, password_asker, QDialog(), Ui_CertificationDialog()) - dialog.ui.combo_community.setCurrentText(community.name) - dialog.ui.edit_pubkey.setText(identity.pubkey) - dialog.ui.radio_pubkey.setChecked(True) - dialog.refresh() - return await dialog.async_exec() - - @asyncify - async def accept(self): - """ - Validate the dialog - """ - pubkey = self.selected_pubkey() - if pubkey: - password = await self.password_asker.async_exec() - if password == "": - self.ui.button_box.setEnabled(True) - return - QApplication.setOverrideCursor(Qt.WaitCursor) - result = await self.account.certify(password, self.community, pubkey) - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("Certification"), - self.tr("Success sending certification")) - else: - await QAsyncMessageBox.information(self.widget, self.tr("Certification"), - self.tr("Success sending certification")) - QApplication.restoreOverrideCursor() - self.widget.accept() - else: - if self.app.preferences['notifications']: - toast.display(self.tr("Certification"), self.tr("Could not broadcast certification : {0}" - .format(result[1]))) - else: - await QAsyncMessageBox.critical(self.widget, self.tr("Certification"), - self.tr("Could not broadcast certification : {0}" - .format(result[1]))) - QApplication.restoreOverrideCursor() - self.ui.button_box.setEnabled(True) - - def change_current_community(self, index): - self.community = self.account.communities[index] - self.ui.search_user.change_community(self.community) - self.ui.member_widget.change_community(self.community) - if self.widget.isVisible(): - self.refresh() - - def selected_pubkey(self): - """ - Get selected pubkey in the widgets of the window - :return: the current pubkey - :rtype: str - """ - pubkey = None - if self.ui.radio_contact.isChecked(): - for contact in self.account.contacts: - if contact['name'] == self.ui.combo_contact.currentText(): - pubkey = contact['pubkey'] - break - elif self.ui.radio_search.isChecked(): - if self.ui.search_user.current_identity(): - pubkey = self.ui.search_user.current_identity().pubkey - else: - pubkey = self.ui.edit_pubkey.text() - return pubkey - - @asyncify - async def refresh_member(self, checked=False): - """ - Refresh the member widget - """ - current_pubkey = self.selected_pubkey() - if current_pubkey: - identity = await self.app.identities_registry.future_find(current_pubkey, self.community) - else: - identity = None - self.ui.member_widget.identity = identity - self.ui.member_widget.refresh() - - @once_at_a_time - @asyncify - async def refresh(self): - account_identity = await self.account.identity(self.community) - is_member = await account_identity.is_member(self.community) - try: - block_0 = await self.community.get_block(0) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - block_0 = None - except NoPeerAvailable as e: - logging.debug(str(e)) - block_0 = None - - params = await self.community.parameters() - certifications = await account_identity.unique_valid_certified_by(self.app.identities_registry, self.community) - nb_certifications = len([c for c in certifications if c['block_number']]) - nb_cert_pending = len([c for c in certifications if not c['block_number']]) - remaining_time = await account_identity.cert_issuance_delay(self.app.identities_registry, self.community) - cert_text = self.tr("Certifications sent : {nb_certifications}/{stock}").format( - nb_certifications=nb_certifications, - stock=params['sigStock']) - if nb_cert_pending > 0: - cert_text += " (+{nb_cert_pending} certifications pending)".format(nb_cert_pending=nb_cert_pending) - if remaining_time > 0: - cert_text += "\n" - days, remainder = divmod(remaining_time, 3600*24) - hours, remainder = divmod(remainder, 3600) - minutes, seconds = divmod(remainder, 60) - if days > 0: - remaining_localized = self.tr("{days} days").format(days=days) - else: - remaining_localized = self.tr("{hours} hours and {min} min.").format(hours=hours, - min=minutes) - cert_text += self.tr("Remaining time before next certification validation : {0}".format(remaining_localized)) - self.ui.label_cert_stock.setText(cert_text) - - if is_member or not block_0: - if nb_certifications < params['sigStock'] or params['sigStock'] == 0: - self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(True) - if remaining_time > 0: - self.ui.button_box.button(QDialogButtonBox.Ok).setText(self.tr("&Ok") + - self.tr(" (Not validated before ") - + remaining_localized + ")") - else: - self.ui.button_box.button(QDialogButtonBox.Ok).setText(self.tr("&Ok")) - else: - self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - self.ui.button_box.button(QDialogButtonBox.Ok).setText(self.tr("No more certifications")) - else: - self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - self.ui.button_box.button(QDialogButtonBox.Ok).setText(self.tr("Not a member")) - - def showEvent(self, event): - super().showEvent(event) - self.first_certification_check() - - def first_certification_check(self): - if self.account.notifications['warning_certifying_first_time']: - self.account.notifications['warning_certifying_first_time'] = False - QMessageBox.warning(self, "Certifying individuals", """Please follow the following guidelines : -1.) Don't certify an account if you believe the issuers identity might be faked. -2.) Don't certify an account if you believe the issuer already has another certified account. -3.) Don't certify an account if you believe the issuer purposely or carelessly violates rule 1 or 2 (the issuer certifies faked or double accounts -""") - - def recipient_mode_changed(self, radio): - """ - :param str radio: - """ - self.ui.edit_pubkey.setEnabled(radio == "pubkey") - self.ui.combo_contact.setEnabled(radio == "contact") - self.ui.search_user.setEnabled(radio == "search") - - def async_exec(self): - future = asyncio.Future() - self.widget.finished.connect(lambda r: future.set_result(r)) - self.widget.open() - self.refresh() - return future - - def exec(self): - self.widget.exec() diff --git a/src/sakia/gui/community_tile.py b/src/sakia/gui/community_tile.py deleted file mode 100644 index 3e59dc2574943482645ad20661f03ba739790945..0000000000000000000000000000000000000000 --- a/src/sakia/gui/community_tile.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -@author: inso -""" - -import enum - -from PyQt5.QtWidgets import QFrame, QLabel, QVBoxLayout, QLayout -from PyQt5.QtCore import QSize, pyqtSignal, QTime -from duniterpy.documents.block import Block -from duniterpy.api import errors - -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from ..tools.exceptions import NoPeerAvailable -from .widgets.busy import Busy - - -@enum.unique -class CommunityState(enum.Enum): - NOT_INIT = 0 - OFFLINE = 1 - READY = 2 - - -class CommunityTile(QFrame): - clicked = pyqtSignal() - _hover_stylesheet = """QFrame#CommunityTile { -border-radius: 5px; -background-color: palette(midlight); -} -""" - _pressed_stylesheet = """QFrame#CommunityTile { -border-radius: 5px; -background-color: palette(dark); -} -""" - _standard_stylesheet = """QFrame#CommunityTile { -border-radius: 5px; -background-color: palette(base); -} -""" - - def __init__(self, parent, app, community): - super().__init__(parent) - self.setObjectName("CommunityTile") - self.app = app - self.community = community - self.community.network.nodes_changed.connect(self.handle_nodes_change) - self.text_label = QLabel() - self.setLayout(QVBoxLayout()) - self.layout().setSizeConstraint(QLayout.SetFixedSize) - self.layout().addWidget(self.text_label) - self.setFrameShape(QFrame.StyledPanel) - self.setFrameShadow(QFrame.Raised) - self.setStyleSheet(CommunityTile._standard_stylesheet) - self.busy = Busy(self) - self.busy.hide() - self._state = CommunityState.NOT_INIT - self.refresh() - - def sizeHint(self): - return QSize(250, 250) - - def handle_nodes_change(self): - if len(self.community.network.online_nodes) > 0: - if self.community.network.current_blockUID.sha_hash == Block.Empty_Hash: - state = CommunityState.NOT_INIT - else: - state = CommunityState.READY - else: - state = CommunityState.OFFLINE - - if state != self._state: - self.refresh() - - def cancel_refresh(self): - cancel_once_task(self, self.refresh) - - @once_at_a_time - @asyncify - async def refresh(self): - self.busy.show() - self.setFixedSize(QSize(150, 150)) - try: - current_block = await self.community.get_block() - members_pubkeys = await self.community.members_pubkeys() - amount = await self.app.current_account.amount(self.community) - localized_amount = await self.app.current_account.current_ref.instance(amount, - self.community, self.app).localized(units=True, - international_system=self.app.preferences['international_system_of_units']) - if current_block['monetaryMass']: - localized_monetary_mass = await self.app.current_account.current_ref.instance(current_block['monetaryMass'], - self.community, self.app).diff_localized(units=True, - international_system=self.app.preferences['international_system_of_units']) - else: - localized_monetary_mass = "" - status = self.app.current_account.pubkey in members_pubkeys - account_identity = await self.app.current_account.identity(self.community) - - mstime_remaining_text = self.tr("Expired or never published") - outdistanced_text = self.tr("Outdistanced") - - requirements = await account_identity.requirements(self.community) - mstime_remaining = 0 - nb_certs = 0 - if requirements: - mstime_remaining = requirements['membershipExpiresIn'] - nb_certs = len(requirements['certifications']) - if not requirements['outdistanced']: - outdistanced_text = self.tr("In WoT range") - - if mstime_remaining > 0: - days, remainder = divmod(mstime_remaining, 3600*24) - hours, remainder = divmod(remainder, 3600) - minutes, seconds = divmod(remainder, 60) - mstime_remaining_text = self.tr("Expires in ") - if days > 0: - mstime_remaining_text += "{days} days".format(days=days) - else: - mstime_remaining_text += "{hours} hours and {min} min.".format(hours=hours, - min=minutes) - - status_value = self.tr("Member") if status else self.tr("Non-Member") - status_color = '#00AA00' if status else self.tr('#FF0000') - description = """<html> - <body> - <p> - <span style=" font-size:16pt; font-weight:600;">{currency}</span> - </p> - <p>{nb_members} {members_label}</p> - <p><span style="font-weight:600;">{monetary_mass_label}</span> : {monetary_mass}</p> - <p><span style="font-weight:600;">{status_label}</span> : <span style="color:{status_color};">{status}</span></p> - <p><span style="font-weight:600;">{nb_certs_label}</span> : {nb_certs} ({outdistanced_text})</p> - <p><span style="font-weight:600;">{mstime_remaining_label}</span> : {mstime_remaining}</p> - <p><span style="font-weight:600;">{balance_label}</span> : {balance}</p> - </body> - </html>""".format(currency=self.community.currency, - nb_members=len(members_pubkeys), - members_label=self.tr("members"), - monetary_mass_label=self.tr("Monetary mass"), - monetary_mass=localized_monetary_mass, - status_color=status_color, - status_label=self.tr("Status"), - status=status_value, - nb_certs_label=self.tr("Certs. received"), - nb_certs=nb_certs, - outdistanced_text=outdistanced_text, - mstime_remaining_label=self.tr("Membership"), - mstime_remaining=mstime_remaining_text, - balance_label=self.tr("Balance"), - balance=localized_amount) - self.text_label.setText(description) - self._state = CommunityState.READY - except NoPeerAvailable: - description = """<html> - <body> - <p> - <span style=" font-size:16pt; font-weight:600;">{currency}</span> - </p> - <p>{message}</p> - </body> - </html>""".format(currency=self.community.currency, - message=self.tr("Not connected")) - self.text_label.setText(description) - self._state = CommunityState.OFFLINE - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - description = """<html> - <body> - <p> - <span style=" font-size:16pt; font-weight:600;">{currency}</span> - </p> - <p>{message}</p> - </body> - </html>""".format(currency=self.community.currency, - message=self.tr("Community not initialized")) - self.text_label.setText(description) - self._state = CommunityState.NOT_INIT - else: - raise - - self.busy.hide() - - def mousePressEvent(self, event): - self.grabMouse() - self.setStyleSheet(CommunityTile._pressed_stylesheet) - return super().mousePressEvent(event) - - def mouseReleaseEvent(self, event): - self.releaseMouse() - self.setStyleSheet(CommunityTile._hover_stylesheet) - self.clicked.emit() - return super().mouseReleaseEvent(event) - - def resizeEvent(self, event): - self.busy.resize(event.size()) - super().resizeEvent(event) - - def enterEvent(self, event): - self.setStyleSheet(CommunityTile._hover_stylesheet) - return super().enterEvent(event) - - def leaveEvent(self, event): - self.setStyleSheet(CommunityTile._standard_stylesheet) - return super().leaveEvent(event) diff --git a/src/sakia/gui/community_view.py b/src/sakia/gui/community_view.py deleted file mode 100644 index 53f47851bb33874355621ab45729821b67fc6e20..0000000000000000000000000000000000000000 --- a/src/sakia/gui/community_view.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" - -import logging -import time -from duniterpy.api import errors -from PyQt5.QtCore import pyqtSlot, QDateTime, QLocale, QEvent, QT_TRANSLATE_NOOP, Qt -from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtWidgets import QWidget, QMessageBox, QDialog, QPushButton, QTabBar, QAction, QMenu, QFileDialog - -from .graphs.wot_tab import WotTabWidget -from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox, QAsyncFileDialog, dialog_async_exec -from .identities_tab import IdentitiesTabWidget -from .informations_tab import InformationsTabWidget -from .network_tab import NetworkTabWidget -from .transactions_tab import TransactionsTabWidget -from .graphs.explorer_tab import ExplorerTabWidget -from ..gen_resources.community_view_uic import Ui_CommunityWidget -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from ..tools.exceptions import MembershipNotFoundError, LookupFailureError, NoPeerAvailable - - -class CommunityWidget(QWidget, Ui_CommunityWidget): - - """ - classdocs - """ - - _tab_history_label = QT_TRANSLATE_NOOP("CommunityWidget", "Transactions") - _tab_wot_label = QT_TRANSLATE_NOOP("CommunityWidget", "Web of Trust") - _tab_identities_label = QT_TRANSLATE_NOOP("CommunityWidget", "Search Identities") - _tab_network_label = QT_TRANSLATE_NOOP("CommunityWidget", "Network") - _tab_informations_label = QT_TRANSLATE_NOOP("CommunityWidget", "Informations") - _action_showinfo_text = QT_TRANSLATE_NOOP("CommunityWidget", "Show informations") - _action_explore_text = QT_TRANSLATE_NOOP("CommunityWidget", "Explore the Web of Trust") - _action_publish_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Publish UID") - _action_revoke_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Revoke UID") - - def __init__(self, app, status_label, label_icon): - """ - Constructor - """ - super().__init__() - self.app = app - self.account = None - self.community = None - self.password_asker = None - self.status_label = status_label - self.label_icon = label_icon - - self.status_info = [] - - self.tab_wot = WotTabWidget(self.app) - self.tab_identities = IdentitiesTabWidget(self.app) - self.tab_history = TransactionsTabWidget(self.app) - self.tab_informations = InformationsTabWidget(self.app) - self.tab_network = NetworkTabWidget(self.app) - self.tab_explorer = ExplorerTabWidget(self.app) - - self.action_publish_uid = QAction(self.tr(CommunityWidget._action_publish_uid_text), self) - self.action_revoke_uid = QAction(self.tr(CommunityWidget._action_revoke_uid_text), self) - self.action_showinfo = QAction(self.tr(CommunityWidget._action_showinfo_text), self) - self.action_explorer = QAction(self.tr(CommunityWidget._action_explore_text), self) - - super().setupUi(self) - - tool_menu = QMenu(self.tr("Tools"), self.toolbutton_menu) - self.toolbutton_menu.setMenu(tool_menu) - - self.tab_identities.view_in_wot.connect(self.tab_wot.draw_graph) - self.tab_identities.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot.widget)) - self.tab_history.view_in_wot.connect(self.tab_wot.draw_graph) - self.tab_history.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot.widget)) - self.tab_identities.money_sent.connect(lambda: self.tab_history.ui.table_history.model().sourceModel().refresh_transfers()) - self.tab_wot.money_sent.connect(lambda: self.tab_history.ui.table_history.model().sourceModel().refresh_transfers()) - - self.tabs.addTab(self.tab_history.widget, - QIcon(':/icons/tx_icon'), - self.tr(CommunityWidget._tab_history_label)) - - self.tabs.addTab(self.tab_wot.widget, - QIcon(':/icons/wot_icon'), - self.tr(CommunityWidget._tab_wot_label)) - - self.tabs.addTab(self.tab_identities.widget, - QIcon(':/icons/members_icon'), - self.tr(CommunityWidget._tab_identities_label)) - - self.tabs.addTab(self.tab_network, - QIcon(":/icons/network_icon"), - self.tr("Network")) - - action_showinfo = QAction(self.tr("Show informations"), self.toolbutton_menu) - action_showinfo.triggered.connect(lambda : self.show_closable_tab(self.tab_informations, - QIcon(":/icons/informations_icon"), self.tr("Informations"))) - tool_menu.addAction(action_showinfo) - - action_showexplorer = QAction(self.tr("Show explorer"), self.toolbutton_menu) - action_showexplorer.triggered.connect(lambda : self.show_closable_tab(self.tab_explorer.widget, - QIcon(":/icons/explorer_icon"), self.tr("Explorer"))) - tool_menu.addAction(action_showexplorer) - - menu_advanced = QMenu(self.tr("Advanced"), self.toolbutton_menu) - action_gen_revokation = QAction(self.tr("Save revokation document"), menu_advanced) - action_gen_revokation.triggered.connect(self.action_save_revokation) - menu_advanced.addAction(action_gen_revokation) - tool_menu.addMenu(menu_advanced) - - self.action_publish_uid.triggered.connect(self.publish_uid) - tool_menu.addAction(self.action_publish_uid) - - self.button_membership.clicked.connect(self.send_membership_demand) - - def show_closable_tab(self, tab, icon, title): - if self.tabs.indexOf(tab) == -1: - self.tabs.addTab(tab, icon, title) - style = self.app.qapp.style() - icon = style.standardIcon(style.SP_DockWidgetCloseButton) - close_button = QPushButton(icon, '') - close_button.clicked.connect(lambda: self.tabs.removeTab(self.tabs.indexOf(tab))) - close_button.setStyleSheet('border-style: inset;') - self.tabs.tabBar().setTabButton(self.tabs.indexOf(tab), QTabBar.RightSide, close_button) - - def cancel_once_tasks(self): - cancel_once_task(self, self.refresh_block) - cancel_once_task(self, self.refresh_status) - logging.debug("Cancelled status") - cancel_once_task(self, self.refresh_quality_buttons) - - def change_account(self, account, password_asker): - self.cancel_once_tasks() - - self.account = account - - self.password_asker = password_asker - self.tab_wot.change_account(account, self.password_asker) - self.tab_identities.change_account(account, self.password_asker) - self.tab_history.change_account(account, self.password_asker) - self.tab_informations.change_account(account) - self.tab_explorer.change_account(account, self.password_asker) - - def change_community(self, community): - self.cancel_once_tasks() - - self.tab_network.change_community(community) - self.tab_wot.change_community(community) - self.tab_history.change_community(community) - self.tab_identities.change_community(community) - self.tab_informations.change_community(community) - self.tab_explorer.change_community(community) - - if self.community: - self.community.network.new_block_mined.disconnect(self.refresh_block) - self.community.network.nodes_changed.disconnect(self.refresh_status) - if community: - community.network.new_block_mined.connect(self.refresh_block) - community.network.nodes_changed.connect(self.refresh_status) - self.label_currency.setText(community.currency) - logging.debug("Changed community to {0}".format(community)) - self.button_membership.setText(self.tr("Membership")) - self.button_membership.setEnabled(False) - self.button_certification.setEnabled(False) - self.action_publish_uid.setEnabled(False) - self.community = community - self.refresh_status() - self.refresh_quality_buttons() - - @pyqtSlot(str) - def display_error(self, error): - QMessageBox.critical(self, ":(", - error, - QMessageBox.Ok) - - @asyncify - async def action_save_revokation(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - - raw_document = await self.account.generate_revokation(self.community, password) - # Testable way of using a QFileDialog - selected_files = await QAsyncFileDialog.get_save_filename(self, self.tr("Save a revokation document"), - "", self.tr("All text files (*.txt)")) - if selected_files: - path = selected_files[0] - if not path.endswith('.txt'): - path = "{0}.txt".format(path) - with open(path, 'w') as save_file: - save_file.write(raw_document) - - dialog = QMessageBox(QMessageBox.Information, self.tr("Revokation file"), - self.tr("""<div>Your revokation document has been saved.</div> -<div><b>Please keep it in a safe place.</b></div> -The publication of this document will remove your identity from the network.</p>"""), QMessageBox.Ok, - self) - dialog.setTextFormat(Qt.RichText) - await dialog_async_exec(dialog) - - @once_at_a_time - @asyncify - async def refresh_block(self, block_number): - """ - When a new block is found, start handling data. - @param: block_number: The number of the block mined - """ - logging.debug("Refresh block") - self.status_info.clear() - try: - person = await self.app.identities_registry.future_find(self.app.current_account.pubkey, self.community) - expiration_time = await person.membership_expiration_time(self.community) - revokation_time = await person.identity_revocation_time(self.community) - parameters = await self.community.parameters() - sig_validity = parameters['sigValidity'] - warning_expiration_time = int(sig_validity / 3) - will_expire_soon = (expiration_time < warning_expiration_time) - revokation_soon = (revokation_time < 2*warning_expiration_time) - if revokation_soon: - days = int(revokation_time / 3600 / 24) - if 'warning_revokation' not in self.status_info: - self.status_info.append('warning_revokation') - - if self.app.preferences['notifications'] and \ - self.account.notifications['warning_revokation'][1]+24*3600 < time.time(): - toast.display(self.tr("Identity revokation"), - self.tr("<b>Warning : Your identity will be implicitely revoked\ - if you dont renew before {0} days</b>").format(days)) - self.account.notifications['warning_revokation'][1] = time.time() - elif will_expire_soon: - days = int(expiration_time / 3600 / 24) - if days > 0: - if 'membership_expire_soon' not in self.status_info: - self.status_info.append('membership_expire_soon') - - if self.app.preferences['notifications'] and\ - self.account.notifications['membership_expire_soon'][1]+24*3600 < time.time(): - toast.display(self.tr("Membership expiration"), - self.tr("<b>Warning : Membership expiration in {0} days</b>").format(days)) - self.account.notifications['membership_expire_soon'][1] = time.time() - - certifiers_of = await person.unique_valid_certifiers_of(self.app.identities_registry, - self.community) - if len(certifiers_of) < parameters['sigQty']: - if 'warning_certifications' not in self.status_info: - self.status_info.append('warning_certifications') - if self.app.preferences['notifications'] and\ - self.account.notifications['warning_certifications'][1]+24*3600 < time.time(): - toast.display(self.tr("Certifications number"), - self.tr("<b>Warning : You are certified by only {0} persons, need {1}</b>") - .format(len(certifiers_of), - parameters['sigQty'])) - self.account.notifications['warning_certifications'][1] = time.time() - - except MembershipNotFoundError as e: - pass - except NoPeerAvailable: - logging.debug("No peer available") - self.refresh_data() - - def refresh_data(self): - """ - Refresh data - """ - self.tab_history.refresh_balance() - self.refresh_status() - - @once_at_a_time - @asyncify - async def refresh_status(self): - """ - Refresh status bar - """ - logging.debug("Refresh status") - if self.community: - text = "" - - current_block_number = self.community.network.current_blockUID.number - if current_block_number: - text += self.tr("Block {0}").format(current_block_number) - try: - block = await self.community.get_block(current_block_number) - text += " ({0})".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block['medianTime']), - QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) - )) - except NoPeerAvailable as e: - logging.debug(str(e)) - text += " ( ### ) " - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - logging.debug(str(e)) - - if len(self.community.network.synced_nodes) == 0: - self.button_membership.setEnabled(False) - self.button_certification.setEnabled(False) - self.button_send_money.setEnabled(False) - else: - self.button_send_money.setEnabled(True) - self.refresh_quality_buttons() - - if self.community.network.quality > 0.66: - icon = ':/icons/connected' - elif self.community.network.quality > 0.33: - icon = ':/icons/weak_connect' - else: - icon = ':/icons/disconnected' - - status_infotext = " - ".join([self.account.notifications[info][0] for info in self.status_info]) - label_text = text - if status_infotext != "": - label_text += " - {0}".format(status_infotext) - - self.status_label.setText(label_text) - self.label_icon.setPixmap(QPixmap(icon).scaled(24, 24, Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - @once_at_a_time - @asyncify - async def refresh_quality_buttons(self): - if self.account and self.community: - try: - account_identity = await self.account.identity(self.community) - published_uid = await account_identity.published_uid(self.community) - uid_is_revokable = await account_identity.uid_is_revokable(self.community) - if published_uid: - logging.debug("UID Published") - self.action_revoke_uid.setEnabled(uid_is_revokable) - is_member = await account_identity.is_member(self.community) - if is_member: - self.button_membership.setText(self.tr("Renew membership")) - self.button_membership.setEnabled(True) - self.button_certification.setEnabled(True) - self.action_publish_uid.setEnabled(False) - else: - logging.debug("Not a member") - self.button_membership.setText(self.tr("Send membership demand")) - self.button_membership.setEnabled(True) - self.action_publish_uid.setEnabled(False) - if await self.community.get_block(0) is not None: - self.button_certification.setEnabled(False) - else: - logging.debug("UID not published") - self.button_membership.setEnabled(False) - self.button_certification.setEnabled(False) - self.action_publish_uid.setEnabled(True) - except LookupFailureError: - self.button_membership.setEnabled(False) - self.button_certification.setEnabled(False) - self.action_publish_uid.setEnabled(False) - - def showEvent(self, event): - self.refresh_status() - - def referential_changed(self): - if self.community and self.tab_history.ui.table_history.model(): - self.tab_history.ui.table_history.model().sourceModel().refresh_transfers() - self.tab_history.refresh_balance() - self.tab_informations.refresh() - - @asyncify - async def send_membership_demand(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - result = await self.account.send_membership(password, self.community, 'IN') - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) - else: - await QAsyncMessageBox.information(self, self.tr("Membership"), - self.tr("Success sending Membership demand")) - else: - if self.app.preferences['notifications']: - toast.display(self.tr("Membership"), result[1]) - else: - await QAsyncMessageBox.critical(self, self.tr("Membership"), - result[1]) - - @asyncify - async def send_membership_leaving(self): - reply = await QAsyncMessageBox.warning(self, self.tr("Warning"), - self.tr("""Are you sure ? -Sending a leaving demand cannot be canceled. -The process to join back the community later will have to be done again.""") -.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) - if reply == QMessageBox.Ok: - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - result = await self.account.send_membership(password, self.community, 'OUT') - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("Revoke"), self.tr("Success sending Revoke demand")) - else: - await QAsyncMessageBox.information(self, self.tr("Revoke"), - self.tr("Success sending Revoke demand")) - else: - if self.app.preferences['notifications']: - toast.display(self.tr("Revoke"), result[1]) - else: - await QAsyncMessageBox.critical(self, self.tr("Revoke"), - result[1]) - - @asyncify - async def publish_uid(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - result = await self.account.send_selfcert(password, self.community) - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("UID"), self.tr("Success publishing your UID")) - else: - await QAsyncMessageBox.information(self, self.tr("Membership"), - self.tr("Success publishing your UID")) - else: - if self.app.preferences['notifications']: - toast.display(self.tr("UID"), result[1]) - else: - await QAsyncMessageBox.critical(self, self.tr("UID"), - result[1]) - - def retranslateUi(self, widget): - """ - Method to complete translations missing from generated code - :param widget: - :return: - """ - self.tabs.setTabText(self.tabs.indexOf(self.tab_wot.widget), self.tr(CommunityWidget._tab_wot_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_network), self.tr(CommunityWidget._tab_network_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_informations), self.tr(CommunityWidget._tab_informations_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_history.widget), self.tr(CommunityWidget._tab_history_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_identities.widget), self.tr(CommunityWidget._tab_identities_label)) - self.action_publish_uid.setText(self.tr(CommunityWidget._action_publish_uid_text)) - self.action_revoke_uid.setText(self.tr(CommunityWidget._action_revoke_uid_text)) - self.action_showinfo.setText(self.tr(CommunityWidget._action_showinfo_text)) - super().retranslateUi(self) - - def showEvent(self, QShowEvent): - """ - - :param QShowEvent: - :return: - """ - self.refresh_status() - super().showEvent(QShowEvent) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh_status() - return super(CommunityWidget, self).changeEvent(event) diff --git a/src/sakia/gui/contact.py b/src/sakia/gui/contact.py deleted file mode 100644 index 8d3036b91222e6f993f91173a9294fdad1bc6a0b..0000000000000000000000000000000000000000 --- a/src/sakia/gui/contact.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" -import re -import logging - -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox -from ..core.registry import IdentitiesRegistry -from ..tools.exceptions import ContactAlreadyExists -from ..gen_resources.contact_uic import Ui_ConfigureContactDialog - - -class ConfigureContactDialog(QDialog, Ui_ConfigureContactDialog): - - """ - classdocs - """ - - def __init__(self, app, account, parent=None, contact=None, index_edit=None): - """ - Open the dialog to create a new contact - :param sakia.core.Application app: the application - :param sakia.core.Account account: the account - :param PyQt5.QtWidgets.QWidget parent: the parent widget - :param dict contact: the contact with a key 'name' and a key 'pubkey' - :param int index_edit: the index of the edited contact in the account contacts list - :return: - """ - super().__init__(parent) - self.setupUi(self) - self.app = app - self.account = account - self.index_edit = index_edit - self.contact = contact - - if index_edit is not None: - self.contact = account.contacts[index_edit] - self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - - if self.contact: - self.edit_name.setText(self.contact['name']) - self.edit_pubkey.setText(self.contact['pubkey']) - - @classmethod - def from_identity(cls, app, parent, account, identity): - contact = { - 'name': identity.uid, - 'pubkey': identity.pubkey - } - return ConfigureContactDialog(app, account, parent, contact) - - @classmethod - def new_contact(cls, app, account, parent): - """ - Open the dialog to create a new contact - :param sakia.core.Application app: the application - :param sakia.core.Account account: the account - :param PyQt5.QtWidgets.QWidget parent: the parent widget - :return: - """ - return ConfigureContactDialog(app, account, parent) - - @classmethod - def edit_contact(cls, app, account, parent, index): - return ConfigureContactDialog(app, account, parent, None, index) - - def accept(self): - name = self.edit_name.text() - pubkey = self.edit_pubkey.text() - if self.index_edit is not None: - self.account.edit_contact(self.index_edit, {'name': name, - 'pubkey': pubkey}) - logging.debug(self.contact) - else: - try: - self.account.add_contact({'name': name, - 'pubkey': pubkey}) - except ContactAlreadyExists as e: - QMessageBox.critical(self, self.tr("Contact already exists"), - str(e), - QMessageBox.Ok) - self.app.save(self.account) - super().accept() - - def name_edited(self, new_name): - name_ok = len(new_name) > 0 - self.button_box.button(QDialogButtonBox.Ok).setEnabled(name_ok) - - def pubkey_edited(self, new_pubkey): - pattern = re.compile("([1-9A-Za-z][^OIl]{42,45})") - self.button_box.button( - QDialogButtonBox.Ok).setEnabled( - pattern.match(new_pubkey)is not None) diff --git a/src/sakia/core/net/api/bma/__init__.py b/src/sakia/gui/dialogs/__init__.py similarity index 100% rename from src/sakia/core/net/api/bma/__init__.py rename to src/sakia/gui/dialogs/__init__.py diff --git a/src/sakia/gen_resources/__init__.py b/src/sakia/gui/dialogs/certification/__init__.py similarity index 100% rename from src/sakia/gen_resources/__init__.py rename to src/sakia/gui/dialogs/certification/__init__.py diff --git a/src/sakia/gui/dialogs/certification/certification.ui b/src/sakia/gui/dialogs/certification/certification.ui new file mode 100644 index 0000000000000000000000000000000000000000..f67156ae2a03696f7c30b8337562a0ea5a3be949 --- /dev/null +++ b/src/sakia/gui/dialogs/certification/certification.ui @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CertificationDialog</class> + <widget class="QDialog" name="CertificationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>517</width> + <height>338</height> + </rect> + </property> + <property name="windowTitle"> + <string>Certification</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Select your identity</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QComboBox" name="combo_pubkey"/> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Certifications stock</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QLabel" name="label_cert_stock"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupbox_certified"> + <property name="title"> + <string>Certify user</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"/> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="button_box"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> + <slots> + <slot>open_manage_wallet_coins()</slot> + <slot>change_displayed_wallet(int)</slot> + <slot>transfer_mode_changed(bool)</slot> + <slot>recipient_mode_changed(bool)</slot> + <slot>change_current_community(int)</slot> + <slot>amount_changed()</slot> + <slot>relative_amount_changed()</slot> + </slots> +</ui> diff --git a/src/sakia/gui/dialogs/certification/controller.py b/src/sakia/gui/dialogs/certification/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..c656ede2529b9620d450611fa423af4852abab2a --- /dev/null +++ b/src/sakia/gui/dialogs/certification/controller.py @@ -0,0 +1,178 @@ +import asyncio + +from PyQt5.QtCore import Qt, QObject +from PyQt5.QtWidgets import QApplication + +from sakia.data.entities import Identity +from sakia.decorators import asyncify +from sakia.gui.sub.search_user.controller import SearchUserController +from sakia.gui.sub.user_information.controller import UserInformationController +from sakia.gui.password_asker import PasswordAskerDialog +from .model import CertificationModel +from .view import CertificationView +import attr + + +@attr.s() +class CertificationController(QObject): + """ + The Certification view + """ + + view = attr.ib() + model = attr.ib() + search_user = attr.ib(default=None) + user_information = attr.ib(default=None) + + def __attrs_post_init__(self): + super().__init__() + self.view.button_box.accepted.connect(self.accept) + self.view.button_box.rejected.connect(self.reject) + self.view.combo_pubkey.currentIndexChanged.connect(self.change_connection) + + @classmethod + def create(cls, parent, app): + """ + Instanciate a Certification component + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.app.Application app: sakia application + :return: a new Certification controller + :rtype: CertificationController + """ + view = CertificationView(parent.view if parent else None, None, None) + model = CertificationModel(app) + certification = cls(view, model, None, None) + + search_user = SearchUserController.create(certification, app, "") + certification.set_search_user(search_user) + + user_information = UserInformationController.create(certification, app, "", None) + certification.set_user_information(user_information) + + view.set_keys(certification.model.available_connections()) + return certification + + @classmethod + def open_dialog(cls, parent, app, connection): + """ + Certify and identity + :param sakia.gui.component.controller.ComponentController parent: the parent + :param sakia.core.Application app: the application + :param sakia.core.Account account: the account certifying the identity + :param sakia.core.Community community: the community + :return: + """ + dialog = cls.create(parent, app) + if connection: + dialog.view.combo_pubkey.setCurrentText(connection.title()) + dialog.refresh() + return dialog.exec() + + @classmethod + async def certify_identity(cls, parent, app, connection, identity): + """ + Certify and identity + :param sakia.gui.component.controller.ComponentController parent: the parent + :param sakia.core.Application app: the application + :param sakia.data.entities.Connection connection: the connection + :param sakia.data.entities.Identity identity: the identity certified + :return: + """ + dialog = cls.create(parent, app) + dialog.view.combo_pubkey.setCurrentText(connection.title()) + dialog.user_information.change_identity(identity) + dialog.refresh() + return await dialog.async_exec() + + def set_search_user(self, search_user): + """ + + :param search_user: + :return: + """ + self.search_user = search_user + self.view.set_search_user(search_user.view) + search_user.identity_selected.connect(self.refresh_user_information) + + def set_user_information(self, user_information): + """ + + :param user_information: + :return: + """ + self.user_information = user_information + self.view.set_user_information(user_information.view) + self.user_information.identity_loaded.connect(self.refresh) + + @asyncify + async def accept(self): + """ + Validate the dialog + """ + self.view.button_box.setDisabled(True) + password = await PasswordAskerDialog(self.model.connection).async_exec() + if not password: + self.view.button_box.setEnabled(True) + return + QApplication.setOverrideCursor(Qt.WaitCursor) + result = await self.model.certify_identity(password, self.user_information.model.identity) + + if result[0]: + QApplication.restoreOverrideCursor() + await self.view.show_success(self.model.notification()) + self.view.accept() + else: + await self.view.show_error(self.model.notification(), result[1]) + QApplication.restoreOverrideCursor() + self.view.button_box.setEnabled(True) + + @asyncify + async def reject(self): + self.view.reject() + + def refresh(self): + stock = self.model.get_cert_stock() + written, pending = self.model.nb_certifications() + days, hours, minutes, seconds = self.model.remaining_time() + self.view.display_cert_stock(written, pending, stock, days, hours, minutes) + + if self.model.could_certify(): + if written < stock or stock == 0: + if not self.user_information.model.identity: + self.view.set_button_box(CertificationView.ButtonBoxState.SELECT_IDENTITY) + elif days+hours+minutes > 0: + if days > 0: + remaining_localized = self.tr("{days} days").format(days=days) + else: + remaining_localized = self.tr("{hours}h {min}min").format(hours=hours, min=minutes) + self.view.set_button_box(CertificationView.ButtonBoxState.REMAINING_TIME_BEFORE_VALIDATION, + remaining=remaining_localized) + else: + self.view.set_button_box(CertificationView.ButtonBoxState.OK) + else: + self.view.set_button_box(CertificationView.ButtonBoxState.NO_MORE_CERTIFICATION) + else: + self.view.set_button_box(CertificationView.ButtonBoxState.NOT_A_MEMBER) + + def refresh_user_information(self): + """ + Refresh user information + """ + self.user_information.search_identity(self.search_user.model.identity()) + + def change_connection(self, index): + self.model.set_connection(index) + self.search_user.set_currency(self.model.connection.currency) + self.user_information.set_currency(self.model.connection.currency) + self.refresh() + + def async_exec(self): + future = asyncio.Future() + self.view.finished.connect(lambda r: future.set_result(r)) + self.view.open() + self.refresh() + return future + + def exec(self): + self.refresh() + self.view.exec() diff --git a/src/sakia/gui/dialogs/certification/model.py b/src/sakia/gui/dialogs/certification/model.py new file mode 100644 index 0000000000000000000000000000000000000000..d3b5a6bda0f8fa70ae42159a40b6c1cf2c85f4b6 --- /dev/null +++ b/src/sakia/gui/dialogs/certification/model.py @@ -0,0 +1,94 @@ +from PyQt5.QtCore import QObject +from sakia.data.processors import IdentitiesProcessor, CertificationsProcessor, \ + BlockchainProcessor, ConnectionsProcessor +import attr + + +@attr.s() +class CertificationModel(QObject): + """ + The model of Certification component + """ + + app = attr.ib() + connection = attr.ib(default=None) + _connections_processor = attr.ib(default=None) + _certifications_processor = attr.ib(default=None) + _identities_processor = attr.ib(default=None) + _blockchain_processor = attr.ib(default=None) + + def __attrs_post_init__(self): + super().__init__() + self._connections_processor = ConnectionsProcessor.instanciate(self.app) + self._certifications_processor = CertificationsProcessor.instanciate(self.app) + self._identities_processor = IdentitiesProcessor.instanciate(self.app) + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) + + def change_connection(self, index): + """ + Change current currency + :param int index: index of the community in the account list + """ + self.connection = self.connections_repo.get_currencies()[index] + + def get_cert_stock(self): + """ + + :return: the certifications stock + :rtype: int + """ + return self._blockchain_processor.parameters(self.connection.currency).sig_stock + + def remaining_time(self): + """ + Get remaining time as a tuple to display + :return: a tuple containing (days, hours, minutes, seconds) + :rtype: tuple[int] + """ + parameters = self._blockchain_processor.parameters(self.connection.currency) + blockchain_time = self._blockchain_processor.time(self.connection.currency) + remaining_time = self._certifications_processor.cert_issuance_delay(self.connection.currency, + self.connection.pubkey, + parameters, blockchain_time) + + days, remainder = divmod(remaining_time, 3600 * 24) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + return days, hours, minutes, seconds + + def nb_certifications(self): + """ + Get + :return: a tuple containing (written valid certifications, pending certifications) + :rtype: tuple[int] + """ + certifications = self._certifications_processor.certifications_sent(self.connection.currency, + self.connection.pubkey) + nb_certifications = len([c for c in certifications if c.written_on]) + nb_cert_pending = len([c for c in certifications if not c.written_on]) + return nb_certifications, nb_cert_pending + + def could_certify(self): + """ + Check if the user could theorically certify + :return: true if the user can certifiy + :rtype: bool + """ + is_member = self._identities_processor.get_identity(self.connection.currency, + self.connection.pubkey, + self.connection.pubkey) + + return is_member and self._blockchain_processor.current_buid(self.connection.currency) + + def available_connections(self): + return self._connections_processor.connections() + + def set_connection(self, index): + connections = self._connections_processor.connections() + self.connection = connections[index] + + def notification(self): + return self.app.parameters.notifications + + async def certify_identity(self, password, identity): + return await self.app.documents_service.certify(self.connection, password, identity) diff --git a/src/sakia/gui/dialogs/certification/view.py b/src/sakia/gui/dialogs/certification/view.py new file mode 100644 index 0000000000000000000000000000000000000000..1ac5dc8a7ad7fc44a72555624b75397ed878afa7 --- /dev/null +++ b/src/sakia/gui/dialogs/certification/view.py @@ -0,0 +1,127 @@ +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox +from PyQt5.QtCore import QT_TRANSLATE_NOOP, pyqtSignal +from .certification_uic import Ui_CertificationDialog +from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox +from enum import Enum + + +class CertificationView(QDialog, Ui_CertificationDialog): + """ + The view of the certification component + """ + + class ButtonBoxState(Enum): + NO_MORE_CERTIFICATION = 0 + NOT_A_MEMBER = 1 + REMAINING_TIME_BEFORE_VALIDATION = 2 + OK = 3 + SELECT_IDENTITY = 4 + + _button_box_values = { + ButtonBoxState.NO_MORE_CERTIFICATION: (False, + QT_TRANSLATE_NOOP("CertificationView", "No more certifications")), + ButtonBoxState.NOT_A_MEMBER: (False, QT_TRANSLATE_NOOP("CertificationView", "Not a member")), + ButtonBoxState.SELECT_IDENTITY: (False, QT_TRANSLATE_NOOP("CertificationView", "Please select an identity")), + ButtonBoxState.REMAINING_TIME_BEFORE_VALIDATION: (True, + QT_TRANSLATE_NOOP("CertificationView", + "&Ok (Not validated before {remaining})")), + ButtonBoxState.OK: (True, QT_TRANSLATE_NOOP("CertificationView", "&Ok")) + } + + def __init__(self, parent, search_user_view, user_information_view): + """ + + :param parent: + :param sakia.gui.search_user.view.SearchUserView search_user_view: + :param sakia.gui.user_information.view.UserInformationView user_information_view: + :param list[sakia.data.entities.Connection] connections: + """ + super().__init__(parent) + self.setupUi(self) + + self.search_user = search_user_view + self.user_information_view = user_information_view + + def set_keys(self, connections): + self.combo_pubkey.clear() + for c in connections: + self.combo_pubkey.addItem(c.title()) + + def set_selected_key(self, connection): + """ + :param sakia.data.entities.Connection connection: + """ + self.combo_pubkey.setCurrentText(connection.title()) + + def set_search_user(self, search_user_view): + """ + + :param sakia.gui.search_user.view.SearchUserView search_user_view: + :return: + """ + self.search_user = search_user_view + self.groupbox_certified.layout().addWidget(search_user_view) + self.search_user.button_reset.hide() + + def set_user_information(self, user_information_view): + self.user_information_view = user_information_view + self.groupbox_certified.layout().addWidget(user_information_view) + + def pubkey_value(self): + return self.edit_pubkey.text() + + async def show_success(self, notification): + if notification: + toast.display(self.tr("Certification"), + self.tr("Success sending certification")) + else: + await QAsyncMessageBox.information(self.widget, self.tr("Certification"), + self.tr("Success sending certification")) + + async def show_error(self, notification, error_txt): + + if notification: + toast.display(self.tr("Certification"), self.tr("Could not broadcast certification : {0}" + .format(error_txt))) + else: + await QAsyncMessageBox.critical(self.widget, self.tr("Certification"), + self.tr("Could not broadcast certification : {0}" + .format(error_txt))) + + def display_cert_stock(self, written, pending, stock, + remaining_days, remaining_hours, remaining_minutes): + """ + Display values in informations label + :param int written: number of written certifications + :param int pending: number of pending certifications + :param int stock: maximum certifications + :param int remaining_days: + :param int remaining_hours: + :param int remaining_minutes: + """ + cert_text = self.tr("Certifications sent : {nb_certifications}/{stock}").format( + nb_certifications=written, + stock=stock) + if pending > 0: + cert_text += " (+{nb_cert_pending} certifications pending)".format(nb_cert_pending=pending) + + if remaining_days > 0: + remaining_localized = self.tr("{days} days").format(days=remaining_days) + else: + remaining_localized = self.tr("{hours} hours and {min} min.").format(hours=remaining_hours, + min=remaining_minutes) + cert_text += "\n" + cert_text += self.tr("Remaining time before next certification validation : {0}".format(remaining_localized)) + self.label_cert_stock.setText(cert_text) + + def set_button_box(self, state, **kwargs): + """ + Set button box state + :param sakia.gui.certification.view.CertificationView.ButtonBoxState state: the state of te button box + :param dict kwargs: the values to replace from the text in the state + :return: + """ + button_box_state = CertificationView._button_box_values[state] + self.button_box.button(QDialogButtonBox.Ok).setEnabled(button_box_state[0]) + self.button_box.button(QDialogButtonBox.Ok).setText(button_box_state[1].format(**kwargs)) diff --git a/src/sakia/gui/dialogs/connection_cfg/__init__.py b/src/sakia/gui/dialogs/connection_cfg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..28a47ef45ea3b576216d2009ecd3fd877d27b4f0 --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/__init__.py @@ -0,0 +1 @@ +from .controller import ConnectionConfigController \ No newline at end of file diff --git a/src/sakia/gui/dialogs/connection_cfg/connection_cfg.ui b/src/sakia/gui/dialogs/connection_cfg/connection_cfg.ui new file mode 100644 index 0000000000000000000000000000000000000000..a70c6198cab5d72587155461fde27ac00e9d70f2 --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/connection_cfg.ui @@ -0,0 +1,489 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ConnectionConfigurationDialog</class> + <widget class="QDialog" name="ConnectionConfigurationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>623</width> + <height>545</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add a connection</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QStackedWidget" name="stacked_pages"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="page_node"> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Please enter the address of a node :</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_10"> + <property name="rightMargin"> + <number>5</number> + </property> + <item> + <widget class="QLineEdit" name="edit_server"/> + </item> + <item> + <widget class="QLabel" name="label_double_dot"> + <property name="text"> + <string>:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spinbox_port"> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>8001</number> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="checkbox_secured"> + <property name="text"> + <string>SSL/TLS</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_13"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <widget class="QPushButton" name="button_register"> + <property name="text"> + <string>Register a new account</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_connect"> + <property name="text"> + <string>Connect with an existing account</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_wallet"> + <property name="text"> + <string>Connect a wallet</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/wallet_icon</normaloff>:/icons/wallet_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>50</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_8"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_connection"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Account parameters</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_action"> + <property name="text"> + <string>Account name (uid)</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_uid"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_delete"> + <property name="styleSheet"> + <string notr="true">color: rgb(255, 0, 0);</string> + </property> + <property name="text"> + <string>Delete account</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Key parameters</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"/> + </item> + <item> + <widget class="QLineEdit" name="edit_salt"> + <property name="text"> + <string/> + </property> + <property name="placeholderText"> + <string>Secret key</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_salt_bis"> + <property name="placeholderText"> + <string>Please repeat your secret key</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_password"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Your password</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_password_repeat"> + <property name="text"> + <string/> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Please repeat your password</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Scrypt parameters</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="combo_scrypt_params"> + <item> + <property name="text"> + <string>Simple</string> + </property> + </item> + <item> + <property name="text"> + <string>Secure</string> + </property> + </item> + <item> + <property name="text"> + <string>Hardest</string> + </property> + </item> + <item> + <property name="text"> + <string>Extreme</string> + </property> + </item> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>N :</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spin_n"/> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>r :</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spin_r"/> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>p :</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spin_p"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <property name="topMargin"> + <number>5</number> + </property> + <item> + <widget class="QPushButton" name="button_generate"> + <property name="text"> + <string>Show public key</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <property name="topMargin"> + <number>5</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_next"> + <property name="text"> + <string>Next</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_services"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QProgressBar" name="progress_bar"> + <property name="value"> + <number>24</number> + </property> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="plain_text_edit"/> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_currency"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_info"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../res/icons/icons.qrc"/> + </resources> + <connections/> + <slots> + <slot>open_process_add_community()</slot> + <slot>key_changed(int)</slot> + <slot>action_remove_community()</slot> + <slot>open_process_edit_community(QModelIndex)</slot> + <slot>next()</slot> + <slot>previous()</slot> + <slot>open_import_key()</slot> + <slot>open_generate_account_key()</slot> + <slot>action_edit_account_key()</slot> + <slot>action_edit_account_parameters()</slot> + <slot>action_show_pubkey()</slot> + <slot>action_delete_account()</slot> + </slots> +</ui> diff --git a/src/sakia/gui/dialogs/connection_cfg/controller.py b/src/sakia/gui/dialogs/connection_cfg/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..fd0e056ecce8ec321d0c6e969391ad17f2cbfbe7 --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/controller.py @@ -0,0 +1,333 @@ +import asyncio +import logging + +from aiohttp.errors import DisconnectedError, ClientError, TimeoutError +from duniterpy.documents import MalformedDocumentError +from duniterpy.api.errors import DuniterError +from sakia.errors import NoPeerAvailable +from sakia.decorators import asyncify +from sakia.data.processors import IdentitiesProcessor, NodesProcessor +from sakia.data.connectors import BmaConnector +from sakia.gui.password_asker import PasswordAskerDialog, detect_non_printable +from .model import ConnectionConfigModel +from .view import ConnectionConfigView +from PyQt5.QtCore import QObject + + +class ConnectionConfigController(QObject): + """ + The AccountConfigController view + """ + + CONNECT = 0 + REGISTER = 1 + WALLET = 2 + + def __init__(self, parent, view, model): + """ + Constructor of the AccountConfigController component + + :param sakia.gui.dialogs.connection_cfg.view.ConnectionConfigView: the view + :param sakia.gui.dialogs.connection_cfg.model.ConnectionConfigView model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + + self.step_node = asyncio.Future() + self.step_key = asyncio.Future() + self.view.button_connect.clicked.connect( + lambda: self.step_node.set_result(ConnectionConfigController.CONNECT)) + self.view.button_register.clicked.connect( + lambda: self.step_node.set_result(ConnectionConfigController.REGISTER)) + self.view.button_wallet.clicked.connect( + lambda: self.step_node.set_result(ConnectionConfigController.WALLET)) + self.password_asker = None + self.view.values_changed.connect(lambda: self.view.button_next.setEnabled(self.check_key())) + self.view.values_changed.connect(lambda: self.view.button_generate.setEnabled(self.check_key())) + self._logger = logging.getLogger('sakia') + + @classmethod + def create(cls, parent, app): + """ + Instanciate a AccountConfigController component + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.app.Application app: + :return: a new AccountConfigController controller + :rtype: AccountConfigController + """ + view = ConnectionConfigView(parent.view if parent else None) + model = ConnectionConfigModel(None, app, None, + IdentitiesProcessor(app.db.identities_repo, app.db.blockchains_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo), + app.parameters))) + account_cfg = cls(parent, view, model) + model.setParent(account_cfg) + return account_cfg + + @classmethod + def create_connection(cls, parent, app): + """ + Open a dialog to create a new account + :param parent: + :param app: + :return: + """ + connection_cfg = cls.create(parent, app) + connection_cfg.view.set_creation_layout() + asyncio.ensure_future(connection_cfg.process()) + return connection_cfg + + @classmethod + def modify_connection(cls, parent, app, connection): + """ + Open a dialog to modify an existing account + :param parent: + :param app: + :param account: + :return: + """ + connection_cfg = cls.create(parent, app, connection=connection) + #connection_cfg.view.set_modification_layout(account.name) + connection_cfg._current_step = 1 + + def init_nodes_page(self): + self.view.set_steps_buttons_visible(True) + model = self.model.init_nodes_model() + self.view.tree_peers.customContextMenuRequested(self.show_context_menu) + + self.view.set_nodes_model(model) + self.view.button_previous.setEnabled(False) + self.view.button_next.setText(self.config_dialog.tr("Ok")) + + def init_name_page(self): + """ + Initialize an account name page + """ + if self.model.connection: + self.view.set_account_name(self.model.connection.uid) + + self.view.button_previous.setEnabled(False) + self.view.button_next.setEnabled(False) + + def check_name(self): + return len(self.view.edit_account_name.text()) > 2 + + async def process(self): + self._logger.debug("Begin process") + if self.model.connection: + mode = await self.step_node + else: + while not self.model.connection: + mode = await self.step_node + self._logger.debug("Create connection") + try: + self.view.button_connect.setEnabled(False) + self.view.button_register.setEnabled(False) + await self.model.create_connection(self.view.edit_server.text(), + self.view.spinbox_port.value(), + self.view.checkbox_secured.isChecked()) + self.password_asker = PasswordAskerDialog(self.model.connection) + except (DisconnectedError, ClientError, MalformedDocumentError, ValueError, TimeoutError) as e: + self._logger.debug(str(e)) + self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port : <br/>" + + str(e))) + self.step_node = asyncio.Future() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + + self._logger.debug("Key step") + self.view.set_currency(self.model.connection.currency) + if mode == ConnectionConfigController.REGISTER: + self._logger.debug("Registering mode") + self.view.button_next.clicked.connect(self.check_register) + self.view.stacked_pages.setCurrentWidget(self.view.page_connection) + connection_identity = await self.step_key + elif mode == ConnectionConfigController.CONNECT: + self._logger.debug("Connect mode") + self.view.button_next.clicked.connect(self.check_connect) + self.view.stacked_pages.setCurrentWidget(self.view.page_connection) + connection_identity = await self.step_key + elif mode == ConnectionConfigController.WALLET: + self._logger.debug("Wallet mode") + self.view.button_next.clicked.connect(self.check_wallet) + self.view.edit_uid.hide() + self.view.stacked_pages.setCurrentWidget(self.view.page_connection) + connection_identity = await self.step_key + + self.model.insert_or_update_connection() + self.view.stacked_pages.setCurrentWidget(self.view.page_services) + self.view.progress_bar.setValue(0) + self.view.progress_bar.setMaximum(3) + try: + await self.model.initialize_blockchain(self.view.stream_log) + self.view.progress_bar.setValue(1) + + if mode == ConnectionConfigController.REGISTER: + self.view.display_info(self.tr("Broadcasting identity...")) + self.view.stream_log("Broadcasting identity...") + result, connection_identity = await self.model.publish_selfcert() + if result[0]: + await self.view.show_success(self.model.notification()) + else: + self.view.show_error(self.model.notification(), result[1]) + + self.view.progress_bar.setValue(2) + + if mode in (ConnectionConfigController.REGISTER, ConnectionConfigController.CONNECT): + self.view.stream_log("Saving identity...") + self.model.connection.blockstamp = connection_identity.blockstamp + self.model.insert_or_update_connection() + self.model.insert_or_update_identity(connection_identity) + self.view.stream_log("Initializing identity informations...") + await self.model.initialize_identity(connection_identity, log_stream=self.view.stream_log) + self.view.stream_log("Initializing certifications informations...") + await self.model.initialize_certifications(connection_identity, log_stream=self.view.stream_log) + + if mode in (ConnectionConfigController.REGISTER, + ConnectionConfigController.CONNECT, + ConnectionConfigController.WALLET): + self.view.stream_log("Initializing transactions history...") + transactions = await self.model.initialize_transactions(self.model.connection, log_stream=self.view.stream_log) + self.view.stream_log("Initializing dividends history...") + await self.model.initialize_dividends(self.model.connection, transactions, log_stream=self.view.stream_log) + + self.view.progress_bar.setValue(3) + await self.model.initialize_sources(self.view.stream_log) + + self._logger.debug("Validate changes") + self.model.app.db.commit() + if self.model.node_connector: + await self.model.node_connector.session.close() + except (NoPeerAvailable, DuniterError) as e: + raise + self._logger.debug(str(e)) + self.view.stacked_pages.setCurrentWidget(self.view.page_connection) + self.step_node = asyncio.Future() + self.step_node.set_result(mode) + self.step_key = asyncio.Future() + self.view.button_next.disconnect() + self.view.edit_uid.show() + asyncio.ensure_future(self.process()) + return + self.accept() + + def check_key(self): + if self.model.app.parameters.expert_mode: + return True + + if len(self.view.edit_salt.text()) < 6: + self.view.label_info.setText(self.tr("Forbidden : salt is too short")) + return False + + if len(self.view.edit_password.text()) < 6: + self.view.label_info.setText(self.tr("Forbidden : password is too short")) + return False + + if detect_non_printable(self.view.edit_salt.text()): + self.view.label_info.setText(self.tr("Forbidden : Invalid characters in salt field")) + return False + + if detect_non_printable(self.view.edit_password.text()): + self.view.label_info.setText( + self.tr("Forbidden : Invalid characters in password field")) + return False + + if self.view.edit_password.text() != \ + self.view.edit_password_repeat.text(): + self.view.label_info.setText(self.tr("Error : passwords are different")) + return False + + if self.view.edit_salt.text() != \ + self.view.edit_salt_bis.text(): + self.view.label_info.setText(self.tr("Error : secret keys are different")) + return False + + self.view.label_info.setText("") + return True + + @asyncify + async def check_wallet(self, checked=False): + self._logger.debug("Is valid ? ") + self.view.display_info(self.tr("connecting...")) + try: + salt = self.view.edit_salt.text() + password = self.view.edit_password.text() + self.model.set_scrypt_infos(salt, password, self.view.scrypt_params) + self.model.set_uid("") + if not self.model.key_exists(): + registered, found_identity = await self.model.check_registered() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + self.step_key.set_result(None) + elif registered[2]: + self.view.display_info(self.tr("""Your pubkey is associated to a pubkey. + Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.view.display_info(self.tr("A connection already exists using this key.")) + + except NoPeerAvailable: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) + + @asyncify + async def check_connect(self, checked=False): + self._logger.debug("Is valid ? ") + self.view.display_info(self.tr("connecting...")) + try: + salt = self.view.edit_salt.text() + password = self.view.edit_password.text() + self.model.set_scrypt_infos(salt, password, self.view.scrypt_params) + self.model.set_uid(self.view.edit_uid.text()) + if not self.model.key_exists(): + registered, found_identity = await self.model.check_registered() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + self.view.display_info(self.tr("Could not find your identity on the network.")) + elif registered[0] is False and registered[2]: + self.view.display_info(self.tr("""Your pubkey or UID is different on the network. + Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.step_key.set_result(found_identity) + else: + self.view.display_info(self.tr("A connection already exists using this key.")) + + except NoPeerAvailable: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) + + @asyncify + async def check_register(self, checked=False): + self._logger.debug("Is valid ? ") + self.view.display_info(self.tr("connecting...")) + try: + salt = self.view.edit_salt.text() + password = self.view.edit_password.text() + self.model.set_scrypt_infos(salt, password, self.view.scrypt_params) + self.model.set_uid(self.view.edit_uid.text()) + if not self.model.key_exists(): + registered, found_identity = await self.model.check_registered() + if registered[0] is False and registered[2] is None: + self.step_key.set_result(None) + elif registered[0] is False and registered[2]: + self.view.display_info(self.tr("""Your pubkey or UID was already found on the network. + Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.view.display_info("Your account already exists on the network") + else: + self.view.display_info(self.tr("A connection already exists using this key.")) + except NoPeerAvailable: + self.view.display_info(self.tr("Could not connect. Check node peering entry")) + + @asyncify + async def accept(self): + self.view.accept() + + def async_exec(self): + future = asyncio.Future() + self.view.finished.connect(lambda r: future.set_result(r)) + self.view.open() + return future + + def exec(self): + return self.view.exec() \ No newline at end of file diff --git a/src/sakia/gui/dialogs/connection_cfg/model.py b/src/sakia/gui/dialogs/connection_cfg/model.py new file mode 100644 index 0000000000000000000000000000000000000000..c1a7c3b9b6c054bc7c6e6368a0956fce729eb8f7 --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/model.py @@ -0,0 +1,206 @@ +import aiohttp +from PyQt5.QtCore import QObject +from duniterpy.documents import BlockUID, BMAEndpoint, SecuredBMAEndpoint +from duniterpy.api import bma, errors +from duniterpy.key import SigningKey +from sakia.data.entities import Connection, Identity, Node +from sakia.data.connectors import NodeConnector +from sakia.data.processors import ConnectionsProcessor, NodesProcessor, BlockchainProcessor, \ + SourcesProcessor, CertificationsProcessor, TransactionsProcessor, DividendsProcessor + + +class ConnectionConfigModel(QObject): + """ + The model of AccountConfig component + """ + + def __init__(self, parent, app, connection, identities_processor, node_connector=None): + """ + + :param sakia.gui.dialogs.account_cfg.controller.AccountConfigController parent: + :param sakia.app.Application app: the main application + :param sakia.data.entities.Connection connection: the connection + :param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor + :param sakia.data.connectors.NodeConnector node_connector: the node connector + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.node_connector = node_connector + self.identities_processor = identities_processor + + async def create_connection(self, server, port, secured): + node_connector = await NodeConnector.from_address(None, secured, server, port, + user_parameters=self.app.parameters) + currencies = self.app.db.connections_repo.get_currencies() + if len(currencies) > 0 and node_connector.node.currency != currencies[0]: + raise ValueError("""This node is running for {0} network.<br/> +Current database is storing {1} network.""".format(node_connector.node.currency, currencies[0])) + self.node_connector = node_connector + self.connection = Connection(self.node_connector.node.currency, "", "") + self.node_connector.node.state = Node.ONLINE + + def notification(self): + return self.app.parameters.notifications + + def set_uid(self, uid): + self.connection.uid = uid + + def set_scrypt_infos(self, salt, password, scrypt_params): + self.connection.salt = salt + self.connection.N = scrypt_params.N + self.connection.r = scrypt_params.r + self.connection.p = scrypt_params.p + self.connection.password = password + self.connection.pubkey = SigningKey(self.connection.salt, password, scrypt_params).pubkey + + def insert_or_update_connection(self): + ConnectionsProcessor(self.app.db.connections_repo).commit_connection(self.connection) + NodesProcessor(self.app.db.nodes_repo).commit_node(self.node_connector.node) + + def insert_or_update_identity(self, identity): + self.identities_processor.insert_or_update_identity(identity) + + async def initialize_blockchain(self, log_stream): + """ + Download blockchain information locally + :param function log_stream: a method to log data in the screen + :return: + """ + blockchain_processor = BlockchainProcessor.instanciate(self.app) + await blockchain_processor.initialize_blockchain(self.node_connector.node.currency, log_stream) + + async def initialize_sources(self, log_stream): + """ + Download sources information locally + :param function log_stream: a method to log data in the screen + :return: + """ + sources_processor = SourcesProcessor.instanciate(self.app) + await sources_processor.initialize_sources(self.node_connector.node.currency, self.connection.pubkey, log_stream) + + async def initialize_identity(self, identity, log_stream): + """ + Download identity information locally + :param sakia.data.entities.Identity identity: the identity to initialize + :param function log_stream: a method to log data in the screen + :return: + """ + await self.identities_processor.initialize_identity(identity, log_stream) + + async def initialize_certifications(self, identity, log_stream): + """ + Download certifications information locally + :param sakia.data.entities.Identity identity: the identity to initialize + :param function log_stream: a method to log data in the screen + :return: + """ + certifications_processor = CertificationsProcessor.instanciate(self.app) + await certifications_processor.initialize_certifications(identity, log_stream) + + async def initialize_transactions(self, identity, log_stream): + """ + Download certifications information locally + :param sakia.data.entities.Identity identity: the identity to initialize + :param function log_stream: a method to log data in the screen + :return: + """ + transactions_processor = TransactionsProcessor.instanciate(self.app) + return await transactions_processor.initialize_transactions(identity, log_stream) + + async def initialize_dividends(self, identity, transactions, log_stream): + """ + Download certifications information locally + :param sakia.data.entities.Identity identity: the identity to initialize + :param List[sakia.data.entities.Transaction] transactions: the list of transactions found by tx processor + :param function log_stream: a method to log data in the screen + :return: + """ + dividends_processor = DividendsProcessor.instanciate(self.app) + return await dividends_processor.initialize_dividends(identity, transactions, log_stream) + + async def publish_selfcert(self): + """" + Publish the self certification of the connection identity + """ + return await self.app.documents_service.broadcast_identity(self.connection, self.connection.password) + + async def check_registered(self): + """ + Checks for the pubkey and the uid of an account on a given node + :return: (True if found, local value, network value) + """ + identity = Identity(self.connection.currency, self.connection.pubkey, self.connection.uid) + found_identity = Identity(self.connection.currency, self.connection.pubkey, self.connection.uid) + + def _parse_uid_lookup(data): + timestamp = BlockUID.empty() + found_uid = "" + for result in data['results']: + if result["pubkey"] == identity.pubkey: + uids = result['uids'] + for uid_data in uids: + if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: + timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) + found_identity.blockstamp = timestamp + found_uid = uid_data["uid"] + found_identity.signature = uid_data["self"] + return identity.uid == found_uid, identity.uid, found_uid + + def _parse_pubkey_lookup(data): + timestamp = BlockUID.empty() + found_uid = "" + found_result = ["", ""] + for result in data['results']: + uids = result['uids'] + for uid_data in uids: + if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: + timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) + found_identity.blockstamp = timestamp + found_uid = uid_data["uid"] + found_identity.signature = uid_data["self"] + if found_uid == identity.uid: + found_result = result['pubkey'], found_uid + if found_result[1] == identity.uid: + return identity.pubkey == found_result[0], identity.pubkey, found_result[0] + else: + return False, identity.pubkey, None + + async def execute_requests(parser, search): + tries = 0 + nonlocal registered + for endpoint in [e for e in self.node_connector.node.endpoints + if isinstance(e, BMAEndpoint) or isinstance(e, SecuredBMAEndpoint)]: + if not registered[0] and not registered[2]: + try: + data = await self.node_connector.safe_request(endpoint, bma.wot.lookup, + req_args={'search': search}, + proxy=self.app.parameters.proxy()) + if data: + registered = parser(data) + tries += 1 + except errors.DuniterError as e: + if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, errors.NO_MATCHING_IDENTITY): + tries += 1 + else: + raise + else: + break + + # cell 0 contains True if the user is already registered + # cell 1 contains the uid/pubkey selected locally + # cell 2 contains the uid/pubkey found on the network + registered = (False, identity.uid, None) + # We execute search based on pubkey + # And look for account UID + await execute_requests(_parse_uid_lookup, identity.pubkey) + + # If the uid wasn't found when looking for the pubkey + # We look for the uid and check for the pubkey + if not registered[0] and not registered[2]: + await execute_requests(_parse_pubkey_lookup, identity.uid) + + return registered, found_identity + + def key_exists(self): + return self.connection.pubkey in ConnectionsProcessor.instanciate(self.app).pubkeys() diff --git a/src/sakia/gui/dialogs/connection_cfg/view.py b/src/sakia/gui/dialogs/connection_cfg/view.py new file mode 100644 index 0000000000000000000000000000000000000000..45824214fcecfea1c04376f6a12f7ccac2e4fe09 --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/view.py @@ -0,0 +1,146 @@ +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import pyqtSignal, Qt +from .connection_cfg_uic import Ui_ConnectionConfigurationDialog +from duniterpy.key import SigningKey, ScryptParams +from math import ceil, log +from ...widgets import toast +from ...widgets.dialogs import QAsyncMessageBox + + +class ConnectionConfigView(QDialog, Ui_ConnectionConfigurationDialog): + """ + Connection config view + """ + values_changed = pyqtSignal() + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + self.setupUi(self) + self.edit_uid.textChanged.connect(self.values_changed) + self.edit_password.textChanged.connect(self.values_changed) + self.edit_password_repeat.textChanged.connect(self.values_changed) + self.edit_salt.textChanged.connect(self.values_changed) + self.button_generate.clicked.connect(self.action_show_pubkey) + + self.combo_scrypt_params.currentIndexChanged.connect(self.handle_combo_change) + self.scrypt_params = ScryptParams(4096, 16, 1) + self.spin_n.setMaximum(2 ** 20) + self.spin_n.setValue(self.scrypt_params.N) + self.spin_n.valueChanged.connect(self.handle_n_change) + self.spin_r.setMaximum(128) + self.spin_r.setValue(self.scrypt_params.r) + self.spin_r.valueChanged.connect(self.handle_r_change) + self.spin_p.setMaximum(128) + self.spin_p.setValue(self.scrypt_params.p) + self.spin_p.valueChanged.connect(self.handle_p_change) + self.label_info.setTextFormat(Qt.RichText) + + def handle_combo_change(self, index): + strengths = [ + (2 ** 12, 16, 1), + (2 ** 14, 32, 2), + (2 ** 16, 32, 4), + (2 ** 18, 64, 8), + ] + self.spin_n.setValue(strengths[index][0]) + self.spin_r.setValue(strengths[index][1]) + self.spin_p.setValue(strengths[index][2]) + + def handle_n_change(self, value): + spinbox = self.sender() + self.scrypt_params.N = ConnectionConfigView.compute_power_of_2(spinbox, value, self.scrypt_params.N) + + def handle_r_change(self, value): + spinbox = self.sender() + self.scrypt_params.r = ConnectionConfigView.compute_power_of_2(spinbox, value, self.scrypt_params.r) + + def handle_p_change(self, value): + spinbox = self.sender() + self.scrypt_params.p = ConnectionConfigView.compute_power_of_2(spinbox, value, self.scrypt_params.p) + + @staticmethod + def compute_power_of_2(spinbox, value, param): + if value > 1: + if value > param: + value = pow(2, ceil(log(value) / log(2))) + else: + value -= 1 + value = 2 ** int(log(value, 2)) + else: + value = 1 + + spinbox.blockSignals(True) + spinbox.setValue(value) + spinbox.blockSignals(False) + + return value + + def display_info(self, info): + self.label_info.setText(info) + + def set_currency(self, currency): + self.label_currency.setText(currency) + + def add_node_parameters(self): + server = self.lineedit_add_address.text() + port = self.spinbox_add_port.value() + return server, port + + async def show_success(self, notification): + if notification: + toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) + else: + await QAsyncMessageBox.information(self, self.tr("UID broadcast"), + self.tr("Identity broadcasted to the network")) + + def show_error(self, notification, error_txt): + if notification: + toast.display(self.tr("UID broadcast"), error_txt) + self.label_info.setText(self.tr("Error") + " " + error_txt) + + def set_nodes_model(self, model): + self.tree_peers.setModel(model) + + def set_creation_layout(self): + """ + Hide unecessary buttons and display correct title + """ + self.setWindowTitle(self.tr("New account")) + self.button_delete.hide() + + def set_modification_layout(self, account_name): + """ + Hide unecessary widgets for account modification + and display correct title + :return: + """ + self.label_action.setText("Edit account uid") + self.edit_account_name.setPlaceholderText(account_name) + self.button_next.setEnabled(True) + self.setWindowTitle(self.tr("Configure {0}".format(account_name))) + + def action_show_pubkey(self): + salt = self.edit_salt.text() + password = self.edit_password.text() + pubkey = SigningKey(salt, password, self.scrypt_params).pubkey + self.label_info.setText(pubkey) + + def account_name(self): + return self.edit_account_name.text() + + def set_communities_list_model(self, model): + """ + Set communities list model + :param sakia.models.communities.CommunitiesListModel model: + """ + self.list_communities.setModel(model) + + def stream_log(self, log): + """ + Add log to + :param str log: + """ + self.plain_text_edit.insertPlainText("\n" + log) diff --git a/src/sakia/gui/graphs/__init__.py b/src/sakia/gui/dialogs/revocation/__init__.py similarity index 100% rename from src/sakia/gui/graphs/__init__.py rename to src/sakia/gui/dialogs/revocation/__init__.py diff --git a/src/sakia/gui/dialogs/revocation/controller.py b/src/sakia/gui/dialogs/revocation/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..23d5c3de4047505a58d0f3be174c755558ad080a --- /dev/null +++ b/src/sakia/gui/dialogs/revocation/controller.py @@ -0,0 +1,132 @@ +import asyncio + +from PyQt5.QtCore import QObject +from duniterpy.documents import MalformedDocumentError +from sakia.decorators import asyncify +from .model import RevocationModel +from .view import RevocationView + + +class RevocationController(QObject): + """ + The revocation view + """ + + def __init__(self, view, model): + """ + Constructor of the revocation component + + :param sakia.gui.dialogs.revocation.view.revocationView: the view + :param sakia.gui.dialogs.revocation.model.revocationModel model: the model + """ + super().__init__() + self.view = view + self.model = model + + self.view.button_next.clicked.connect(lambda checked: self.handle_next_step(False)) + self.view.button_load.clicked.connect(self.load_from_file) + self.view.spinbox_port.valueChanged.connect(self.refresh_revocation_info) + self.view.edit_address.textChanged.connect(self.refresh_revocation_info) + self.view.radio_address.toggled.connect(self.refresh_revocation_info) + self.view.radio_currency.toggled.connect(self.refresh_revocation_info) + + self._steps = ( + { + 'page': self.view.page_load_file, + 'init': lambda: None, + 'next': lambda: None + }, + { + 'page': self.view.page_destination, + 'init': self.init_publication_page, + 'next': self.publish + } + ) + self._current_step = 0 + + @classmethod + def create(cls, parent, app, connection): + """ + Instanciate a revocation component + :param sakia.app.Application app: + :return: a new revocation controller + :rtype: revocationController + """ + view = RevocationView(parent.view) + model = RevocationModel(app, connection) + revocation = cls(view, model) + return revocation + + @classmethod + def open_dialog(cls, parent, app, connection): + """ + Certify and identity + :param sakia.gui.component.controller.ComponentController parent: the parent + :param sakia.app.Application app: the application + :param sakia.data.entities.Connection connection: the connection certifying the identity + :return: + """ + dialog = cls.create(parent, app, connection=connection) + dialog.handle_next_step(init=True) + return dialog.exec() + + def handle_next_step(self, init=False): + if self._current_step < len(self._steps) - 1: + if not init: + self.view.button_next.clicked.disconnect(self._steps[self._current_step]['next']) + self._current_step += 1 + self._steps[self._current_step]['init']() + self.view.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page']) + self.view.button_next.clicked.connect(self._steps[self._current_step]['next']) + + def load_from_file(self): + selected_file = self.view.select_revocation_file() + try: + self.model.load_revocation(selected_file) + self.view.show_revoked_selfcert(self.model.revoked_identity) + self.view.button_next.setEnabled(True) + except FileNotFoundError: + pass + except MalformedDocumentError: + self.view.malformed_file_error() + self.button_next.setEnabled(False) + + def refresh_revocation_info(self): + self.view.refresh_revocation_label(self.model.revoked_identity) + + def init_publication_page(self): + communities_names = self.model.currencies_names() + self.view.set_currencies_names(communities_names) + + def publish(self): + self.view.button_next.setEnabled(False) + if self.view.ask_for_confirmation(): + self.accept() + else: + self.view.button_next.setEnabled(True) + + @asyncify + async def accept(self): + if self.view.radio_currency.isChecked(): + index = self.view.combo_currency.currentIndex() + result, error = await self.model.broadcast_to_network(index) + else: + server = self.view.edit_address.text() + port = self.view.spinbox_port.value() + secured = self.view.checkbox_secured.isChecked() + result, error = await self.model.send_to_node(server, port, secured) + + if result: + self.view.accept() + else: + await self.view.revocation_broadcast_error(error) + + def async_exec(self): + future = asyncio.Future() + self.view.finished.connect(lambda r: future.set_result(r)) + self.view.open() + self.refresh() + return future + + def exec(self): + self.view.exec() diff --git a/src/sakia/gui/dialogs/revocation/model.py b/src/sakia/gui/dialogs/revocation/model.py new file mode 100644 index 0000000000000000000000000000000000000000..519c63220506eecc85ee8f476f5fee82bc71cbe1 --- /dev/null +++ b/src/sakia/gui/dialogs/revocation/model.py @@ -0,0 +1,64 @@ +from duniterpy.documents import Revocation, BMAEndpoint, SecuredBMAEndpoint +from duniterpy.api import bma, errors +from sakia.data.connectors import NodeConnector +from asyncio import TimeoutError +from socket import gaierror +import jsonschema +from aiohttp.errors import ClientError, DisconnectedError +from aiohttp.errors import ClientResponseError +from PyQt5.QtCore import QObject +import aiohttp + + +class RevocationModel(QObject): + """ + The model of HomeScreen component + """ + + def __init__(self, app, connection): + super().__init__() + self.app = app + self.connection = connection + + self.revocation_document = None + self.revoked_identity = None + + def load_revocation(self, path): + """ + Load a revocation document from a file + :param str path: + """ + with open(path, 'r') as file: + file_content = file.read() + self.revocation_document = Revocation.from_signed_raw(file_content) + self.revoked_identity = Revocation.extract_self_cert(file_content) + + def currencies_names(self): + return [c for c in self.app.db.connections_repo.get_currencies()] + + async def broadcast_to_network(self, index): + currency = self.currencies_names()[index] + return await self.app.documents_service.broadcast_revocation(currency, self.revoked_identity, + self.revocation_document) + + async def send_to_node(self, server, port, secured): + signed_raw = self.revocation_document.signed_raw(self.revoked_identity) + node_connector = await NodeConnector.from_address(None, secured, server, port, self.app.parameters) + for endpoint in [e for e in node_connector.node.endpoints + if isinstance(e, BMAEndpoint) or isinstance(e, SecuredBMAEndpoint)]: + try: + self._logger.debug("Broadcasting : \n" + signed_raw) + conn_handler = endpoint.conn_handler(node_connector.session, proxy=self.app.parameters.proxy()) + result = await bma.wot.revoke(conn_handler, signed_raw) + if result.status == 200: + return True, "" + else: + return False, bma.api.parse_error(await result.text())["message"] + except errors.DuniterError as e: + return False, e.message + except (jsonschema.ValidationError, ClientError, gaierror, + TimeoutError, ConnectionRefusedError, DisconnectedError, ValueError) as e: + return False, str(e) + finally: + node_connector.session.close() + return True, "" diff --git a/res/ui/revocation.ui b/src/sakia/gui/dialogs/revocation/revocation.ui similarity index 92% rename from res/ui/revocation.ui rename to src/sakia/gui/dialogs/revocation/revocation.ui index d7edc4990e334b82dbe71405c249084bd05ad234..7688bd3f38c2b87a16a3a63b9b88f124f37ecfbc 100644 --- a/res/ui/revocation.ui +++ b/src/sakia/gui/dialogs/revocation/revocation.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>250</height> + <width>452</width> + <height>358</height> </rect> </property> <property name="windowTitle"> @@ -103,14 +103,14 @@ QGroupBox::title { <item> <layout class="QHBoxLayout" name="horizontalLayout_2"> <item> - <widget class="QRadioButton" name="radio_community"> + <widget class="QRadioButton" name="radio_currency"> <property name="text"> <string>To a co&mmunity</string> </property> </widget> </item> <item> - <widget class="QComboBox" name="combo_community"/> + <widget class="QComboBox" name="combo_currency"/> </item> </layout> </item> @@ -139,6 +139,13 @@ QGroupBox::title { </property> </widget> </item> + <item> + <widget class="QCheckBox" name="checkbox_secured"> + <property name="text"> + <string>SSL/TLS</string> + </property> + </widget> + </item> </layout> </item> <item> @@ -166,6 +173,13 @@ QGroupBox::title { </property> </widget> </item> + <item> + <widget class="QLabel" name="label_target"> + <property name="text"> + <string/> + </property> + </widget> + </item> <item> <spacer name="verticalSpacer"> <property name="orientation"> diff --git a/src/sakia/gui/dialogs/revocation/view.py b/src/sakia/gui/dialogs/revocation/view.py new file mode 100644 index 0000000000000000000000000000000000000000..4483a3d8c07c6b7770e2ba9a240b6803a38930c0 --- /dev/null +++ b/src/sakia/gui/dialogs/revocation/view.py @@ -0,0 +1,130 @@ +from enum import Enum + +from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox + +from sakia.decorators import asyncify +from sakia.gui.widgets.dialogs import QAsyncMessageBox +from .revocation_uic import Ui_RevocationDialog + + +class RevocationView(QDialog, Ui_RevocationDialog): + """ + Home screen view + """ + + class PublicationMode(Enum): + ADDRESS = 0 + COMMUNITY = 1 + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + self.setupUi(self) + + self.button_next.setEnabled(False) + + self.radio_address.toggled.connect(lambda c: self.publication_mode_changed(RevocationView.PublicationMode.ADDRESS)) + self.radio_currency.toggled.connect(lambda c: self.publication_mode_changed(RevocationView.PublicationMode.COMMUNITY)) + + def publication_mode_changed(self, radio): + self.edit_address.setEnabled(radio == RevocationView.PublicationMode.ADDRESS) + self.spinbox_port.setEnabled(radio == RevocationView.PublicationMode.ADDRESS) + self.combo_currency.setEnabled(radio == RevocationView.PublicationMode.COMMUNITY) + + def refresh_target(self): + if self.radio_currency.isChecked(): + target = self.tr( + "All nodes of currency {name}".format(name=self.combo_currency.currentText())) + elif self.radio_address.isChecked(): + target = self.tr("Address {address}:{port}".format(address=self.edit_address.text(), + port=self.spinbox_port.value())) + else: + target = "" + self.label_target.setText(""" +<h4>Publication address</h4> +<div>{target}</div> +""".format(target=target)) + + def refresh_revocation_label(self, revoked_identity): + if revoked_identity: + text = self.tr(""" +<div>Identity revoked : {uid} (public key : {pubkey}...)</div> +<div>Identity signed on block : {timestamp}</div> + """.format(uid=revoked_identity.uid, + pubkey=revoked_identity.pubkey[:12], + timestamp=revoked_identity.timestamp)) + + self.label_revocation_content.setText(text) + + if self.radio_currency.isChecked(): + target = self.tr("All nodes of currency {name}".format(name=self.combo_currency.currentText())) + elif self.radio_address.isChecked(): + target = self.tr("Address {address}:{port}".format(address=self.edit_address.text(), + port=self.spinbox_port.value())) + else: + target = "" + self.label_revocation_info.setText(""" +<h4>Revocation document</h4> +<div>{text}</div> +<h4>Publication address</h4> +<div>{target}</div> +""".format(text=text, + target=target)) + else: + self.label_revocation_content.setText("") + + def select_revocation_file(self): + """ + Get a revocation file using a file dialog + :rtype: str + """ + selected_files = QFileDialog.getOpenFileName(self, + self.tr("Load a revocation file"), + "", + self.tr("All text files (*.txt)")) + selected_file = selected_files[0] + return selected_file + + def malformed_file_error(self): + QMessageBox.critical(self, self.tr("Error loading document"), + self.tr("Loaded document is not a revocation document"), + buttons=QMessageBox.Ok) + + async def revocation_broadcast_error(self, error): + await QAsyncMessageBox.critical(self, self.tr("Error broadcasting document"), + error) + + def show_revoked_selfcert(self, selfcert): + text = self.tr(""" + <div>Identity revoked : {uid} (public key : {pubkey}...)</div> + <div>Identity signed on block : {timestamp}</div> + """.format(uid=selfcert.uid, + pubkey=selfcert.pubkey[:12], + timestamp=selfcert.timestamp)) + self.label_revocation_content.setText(text) + + def set_currencies_names(self, names): + self.combo_currency.clear() + for name in names: + self.combo_currency.addItem(name) + self.radio_currency.setChecked(True) + + def ask_for_confirmation(self): + answer = QMessageBox.warning(self, self.tr("Revocation"), + self.tr("""<h4>The publication of this document will remove your identity from the network.</h4> + <li> + <li> <b>This identity won't be able to join the targeted currency anymore.</b> </li> + <li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li> + <li> <b>This identity won't be able to certify individuals anymore.</b> </li> + </li> + Please think twice before publishing this document. + """), QMessageBox.Ok | QMessageBox.Cancel) + return answer == QMessageBox.Ok + + @asyncify + async def accept(self): + await QAsyncMessageBox.information(self, self.tr("Revocation broadcast"), + self.tr("The document was successfully broadcasted.")) + super().accept() diff --git a/src/sakia/tests/functional/__init__.py b/src/sakia/gui/dialogs/transfer/__init__.py similarity index 100% rename from src/sakia/tests/functional/__init__.py rename to src/sakia/gui/dialogs/transfer/__init__.py diff --git a/src/sakia/gui/dialogs/transfer/controller.py b/src/sakia/gui/dialogs/transfer/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..e50ffbcf3f0299dc5317ae7be4dae13b717d0b86 --- /dev/null +++ b/src/sakia/gui/dialogs/transfer/controller.py @@ -0,0 +1,221 @@ +import asyncio +import logging + +from PyQt5.QtCore import Qt, QObject +from PyQt5.QtWidgets import QApplication + +from sakia.data.processors import ConnectionsProcessor +from sakia.decorators import asyncify +from sakia.gui.password_asker import PasswordAskerDialog +from sakia.gui.sub.search_user.controller import SearchUserController +from sakia.gui.sub.user_information.controller import UserInformationController +from .model import TransferModel +from .view import TransferView + + +class TransferController(QObject): + """ + The transfer component controller + """ + + def __init__(self, view, model, search_user, user_information): + """ + Constructor of the transfer component + + :param sakia.gui.dialogs.transfer.view.TransferView: the view + :param sakia.gui.dialogs.transfer.model.TransferModel model: the model + """ + super().__init__() + self.view = view + self.model = model + self.search_user = search_user + self.user_information = user_information + self.view.button_box.accepted.connect(self.accept) + self.view.button_box.rejected.connect(self.reject) + self.view.combo_connections.currentIndexChanged.connect(self.change_current_connection) + self.view.spinbox_amount.valueChanged.connect(self.handle_amount_change) + self.view.spinbox_relative.valueChanged.connect(self.handle_relative_change) + + @classmethod + def create(cls, parent, app): + """ + Instanciate a transfer component + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.core.Application app: + :return: a new Transfer controller + :rtype: TransferController + """ + view = TransferView(parent.view if parent else None, None, None) + model = TransferModel(app) + transfer = cls(view, model, None, None) + + search_user = SearchUserController.create(transfer, app, "") + transfer.set_search_user(search_user) + + user_information = UserInformationController.create(transfer, app, "", None) + transfer.set_user_information(user_information) + + search_user.identity_selected.connect(user_information.search_identity) + + view.set_keys(transfer.model.available_connections()) + return transfer + + @classmethod + def open_dialog(cls, parent, app, connection): + dialog = cls.create(parent, app) + if connection: + dialog.view.combo_connections.setCurrentText(connection.title()) + dialog.refresh() + return dialog.exec() + + @classmethod + async def send_money_to_identity(cls, parent, app, connection, identity): + dialog = cls.create(parent, app) + dialog.view.combo_connections.setCurrentText(connection.title()) + dialog.user_information.change_identity(identity) + dialog.view.edit_pubkey.setText(identity.pubkey) + dialog.view.radio_pubkey.setChecked(True) + + dialog.refresh() + return await dialog.async_exec() + + @classmethod + def send_transfer_again(cls, parent, app, connection, resent_transfer): + dialog = cls.create(parent, app) + dialog.view.combo_connections.setCurrentText(connection.title()) + dialog.view.edit_pubkey.setText(resent_transfer.receiver) + dialog.view.radio_pubkey.setChecked(True) + + dialog.refresh() + relative = dialog.model.quant_to_rel(resent_transfer.amount) + dialog.view.set_spinboxes_parameters(1, resent_transfer.amount, relative) + dialog.view.change_relative_amount(relative) + dialog.view.change_quantitative_amount(resent_transfer.amount) + + connections_processor = ConnectionsProcessor.instanciate(app) + wallet_index = connections_processor.connections().index(connection) + dialog.view.combo_connections.setCurrentIndex(wallet_index) + dialog.view.edit_pubkey.setText(resent_transfer.receiver) + dialog.view.radio_pubkey.setChecked(True) + dialog.view.edit_message.setText(resent_transfer.comment) + + return dialog.exec() + + def set_search_user(self, search_user): + """ + + :param search_user: + :return: + """ + self.search_user = search_user + self.view.set_search_user(search_user.view) + + def set_user_information(self, user_information): + """ + + :param user_information: + :return: + """ + self.user_information = user_information + self.view.set_user_information(user_information.view) + + def selected_pubkey(self): + """ + Get selected pubkey in the widgets of the window + :return: the current pubkey + :rtype: str + """ + pubkey = None + + if self.view.recipient_mode() == TransferView.RecipientMode.SEARCH: + if self.search_user.current_identity(): + pubkey = self.search_user.current_identity().pubkey + else: + pubkey = self.view.pubkey_value() + return pubkey + + @asyncify + async def accept(self): + logging.debug("Accept transfer action...") + self.view.button_box.setEnabled(False) + comment = self.view.edit_message.text() + + logging.debug("checking recipient mode...") + recipient = self.selected_pubkey() + amount = self.view.spinbox_amount.value() * 100 + #TODO: Handle other amount base than 0 + amount_base = 0 + + logging.debug("Showing password dialog...") + password = await PasswordAskerDialog(self.model.connection).async_exec() + if password == "": + self.view.button_box.setEnabled(True) + return + + logging.debug("Setting cursor...") + QApplication.setOverrideCursor(Qt.WaitCursor) + + logging.debug("Send money...") + result, transaction = await self.model.send_money(recipient, password, amount, amount_base, comment) + if result[0]: + await self.view.show_success(self.model.notifications(), recipient) + logging.debug("Restore cursor...") + QApplication.restoreOverrideCursor() + + # If we sent back a transaction we cancel the first one + self.model.cancel_previous() + self.model.app.new_transfer.emit(transaction) + self.view.accept() + else: + await self.view.show_error(self.model.notifications(), result[1]) + self.model.app.new_transfer.emit(transaction) + + QApplication.restoreOverrideCursor() + self.view.button_box.setEnabled(True) + + def reject(self): + self.view.reject() + + def refresh(self): + amount = self.model.wallet_value() + total_text = self.model.localized_amount(amount) + self.view.refresh_labels(total_text) + + if amount == 0: + self.view.set_button_box(TransferView.ButtonBoxState.NO_AMOUNT) + else: + self.view.set_button_box(TransferView.ButtonBoxState.OK) + + max_relative = self.model.quant_to_rel(amount/100) + current_base = self.model.current_base() + + self.view.set_spinboxes_parameters(pow(10, current_base), amount, max_relative) + + def handle_amount_change(self, value): + relative = self.model.quant_to_rel(value) + self.view.change_relative_amount(relative) + self.refresh_amount_suffix() + + def refresh_amount_suffix(self): + self.view.spinbox_amount.setSuffix(" " + self.model.connection.currency) + + def handle_relative_change(self, value): + amount = self.model.rel_to_quant(value) + self.view.change_quantitative_amount(amount) + + def change_current_connection(self, index): + self.model.set_connection(index) + self.search_user.set_currency(self.model.connection.currency) + self.user_information.set_currency(self.model.connection.currency) + self.refresh() + + def async_exec(self): + future = asyncio.Future() + self.view.finished.connect(lambda r: future.set_result(r)) + self.view.open() + self.refresh() + return future + + def exec(self): + self.refresh() + self.view.exec() \ No newline at end of file diff --git a/src/sakia/gui/dialogs/transfer/model.py b/src/sakia/gui/dialogs/transfer/model.py new file mode 100644 index 0000000000000000000000000000000000000000..7d07b33d80273f61e2fe62d38103c6a7fd5f4987 --- /dev/null +++ b/src/sakia/gui/dialogs/transfer/model.py @@ -0,0 +1,100 @@ +import attr +from PyQt5.QtCore import QObject +from sakia.data.processors import BlockchainProcessor, SourcesProcessor, ConnectionsProcessor + + +@attr.s() +class TransferModel(QObject): + """ + The model of transfer component + + :param sakia.app.Application app: + :param sakia.data.entities.Connection connection: + :param sakia.data.processors.BlockchainProcessor _blockchain_processor: + """ + + app = attr.ib() + connection = attr.ib(default=None) + resent_transfer = attr.ib(default=None) + _blockchain_processor = attr.ib(default=None) + _sources_processor = attr.ib(default=None) + _connections_processor = attr.ib(default=None) + + def __attrs_post_init__(self): + super().__init__() + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) + self._sources_processor = SourcesProcessor.instanciate(self.app) + self._connections_processor = ConnectionsProcessor.instanciate(self.app) + + def rel_to_quant(self, rel_value): + """ + Get the quantitative value of a relative amount + :param float rel_value: + :rtype: int + """ + dividend, base = self._blockchain_processor.last_ud(self.connection.currency) + amount = rel_value * dividend * pow(10, base) + # amount is rounded to the nearest power of 10 depending of last ud base + rounded = int(pow(10, base) * round(float(amount) / pow(10, base))) + return rounded / 100 + + def quant_to_rel(self, amount): + """ + Get the relative value of a given amount + :param int amount: + :rtype: float + """ + dividend, base = self._blockchain_processor.last_ud(self.connection.currency) + relative = amount * 100 / (dividend * pow(10, base)) + return relative + + def wallet_value(self): + """ + Get the value of the current wallet in the current community + """ + return self._sources_processor.amount(self.connection.currency, self.connection.pubkey) + + def current_base(self): + """ + Get the current base of the network + """ + dividend, base = self._blockchain_processor.last_ud(self.connection.currency) + return base + + def localized_amount(self, amount): + """ + Get the value of the current referential + """ + + localized = self.app.current_ref.instance(amount, self.connection.currency, self.app).diff_localized(True, True) + return localized + + def cancel_previous(self): + if self.resent_transfer: + self.resent_transfer.cancel() + + def available_connections(self): + return self._connections_processor.connections() + + def set_connection(self, index): + connections = self._connections_processor.connections() + self.connection = connections[index] + + async def send_money(self, recipient, password, amount, amount_base, comment): + """ + Send money to given recipient using the account + :param str recipient: + :param int amount: + :param int amount_base: + :param str comment: + :param str password: + :return: the result of the send + """ + + result = await self.app.documents_service.send_money(self.connection, password, + recipient, amount, amount_base, comment) + self.app.db.commit() + return result + + def notifications(self): + return self.app.parameters.notifications diff --git a/res/ui/certification.ui b/src/sakia/gui/dialogs/transfer/transfer.ui similarity index 55% rename from res/ui/certification.ui rename to src/sakia/gui/dialogs/transfer/transfer.ui index 59f527b542ae9c55862bc1c29a8ae8438b2aa1c7..5b9a2593595ea6daca3a870e3a868b4efc3c158a 100644 --- a/res/ui/certification.ui +++ b/src/sakia/gui/dialogs/transfer/transfer.ui @@ -1,119 +1,57 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>CertificationDialog</class> - <widget class="QDialog" name="CertificationDialog"> + <class>TransferMoneyDialog</class> + <widget class="QDialog" name="TransferMoneyDialog"> <property name="geometry"> <rect> <x>0</x> <y>0</y> - <width>715</width> - <height>477</height> + <width>566</width> + <height>415</height> </rect> </property> <property name="windowTitle"> - <string>Certification</string> + <string>Transfer money</string> </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QGroupBox" name="groupBox_2"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> <property name="title"> - <string>Community</string> + <string>Select connection</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> <item> - <widget class="QComboBox" name="combo_community"/> - </item> - <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="title"> - <string>Certifications stock</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QLabel" name="label_cert_stock"> - <property name="text"> - <string/> - </property> - </widget> - </item> - </layout> - </widget> + <widget class="QComboBox" name="combo_connections"/> </item> </layout> </widget> </item> <item> - <widget class="QGroupBox" name="groupBox"> + <widget class="QGroupBox" name="group_box_recipient"> <property name="title"> - <string>Certify user</string> + <string>Transfer money to</string> </property> <layout class="QHBoxLayout" name="horizontalLayout_5"> <item> - <layout class="QVBoxLayout" name="verticalLayout_3"> + <layout class="QVBoxLayout" name="verticalLayout_4"> <property name="topMargin"> <number>6</number> </property> + <property name="bottomMargin"> + <number>6</number> + </property> <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QRadioButton" name="radio_contact"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Con&tact</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Maximum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>20</height> - </size> - </property> - </spacer> - </item> + <layout class="QHBoxLayout" name="horizontalLayout"> <item> - <widget class="QComboBox" name="combo_contact"> - <property name="enabled"> - <bool>true</bool> - </property> + <widget class="QRadioButton" name="radio_pubkey"> <property name="sizePolicy"> <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QRadioButton" name="radio_pubkey"> <property name="text"> - <string>&User public key</string> + <string>&Recipient public key</string> </property> <property name="checked"> <bool>false</bool> @@ -142,7 +80,7 @@ <bool>false</bool> </property> <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> @@ -161,25 +99,25 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> + <layout class="QHBoxLayout" name="layout_search_user"> <property name="topMargin"> <number>6</number> </property> <item> <widget class="QRadioButton" name="radio_search"> <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="text"> - <string>Sea&rch user</string> + <string>Search &user</string> </property> </widget> </item> <item> - <spacer name="horizontalSpacer_3"> + <spacer name="horizontalSpacer"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> @@ -194,19 +132,6 @@ </property> </spacer> </item> - <item> - <widget class="SearchUserWidget" name="search_user" native="true"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - </item> </layout> </item> </layout> @@ -214,6 +139,96 @@ </layout> </widget> </item> + <item> + <widget class="QFrame" name="frame"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> + <widget class="QLabel" name="label_total"> + <property name="text"> + <string>Available money : </string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Amount</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="spinbox_relative"> + <property name="suffix"> + <string> UD</string> + </property> + <property name="decimals"> + <number>6</number> + </property> + <property name="maximum"> + <double>99999999999999991611392.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.010000000000000</double> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="spinbox_amount"> + <property name="wrapping"> + <bool>false</bool> + </property> + <property name="frame"> + <bool>true</bool> + </property> + <property name="readOnly"> + <bool>false</bool> + </property> + <property name="buttonSymbols"> + <enum>QAbstractSpinBox::UpDownArrows</enum> + </property> + <property name="showGroupSeparator" stdset="0"> + <bool>false</bool> + </property> + <property name="decimals"> + <number>2</number> + </property> + <property name="singleStep"> + <double>1.000000000000000</double> + </property> + <property name="value"> + <double>0.000000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Transaction message</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QLineEdit" name="edit_message"> + <property name="inputMask"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </item> <item> <widget class="QDialogButtonBox" name="button_box"> <property name="orientation"> @@ -226,14 +241,6 @@ </item> </layout> </widget> - <customwidgets> - <customwidget> - <class>SearchUserWidget</class> - <extends>QWidget</extends> - <header>sakia.gui.widgets.search_user</header> - <container>1</container> - </customwidget> - </customwidgets> <resources/> <connections/> <slots> diff --git a/src/sakia/gui/dialogs/transfer/view.py b/src/sakia/gui/dialogs/transfer/view.py new file mode 100644 index 0000000000000000000000000000000000000000..458215e38fc15adce2eb263965da38ccf245b5a1 --- /dev/null +++ b/src/sakia/gui/dialogs/transfer/view.py @@ -0,0 +1,146 @@ +from PyQt5.QtWidgets import QDialog, QDialogButtonBox +from PyQt5.QtGui import QRegExpValidator +from PyQt5.QtCore import QT_TRANSLATE_NOOP, QRegExp +from .transfer_uic import Ui_TransferMoneyDialog +from enum import Enum +from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox + + +class TransferView(QDialog, Ui_TransferMoneyDialog): + """ + Transfer component view + """ + + class ButtonBoxState(Enum): + NO_AMOUNT = 0 + OK = 1 + + class RecipientMode(Enum): + PUBKEY = 1 + SEARCH = 2 + + _button_box_values = { + ButtonBoxState.NO_AMOUNT: (False, + QT_TRANSLATE_NOOP("TransferView", "No amount. Please give the transfer amount")), + ButtonBoxState.OK: (True, QT_TRANSLATE_NOOP("CertificationView", "&Ok")) + } + + def __init__(self, parent, search_user_view, user_information_view): + """ + + :param parent: + :param sakia.gui.search_user.view.SearchUserView search_user_view: + :param sakia.gui.user_information.view.UserInformationView user_information_view: + """ + super().__init__(parent) + self.setupUi(self) + + self.radio_pubkey.toggled.connect(lambda c, radio=TransferView.RecipientMode.PUBKEY: self.recipient_mode_changed(radio)) + self.radio_search.toggled.connect(lambda c, radio=TransferView.RecipientMode.SEARCH: self.recipient_mode_changed(radio)) + + regexp = QRegExp('^([ a-zA-Z0-9-_:/;*?\[\]\(\)\\\?!^+=@&~#{}|<>%.]{0,255})$') + validator = QRegExpValidator(regexp) + self.edit_message.setValidator(validator) + + self.search_user = search_user_view + self.user_information_view = user_information_view + self._amount_base = 0 + self._currency = "" + + def recipient_mode(self): + if self.radio_search.isChecked(): + return TransferView.RecipientMode.SEARCH + else: + return TransferView.RecipientMode.PUBKEY + + def set_keys(self, connections): + for conn in connections: + self.combo_connections.addItem(conn.title()) + + def set_search_user(self, search_user_view): + """ + + :param sakia.gui.search_user.view.SearchUserView search_user_view: + :return: + """ + self.search_user = search_user_view + self.layout_search_user.addWidget(search_user_view) + self.search_user.button_reset.hide() + + def set_user_information(self, user_information_view): + self.user_information_view = user_information_view + self.group_box_recipient.layout().addWidget(user_information_view) + + def recipient_mode_changed(self, radio): + """ + :param str radio: + """ + self.edit_pubkey.setEnabled(radio == TransferView.RecipientMode.PUBKEY) + self.search_user.setEnabled(radio == TransferView.RecipientMode.SEARCH) + + def change_quantitative_amount(self, amount): + """ + Change relative amount with signals blocked + :param amount: + """ + self.spinbox_amount.blockSignals(True) + self.spinbox_amount.setValue(amount) + self.spinbox_amount.blockSignals(False) + + def change_relative_amount(self, relative): + """ + Change the quantitative amount with signals blocks + :param relative: + """ + self.spinbox_relative.blockSignals(True) + self.spinbox_relative.setValue(relative) + self.spinbox_relative.blockSignals(False) + + def set_spinboxes_parameters(self, tick_quant, max_quant, max_rel): + """ + Configure the spinboxes + It should depend on what the last UD base is + :param int tick_quant: + :param int max_quant: + :param float max_rel: + """ + self.spinbox_amount.setMaximum(max_quant/100) + self.spinbox_relative.setMaximum(max_rel) + self.spinbox_amount.setSingleStep(tick_quant) + + def refresh_labels(self, total_text): + """ + Refresh displayed texts + :param str total_text: + :param str currency: + """ + self.label_total.setText("{0}".format(total_text)) + + def set_button_box(self, state, **kwargs): + """ + Set button box state + :param sakia.gui.transfer.view.TransferView.ButtonBoxState state: the state of te button box + :param dict kwargs: the values to replace from the text in the state + :return: + """ + button_box_state = TransferView._button_box_values[state] + self.button_box.button(QDialogButtonBox.Ok).setEnabled(button_box_state[0]) + self.button_box.button(QDialogButtonBox.Ok).setText(button_box_state[1].format(**kwargs)) + + async def show_success(self, notification, recipient): + if notification: + toast.display(self.tr("Transfer"), + self.tr("Success sending money to {0}").format(recipient)) + else: + await QAsyncMessageBox.information(self.widget, self.tr("Transfer"), + self.tr("Success sending money to {0}").format(recipient)) + + async def show_error(self, notification, error_txt): + if notification: + toast.display(self.tr("Transfer"), "Error : {0}".format(error_txt)) + else: + await QAsyncMessageBox.critical(self.widget, self.tr("Transfer"), error_txt) + + def pubkey_value(self): + return self.edit_pubkey.text() \ No newline at end of file diff --git a/src/sakia/gui/graphs/explorer_tab.py b/src/sakia/gui/graphs/explorer_tab.py deleted file mode 100644 index 78da13791d23c561ea03600e5662c2fcfcb54993..0000000000000000000000000000000000000000 --- a/src/sakia/gui/graphs/explorer_tab.py +++ /dev/null @@ -1,124 +0,0 @@ -import logging - -from PyQt5.QtCore import QEvent, pyqtSignal -from PyQt5.QtWidgets import QWidget - -from ...tools.exceptions import NoPeerAvailable -from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task -from ...core.graph import ExplorerGraph -from .graph_tab import GraphTabWidget -from ...gen_resources.explorer_tab_uic import Ui_ExplorerTabWidget - - -class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): - - money_sent = pyqtSignal() - - def __init__(self, app, account=None, community=None, password_asker=None, - widget=QWidget, view=Ui_ExplorerTabWidget): - """ - :param sakia.core.app.Application app: Application instance - :param sakia.core.app.Application app: Application instance - :param sakia.core.Account account: The account displayed in the widget - :param sakia.core.Community community: The community displayed in the widget - :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords - """ - # construct from qtDesigner - super().__init__(app, account, community, password_asker, widget) - self.ui = view() - self.ui.setupUi(self.widget) - self.ui.search_user_widget.init(app) - - self.set_scene(self.ui.graphicsView.scene()) - self.graph = None - self.app = app - self.draw_task = None - - # nodes list for menu from search - self.nodes = list() - - # create node metadata from account - self._current_identity = None - self.ui.button_go.clicked.connect(self.go_clicked) - self.ui.search_user_widget.identity_selected.connect(self.draw_graph) - self.ui.search_user_widget.reset.connect(self.reset) - - def cancel_once_tasks(self): - cancel_once_task(self, self.refresh_informations_frame) - cancel_once_task(self, self.reset) - - def change_account(self, account, password_asker): - self.ui.search_user_widget.change_account(account) - self.account = account - self.password_asker = password_asker - - def change_community(self, community): - self.community = community - if self.graph: - self.graph.stop_exploration() - self.graph = ExplorerGraph(self.app, self.community) - self.graph.graph_changed.connect(self.refresh) - self.ui.search_user_widget.change_community(community) - self.graph.current_identity_changed.connect(self.ui.graphicsView.scene().update_current_identity) - self.reset() - - def go_clicked(self): - if self.graph: - self.graph.stop_exploration() - self.draw_graph(self._current_identity) - - def draw_graph(self, identity): - """ - Draw community graph centered on the identity - - :param sakia.core.registry.Identity identity: Graph node identity - """ - logging.debug("Draw graph - " + identity.uid) - - if self.community: - #Connect new identity - if self._current_identity != identity: - self._current_identity = identity - - self.graph.start_exploration(identity, self.ui.steps_slider.value()) - - # draw graph in qt scene - self.ui.graphicsView.scene().clear() - self.ui.graphicsView.scene().update_wot(self.graph.nx_graph, identity, self.ui.steps_slider.maximum()) - - def refresh(self): - """ - Refresh graph scene to current metadata - """ - if self._current_identity: - # draw graph in qt scene - self.ui.graphicsView.scene().update_wot(self.graph.nx_graph, self._current_identity, self.ui.steps_slider.maximum()) - else: - self.reset() - - @once_at_a_time - @asyncify - async def reset(self, checked=False): - """ - Reset graph scene to wallet identity - """ - if self.account and self.community: - try: - parameters = await self.community.parameters() - self.ui.steps_slider.setMaximum(parameters['stepMax']) - self.ui.steps_slider.setValue(int(0.33 * parameters['stepMax'])) - identity = await self.account.identity(self.community) - self.draw_graph(identity) - except NoPeerAvailable: - logging.debug("No peer available") - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh() - return super().changeEvent(event) diff --git a/src/sakia/gui/graphs/graph_tab.py b/src/sakia/gui/graphs/graph_tab.py deleted file mode 100644 index d308245b821fb6a43169b3780b0204436cc7c47a..0000000000000000000000000000000000000000 --- a/src/sakia/gui/graphs/graph_tab.py +++ /dev/null @@ -1,182 +0,0 @@ -from PyQt5.QtWidgets import QWidget -from PyQt5.QtCore import pyqtSlot, QEvent, QLocale, QDateTime, pyqtSignal, QObject -from PyQt5.QtGui import QCursor - -from ...tools.exceptions import MembershipNotFoundError -from ...tools.decorators import asyncify, once_at_a_time -from ...core.registry import BlockchainState -from ..widgets.context_menu import ContextMenu - - -class GraphTabWidget(QObject): - - money_sent = pyqtSignal() - - def __init__(self, app, account=None, community=None, password_asker=None, widget=QWidget): - """ - :param sakia.core.app.Application app: Application instance - :param sakia.core.app.Application app: Application instance - :param sakia.core.Account account: The account displayed in the widget - :param sakia.core.Community community: The community displayed in the widget - :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords - :param class widget: The class of the graph tab - """ - super().__init__() - - self.widget = widget() - self.account = account - self.community = community - self.password_asker = password_asker - - self.app = app - - def set_scene(self, scene): - """ - Set the scene and connects the signals - :param sakia.gui.views.scenes.base_scene.BaseScene scene: the scene - :return: - """ - # add scene events - scene.node_context_menu_requested.connect(self.node_context_menu) - scene.node_clicked.connect(self.handle_node_click) - - @once_at_a_time - @asyncify - async def refresh_informations_frame(self): - parameters = self.community.parameters - try: - identity = await self.account.identity(self.community) - membership = identity.membership(self.community) - renew_block = membership['blockNumber'] - last_renewal = self.community.get_block(renew_block)['medianTime'] - expiration = last_renewal + parameters['sigValidity'] - except MembershipNotFoundError: - last_renewal = None - expiration = None - - certified = await identity.unique_valid_certified_by(self.app.identities_registry, self.community) - certifiers = await identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) - if last_renewal and expiration: - date_renewal = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(last_renewal).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat) - ) - date_expiration = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(expiration).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat) - ) - - if self.account.pubkey in self.community.members_pubkeys(): - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.account.name, self.account.pubkey, - self.tr("Membership"), - self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration), - self.tr("Your web of trust"), - self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), - len(certified)) - ) - ) - else: - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.account.name, self.account.pubkey, - self.tr("Not a member"), - self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration), - self.tr("Your web of trust"), - self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), - len(certified)) - ) - ) - else: - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.account.name, self.account.pubkey, - self.tr("Not a member"), - self.tr("Your web of trust"), - self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), - len(certified)) - ) - ) - - @pyqtSlot(str, dict) - def handle_node_click(self, pubkey, metadata): - self.draw_graph( - self.app.identities_registry.from_handled_data( - metadata['text'], - pubkey, - None, - BlockchainState.VALIDATED, - self.community - ) - ) - - @once_at_a_time - @asyncify - async def draw_graph(self, identity): - """ - Draw community graph centered on the identity - - :param sakia.core.registry.Identity identity: Graph node identity - """ - pass - - @once_at_a_time - @asyncify - async def reset(self, checked=False): - """ - Reset graph scene to wallet identity - """ - pass - - def refresh(self): - """ - Refresh graph scene to current metadata - """ - pass - - @asyncify - async def node_context_menu(self, pubkey): - """ - Open the node context menu - :param str pubkey: the pubkey of the node to open - """ - identity = await self.app.identities_registry.future_find(pubkey, self.community) - menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, - (identity,)) - menu.view_identity_in_wot.connect(self.draw_graph) - - # Show the context menu. - menu.qmenu.popup(QCursor.pos()) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh() - return super().changeEvent(event) diff --git a/src/sakia/gui/graphs/wot_tab.py b/src/sakia/gui/graphs/wot_tab.py deleted file mode 100644 index 31eedfdce5b7470cc8343bc5ee1dce59ec012c56..0000000000000000000000000000000000000000 --- a/src/sakia/gui/graphs/wot_tab.py +++ /dev/null @@ -1,145 +0,0 @@ -import logging -import asyncio - -from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP, QObject -from PyQt5.QtWidgets import QWidget -from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task -from ...core.graph import WoTGraph -from ...gen_resources.wot_tab_uic import Ui_WotTabWidget -from ...gui.widgets.busy import Busy -from .graph_tab import GraphTabWidget - - -class WotTabWidget(GraphTabWidget): - - money_sent = pyqtSignal() - - def __init__(self, app, account=None, community=None, password_asker=None, widget=QWidget, view=Ui_WotTabWidget): - """ - :param sakia.core.app.Application app: Application instance - :param sakia.core.app.Application app: Application instance - :param sakia.core.Account account: The account displayed in the widget - :param sakia.core.Community community: The community displayed in the widget - :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords - :param class widget: The class of the PyQt5 widget used for this tab - :param class view: The class of the UI View for this tab - """ - super().__init__(app, account, community, password_asker, widget) - # construct from qtDesigner - self.ui = view() - self.ui.setupUi(self.widget) - - self.ui.search_user_widget.init(app) - self.widget.installEventFilter(self) - self.busy = Busy(self.ui.graphicsView) - self.busy.hide() - - self.set_scene(self.ui.graphicsView.scene()) - - self.account = account - self.community = community - self.password_asker = password_asker - self.app = app - self.draw_task = None - - self.ui.search_user_widget.identity_selected.connect(self.draw_graph) - self.ui.search_user_widget.reset.connect(self.reset) - - # create node metadata from account - self._current_identity = None - - def cancel_once_tasks(self): - cancel_once_task(self, self.draw_graph) - cancel_once_task(self, self.refresh_informations_frame) - cancel_once_task(self, self.reset) - - def change_account(self, account, password_asker): - self.cancel_once_tasks() - self.ui.search_user_widget.change_account(account) - self.account = account - self.password_asker = password_asker - - def change_community(self, community): - self.cancel_once_tasks() - self.ui.search_user_widget.change_community(community) - self._auto_refresh(community) - self.community = community - self.reset() - - def _auto_refresh(self, new_community): - if self.community: - try: - self.community.network.new_block_mined.disconnect(self.refresh) - except TypeError as e: - if "connected" in str(e): - logging.debug("new block mined not connected") - if self.app.preferences["auto_refresh"]: - if new_community: - new_community.network.new_block_mined.connect(self.refresh) - elif self.community: - self.community.network.new_block_mined.connect(self.refresh) - - @once_at_a_time - @asyncify - async def draw_graph(self, identity): - """ - Draw community graph centered on the identity - - :param sakia.core.registry.Identity identity: Center identity - """ - logging.debug("Draw graph - " + identity.uid) - self.busy.show() - - if self.community: - identity_account = await self.account.identity(self.community) - - # create empty graph instance - graph = WoTGraph(self.app, self.community) - await graph.initialize(identity, identity_account) - # draw graph in qt scene - self.ui.graphicsView.scene().update_wot(graph.nx_graph, identity) - - # if selected member is not the account member... - if identity.pubkey != identity_account.pubkey: - # add path from selected member to account member - path = await graph.get_shortest_path_to_identity(identity, identity_account) - if path: - self.ui.graphicsView.scene().update_path(graph.nx_graph, path) - self.busy.hide() - - @once_at_a_time - @asyncify - async def reset(self, checked=False): - """ - Reset graph scene to wallet identity - """ - if self.account and self.community: - identity = await self.account.identity(self.community) - self.draw_graph(identity) - - def refresh(self): - """ - Refresh graph scene to current metadata - """ - if self._current_identity: - self.draw_graph(self._current_identity) - else: - self.reset() - - def eventFilter(self, source, event): - if event.type() == QEvent.Resize: - self.busy.resize(event.size()) - self.widget.resizeEvent(event) - return self.widget.eventFilter(source, event) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self._auto_refresh(None) - self.refresh() - return super(WotTabWidget, self).changeEvent(event) diff --git a/src/sakia/gui/homescreen.py b/src/sakia/gui/homescreen.py deleted file mode 100644 index 3eb44eadf63bb86595608e6baabae20bf93c3714..0000000000000000000000000000000000000000 --- a/src/sakia/gui/homescreen.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Created on 31 janv. 2015 - -@author: vit -""" - -from PyQt5.QtWidgets import QWidget, QFrame, QGridLayout, QAction -from PyQt5.QtCore import QEvent, Qt, pyqtSlot, pyqtSignal -from ..gen_resources.homescreen_uic import Ui_HomescreenWidget -from .community_tile import CommunityTile -from ..core.community import Community -import logging - - -class FrameCommunities(QFrame): - community_tile_clicked = pyqtSignal(Community) - - def __init__(self, parent): - super().__init__(parent) - self.grid_layout = QGridLayout() - self.setLayout(self.grid_layout) - self.grid_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.setFrameShape(QFrame.StyledPanel) - self.setFrameShadow(QFrame.Raised) - self.tiles = [] - - def sizeHint(self): - return self.parentWidget().size() - - def refresh(self, app): - for t in self.tiles: - t.cancel_refresh() - t.setParent(None) - self.tiles = [] - if app.current_account: - for c in app.current_account.communities: - community_tile = CommunityTile(self, app, c) - community_tile.clicked.connect(self.click_on_tile) - self.layout().addWidget(community_tile) - self.tiles.append(community_tile) - - def refresh_content(self): - for t in self.tiles: - t.refresh() - - @pyqtSlot() - def click_on_tile(self): - tile = self.sender() - logging.debug("Click on tile") - self.community_tile_clicked.emit(tile.community) - - -class HomeScreenWidget(QWidget, Ui_HomescreenWidget): - """ - classdocs - """ - - def __init__(self, app, status_label): - """ - Constructor - """ - super().__init__() - self.setupUi(self) - self.app = app - self.frame_communities = FrameCommunities(self) - self.layout().addWidget(self.frame_communities) - self.status_label = status_label - - def refresh(self): - self.frame_communities.refresh(self.app) - if self.app.current_account: - self.frame_connected.show() - self.label_connected.setText(self.tr("Connected as {0}".format(self.app.current_account.name))) - self.frame_disconnected.hide() - else: - self.frame_disconnected.show() - self.frame_connected.hide() - - def referential_changed(self): - self.frame_communities.refresh_content() - - def showEvent(self, QShowEvent): - """ - - :param QShowEvent: - :return: - """ - self.frame_communities.refresh_content() - self.status_label.setText("") - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - return super(HomeScreenWidget, self).changeEvent(event) - - diff --git a/src/sakia/gui/identities_tab.py b/src/sakia/gui/identities_tab.py deleted file mode 100644 index 21e5ab64e61f2f5ee56208bfdee97bfcfc56c90d..0000000000000000000000000000000000000000 --- a/src/sakia/gui/identities_tab.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" - -import logging - -from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QT_TRANSLATE_NOOP, QObject -from PyQt5.QtGui import QCursor -from PyQt5.QtWidgets import QWidget, QAction, QMenu, QDialog, \ - QAbstractItemView -from duniterpy.api import bma, errors -from duniterpy.documents import BlockUID - -from ..models.identities import IdentitiesFilterProxyModel, IdentitiesTableModel -from ..gen_resources.identities_tab_uic import Ui_IdentitiesTab -from ..core.registry import Identity, BlockchainState -from ..tools.exceptions import NoPeerAvailable -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from .widgets.context_menu import ContextMenu - - -class IdentitiesTabWidget(QObject): - - """ - classdocs - """ - view_in_wot = pyqtSignal(object) - money_sent = pyqtSignal() - - _direct_connections_text = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Search direct certifications") - _search_placeholder = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Research a pubkey, an uid...") - - def __init__(self, app, account=None, community=None, password_asker=None, - widget=QWidget, view=Ui_IdentitiesTab): - """ - Init - - :param sakia.core.app.Application app: Application instance - :param sakia.core.Account account: The account displayed in the widget - :param sakia.core.Community community: The community displayed in the widget - :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords - :param class widget: The class of the PyQt5 widget used for this tab - :param class view: The class of the UI View for this tab - """ - super().__init__() - self.widget = widget() - self.ui = view() - self.ui.setupUi(self.widget) - - self.app = app - self.community = community - self.account = account - self.password_asker = password_asker - - self.direct_connections = QAction(self.tr(IdentitiesTabWidget._direct_connections_text), self) - self.ui.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) - - identities_model = IdentitiesTableModel() - proxy = IdentitiesFilterProxyModel() - proxy.setSourceModel(identities_model) - self.ui.table_identities.setModel(proxy) - self.ui.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) - self.ui.table_identities.customContextMenuRequested.connect(self.identity_context_menu) - self.ui.table_identities.sortByColumn(0, Qt.AscendingOrder) - self.ui.table_identities.resizeColumnsToContents() - identities_model.modelAboutToBeReset.connect(lambda: self.ui.table_identities.setEnabled(False)) - identities_model.modelReset.connect(lambda: self.ui.table_identities.setEnabled(True)) - - self.direct_connections.triggered.connect(self._async_search_direct_connections) - self.ui.button_search.addAction(self.direct_connections) - self.ui.button_search.clicked.connect(self._async_execute_search_text) - - def cancel_once_tasks(self): - cancel_once_task(self, self.identity_context_menu) - cancel_once_task(self, self._async_execute_search_text) - cancel_once_task(self, self._async_search_direct_connections) - cancel_once_task(self, self.refresh_identities) - - def change_account(self, account, password_asker): - self.cancel_once_tasks() - self.account = account - self.password_asker = password_asker - if self.account is None: - self.community = None - - def change_community(self, community): - self.cancel_once_tasks() - self.community = community - self.ui.table_identities.model().change_community(community) - self._async_search_direct_connections() - - @once_at_a_time - @asyncify - async def identity_context_menu(self, point): - index = self.ui.table_identities.indexAt(point) - model = self.ui.table_identities.model() - if index.isValid() and index.row() < model.rowCount(): - source_index = model.mapToSource(index) - pubkey_col = model.sourceModel().columns_ids.index('pubkey') - pubkey_index = model.sourceModel().index(source_index.row(), - pubkey_col) - pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) - identity = await self.app.identities_registry.future_find(pubkey, self.community) - menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, - (identity,)) - menu.view_identity_in_wot.connect(self.view_in_wot) - - # Show the context menu. - menu.qmenu.popup(QCursor.pos()) - - @once_at_a_time - @asyncify - async def _async_execute_search_text(self, checked): - cancel_once_task(self, self._async_search_direct_connections) - - self.ui.busy.show() - text = self.ui.edit_textsearch.text() - if len(text) < 2: - return - try: - response = await self.community.bma_access.future_request(bma.wot.Lookup, {'search': text}) - identities = [] - for identity_data in response['results']: - for uid_data in identity_data['uids']: - identity = Identity.from_handled_data(uid_data['uid'], - identity_data['pubkey'], - BlockUID.from_str(uid_data['meta']['timestamp']), - BlockchainState.BUFFERED) - identities.append(identity) - - self.ui.edit_textsearch.clear() - self.ui.edit_textsearch.setPlaceholderText(text) - await self.refresh_identities(identities) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - logging.debug(str(e)) - except NoPeerAvailable as e: - logging.debug(str(e)) - finally: - self.ui.busy.hide() - - @once_at_a_time - @asyncify - async def _async_search_direct_connections(self, checked=False): - """ - Search members of community and display found members - """ - cancel_once_task(self, self._async_execute_search_text) - - if self.account and self.community: - try: - self.ui.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) - await self.refresh_identities([]) - self.ui.busy.show() - self_identity = await self.account.identity(self.community) - account_connections = [] - certs_of = await self_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) - for p in certs_of: - account_connections.append(p['identity']) - certifiers_of = [p for p in account_connections] - certs_by = await self_identity.unique_valid_certified_by(self.app.identities_registry, self.community) - for p in certs_by: - account_connections.append(p['identity']) - certified_by = [p for p in account_connections - if p.pubkey not in [i.pubkey for i in certifiers_of]] - identities = certifiers_of + certified_by - self.ui.busy.hide() - await self.refresh_identities(identities) - except NoPeerAvailable: - self.ui.busy.hide() - - async def refresh_identities(self, identities): - """ - Refresh the table with specified identities. - If no identities is passed, use the account connections. - """ - await self.ui.table_identities.model().sourceModel().refresh_identities(identities) - self.ui.table_identities.resizeColumnsToContents() - - def retranslateUi(self, widget): - self.direct_connections.setText(self.tr(IdentitiesTabWidget._direct_connections_text)) - super().retranslateUi(self) - - def resizeEvent(self, event): - self.ui.busy.resize(event.size()) - super().resizeEvent(event) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - return super(IdentitiesTabWidget, self).changeEvent(event) - diff --git a/src/sakia/gui/import_account.py b/src/sakia/gui/import_account.py deleted file mode 100644 index 59d73ff48fd9b2839406094b297ecd2b5c0ed25d..0000000000000000000000000000000000000000 --- a/src/sakia/gui/import_account.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Created on 22 mai 2014 - -@author: inso -""" -import re -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QFileDialog - -from ..tools.exceptions import Error -from ..gen_resources.import_account_uic import Ui_ImportAccountDialog - - -class ImportAccountDialog(QDialog, Ui_ImportAccountDialog): - - """ - classdocs - """ - - def __init__(self, app, parent=None): - """ - Constructor - """ - super().__init__() - self.setupUi(self) - self.app = app - self.main_window = parent - self.selected_file = "" - self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - - def accept(self): - account_name = self.edit_name.text() - try: - self.app.import_account(self.selected_file, account_name) - except Exception as e: - QMessageBox.critical(self, self.tr("Error"), - "{0}".format(e), - QMessageBox.Ok) - return - QMessageBox.information(self, self.tr("Account import"), - self.tr("Account imported succefully !")) - super().accept() - - def import_account(self): - self.selected_file = QFileDialog.getOpenFileName(self, - self.tr("Import an account file"), - "", - self.tr("All account files (*.acc)")) - self.selected_file = self.selected_file[0] - self.edit_file.setText(self.selected_file) - self.check() - - def name_changed(self): - self.check() - - def check(self): - name = self.edit_name.text() - if name == "": - self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - self.label_errors.setText(self.tr("Please enter a name")) - return - for account in self.app.accounts: - if name == account: - self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - self.label_errors.setText(self.tr("Name already exists")) - return - if self.selected_file[-4:] != ".acc": - self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - self.label_errors.setText(self.tr("File is not an account format")) - return - self.label_errors.setText("") - self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) diff --git a/src/sakia/gui/informations_tab.py b/src/sakia/gui/informations_tab.py deleted file mode 100644 index af6be48e89a9534237fa120560e25c2ea6355819..0000000000000000000000000000000000000000 --- a/src/sakia/gui/informations_tab.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -Created on 31 janv. 2015 - -@author: vit -""" - -import logging -import math -from PyQt5.QtCore import QLocale, QDateTime, QEvent -from PyQt5.QtWidgets import QWidget -from ..gen_resources.informations_tab_uic import Ui_InformationsTabWidget -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from ..tools.exceptions import NoPeerAvailable -from .widgets import Busy -from ..core.money import Referentials - - -class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): - """ - classdocs - """ - - def __init__(self, app): - """ - Constructor of the InformationsTabWidget - - :param sakia.core.app.Application app: Application instance - - :return: - """ - super().__init__() - self.setupUi(self) - self.app = app - self.account = None - self.community = None - self.busy = Busy(self.scrollArea) - self.busy.hide() - - def change_account(self, account): - """ - - :param sakia.core.app.Account account: Account instance selected - """ - cancel_once_task(self, self.refresh_labels) - self.account = account - - def change_community(self, community): - cancel_once_task(self, self.refresh_labels) - self.community = community - self.refresh() - - def refresh(self): - if self.account and self.community: - self.refresh_labels() - - @once_at_a_time - @asyncify - async def refresh_labels(self): - self.busy.show() - #  try to request money parameters - try: - params = await self.community.parameters() - except NoPeerAvailable as e: - logging.debug('community parameters error : ' + str(e)) - return False - - #  try to request money variables from last ud block - try: - block_ud = await self.community.get_ud_block() - except NoPeerAvailable as e: - logging.debug('community get_ud_block error : ' + str(e)) - return False - try: - block_ud_minus_1 = await self.community.get_ud_block(x=1) - except NoPeerAvailable as e: - logging.debug('community get_ud_block error : ' + str(e)) - return False - - if block_ud: - # display float values - localized_ud = await self.account.current_ref.instance(block_ud['dividend'] * math.pow(10, block_ud['unitbase']), - self.community, - self.app) \ - .diff_localized(True, self.app.preferences['international_system_of_units']) - - computed_dividend = await self.community.computed_dividend() - # display float values - localized_ud_plus_1 = await self.account.current_ref.instance(computed_dividend, - self.community, self.app)\ - .diff_localized(True, self.app.preferences['international_system_of_units']) - - localized_mass = await self.account.current_ref.instance(block_ud['monetaryMass'], - self.community, self.app)\ - .diff_localized(True, self.app.preferences['international_system_of_units']) - - localized_ud_median_time = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block_ud['medianTime']), - QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) - ) - - localized_next_ud_median_time = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block_ud['medianTime'] + params['dt']), - QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) - ) - - if block_ud_minus_1: - mass_minus_1 = (float(0) if block_ud['membersCount'] == 0 else - block_ud_minus_1['monetaryMass'] / block_ud['membersCount']) - localized_mass_minus_1_per_member = await self.account.current_ref.instance(mass_minus_1, - self.community, self.app)\ - .diff_localized(True, self.app.preferences['international_system_of_units']) - localized_mass_minus_1 = await self.account.current_ref.instance(block_ud_minus_1['monetaryMass'], - self.community, self.app)\ - .diff_localized(True, self.app.preferences['international_system_of_units']) - # avoid divide by zero ! - if block_ud['membersCount'] == 0 or block_ud_minus_1['monetaryMass'] == 0: - actual_growth = float(0) - else: - actual_growth = (block_ud['dividend'] * math.pow(10, block_ud['unitbase'])) / (block_ud_minus_1['monetaryMass'] / block_ud['membersCount']) - - localized_ud_median_time_minus_1 = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block_ud_minus_1['medianTime']), - QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) - ) - else: - localized_mass_minus_1_per_member = QLocale().toString( - float(0), 'f', self.app.preferences['digits_after_comma'] - ) - localized_mass_minus_1 = QLocale().toString( - float(0), 'f', self.app.preferences['digits_after_comma'] - ) - actual_growth = float(0) - localized_ud_median_time_minus_1 = "####" - - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></div></td><td>{:} {:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:} {:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:} {:}</td></tr> - <tr><td align="right"><b>{:2.2%} / {:} days</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - localized_ud, - self.tr('Universal Dividend UD(t) in'), - self.account.current_ref.instance(0, self.community, self.app, None).diff_units, - localized_mass_minus_1, - self.tr('Monetary Mass M(t-1) in'), - self.account.current_ref.instance(0, self.community, self.app, None).units, - block_ud['membersCount'], - self.tr('Members N(t)'), - localized_mass_minus_1_per_member, - self.tr('Monetary Mass per member M(t-1)/N(t) in'), - self.account.current_ref.instance(0, self.community, self.app, None).diff_units, - actual_growth, - params['dt'] / 86400, - self.tr('Actual growth c = UD(t)/[M(t-1)/N(t)]'), - localized_ud_median_time_minus_1, - self.tr('Penultimate UD date and time (t-1)'), - localized_ud_median_time, - self.tr('Last UD date and time (t)'), - localized_next_ud_median_time, - self.tr('Next UD date and time (t+1)') - ) - ) - else: - self.label_general.setText(self.tr('No Universal Dividend created yet.')) - - if block_ud: - # set infos in label - self.label_rules.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.tr('{:2.0%} / {:} days').format(params['c'], params['dt'] / 86400), - self.tr('Fundamental growth (c) / Delta time (dt)'), - self.tr('UD(t+1) = MAX { UD(t) ; c × M(t) / N(t+1) }'), - self.tr('Universal Dividend (formula)'), - self.tr('{:} = MAX {{ {:} {:} ; {:2.0%} × {:} {:} / {:} }}').format( - localized_ud_plus_1, - localized_ud, - self.account.current_ref.instance(0, self.community, self.app, None).diff_units, - params['c'], - localized_mass, - self.account.current_ref.instance(0, self.community, self.app, None).diff_units, - block_ud['membersCount'] - ), - self.tr('Universal Dividend (computed)') - ) - ) - else: - self.label_rules.setText(self.tr('No Universal Dividend created yet.')) - - # set infos in label - ref_template = """ - <table cellpadding="5"> - <tr><th>{:}</th><td>{:}</td></tr> - <tr><th>{:}</th><td>{:}</td></tr> - <tr><th>{:}</th><td>{:}</td></tr> - <tr><th>{:}</th><td>{:}</td></tr> - </table> - """ - templates = [] - for ref_class in Referentials: - ref = ref_class(0, self.community, self.app, None) - # print(ref_class.__class__.__name__) - # if ref_class.__class__.__name__ == 'RelativeToPast': - # continue - templates.append(ref_template.format(self.tr('Name'), ref.translated_name(), - self.tr('Units'), ref.units, - self.tr('Formula'), ref.formula, - self.tr('Description'), ref.description - ) - ) - - self.label_referentials.setText('<hr>'.join(templates)) - - # set infos in label - self.label_money.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:2.0%} / {:} days</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:} {:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:2.0%}</b></td><td>{:}</td></tr> - </table> - """).format( - params['c'], - params['dt'] / 86400, - self.tr('Fundamental growth (c)'), - params['ud0'], - self.tr('Initial Universal Dividend UD(0) in'), - self.community.short_currency, - params['dt'] / 86400, - self.tr('Time period (dt) in days (86400 seconds) between two UD'), - params['medianTimeBlocks'], - self.tr('Number of blocks used for calculating median time'), - params['avgGenTime'], - self.tr('The average time in seconds for writing 1 block (wished time)'), - params['dtDiffEval'], - self.tr('The number of blocks required to evaluate again PoWMin value'), - params['blocksRot'], - self.tr('The number of previous blocks to check for personalized difficulty'), - params['percentRot'], - self.tr('The percent of previous issuers to reach for personalized difficulty') - ) - ) - - # set infos in label - self.label_wot.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - params['sigPeriod'] / 86400, - self.tr('Minimum delay between 2 certifications (in days)'), - params['sigValidity'] / 86400, - self.tr('Maximum age of a valid signature (in days)'), - params['sigQty'], - self.tr('Minimum quantity of signatures to be part of the WoT'), - params['sigStock'], - self.tr('Maximum quantity of active certifications made by member.'), - params['sigWindow'], - self.tr('Maximum delay a certification can wait before being expired for non-writing.'), - params['xpercent'], - self.tr('Minimum percent of sentries to reach to match the distance rule'), - params['msValidity'] / 86400, - self.tr('Maximum age of a valid membership (in days)'), - params['stepMax'], - self.tr('Maximum distance between each WoT member and a newcomer'), - ) - ) - self.busy.hide() - - def resizeEvent(self, event): - self.busy.resize(event.size()) - super().resizeEvent(event) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh() - return super(InformationsTabWidget, self).changeEvent(event) diff --git a/src/sakia/tests/functional/certification/__init__.py b/src/sakia/gui/main_window/__init__.py similarity index 100% rename from src/sakia/tests/functional/certification/__init__.py rename to src/sakia/gui/main_window/__init__.py diff --git a/src/sakia/gui/main_window/controller.py b/src/sakia/gui/main_window/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..27ff212cb33129bc6aba0679c907bdbb417422f9 --- /dev/null +++ b/src/sakia/gui/main_window/controller.py @@ -0,0 +1,143 @@ +import logging + +from PyQt5.QtWidgets import QMessageBox, QApplication +from PyQt5.QtCore import QEvent, pyqtSlot, QObject +from PyQt5.QtGui import QIcon + +from ..password_asker import PasswordAskerDialog +from ...__init__ import __version__ +from ..widgets import toast +from .view import MainWindowView +from .model import MainWindowModel +from .status_bar.controller import StatusBarController +from .toolbar.controller import ToolbarController +from ..navigation.controller import NavigationController + + +class MainWindowController(QObject): + """ + classdocs + """ + + def __init__(self, view, model, password_asker, status_bar, toolbar, navigation): + """ + Init + :param MainWindowView view: the ui of the mainwindow component + :param sakia.gui.main_window.model.MainWindowModel: the model of the mainwindow component + :param sakia.gui.status_bar.controller.StatusBarController status_bar: the controller of the status bar component + :param sakia.gui.toolbar.controller.ToolbarController toolbar: the controller of the toolbar component + :param sakia.gui.navigation.contoller.NavigationController navigation: the controller of the navigation + + :param PasswordAsker password_asker: the password asker of the application + :type: sakia.core.app.Application + """ + # Set up the user interface from Designer. + super().__init__() + self.view = view + self.model = model + self.initialized = False + self.password_asker = password_asker + self.status_bar = status_bar + self.toolbar = toolbar + self.navigation = navigation + self.stacked_widgets = {} + self.view.bottom_layout.insertWidget(0, self.navigation.view) + self.view.top_layout.addWidget(self.toolbar.view) + self.view.setStatusBar(self.status_bar.view) + + QApplication.setWindowIcon(QIcon(":/icons/sakia_logo")) + + @classmethod + def create(cls, app, password_asker, status_bar, toolbar, navigation): + """ + Instanciate a navigation component + :param sakia.gui.status_bar.controller.StatusBarController status_bar: the controller of the status bar component + :param sakia.gui.toolbar.controller.ToolbarController toolbar: the controller of the toolbar component + :param sakia.gui.navigation.contoller.NavigationController navigation: the controller of the navigation + + :return: a new Navigation controller + :rtype: MainWindowController + """ + view = MainWindowView() + model = MainWindowModel(None, app) + main_window = cls(view, model, password_asker, status_bar, toolbar, navigation) + model.setParent(main_window) + return main_window + + @classmethod + def startup(cls, app): + """ + + :param sakia.app.Application app: + :return: + """ + password_asker = PasswordAskerDialog(None) + navigation = NavigationController.create(None, app) + toolbar = ToolbarController.create(app, navigation) + main_window = cls.create(app, password_asker=password_asker, + status_bar=StatusBarController.create(app), + navigation=navigation, + toolbar=toolbar + ) + currencies = app.db.connections_repo.get_currencies() + if currencies: + currency = currencies[0] + else: + currency = "" + + #app.version_requested.connect(main_window.latest_version_requested) + #app.account_imported.connect(main_window.import_account_accepted) + #app.account_changed.connect(main_window.change_account) + + main_window.view.showMaximized() + main_window.refresh(currency) + return main_window + + @pyqtSlot(str) + def display_error(self, error): + QMessageBox.critical(self, ":(", + error, + QMessageBox.Ok) + + @pyqtSlot(int) + def referential_changed(self, index): + pass + + @pyqtSlot() + def latest_version_requested(self): + latest = self.app.available_version + logging.debug("Latest version requested") + if not latest[0]: + version_info = self.tr("Please get the latest release {version}") \ + .format(version=latest[1]) + version_url = latest[2] + + if self.app.preferences['notifications']: + toast.display("sakia", """{version_info}""".format( + version_info=version_info, + version_url=version_url)) + + def refresh(self, currency): + """ + Refresh main window + When the selected account changes, all the widgets + in the window have to be refreshed + """ + self.status_bar.refresh() + self.toolbar.enable_actions(len(self.navigation.model.navigation[0]['children']) > 0) + self.view.setWindowTitle(self.tr("sakia {0} - {currency}").format(__version__, currency=currency)) + + def eventFilter(self, target, event): + """ + Event filter on the widget + :param QObject target: the target of the event + :param QEvent event: the event + :return: bool + """ + if target == self.widget: + if event.type() == QEvent.LanguageChange: + self.ui.retranslateUi(self) + self.refresh() + return self.widget.eventFilter(target, event) + return False + diff --git a/res/ui/mainwindow.ui b/src/sakia/gui/main_window/mainwindow.ui similarity index 62% rename from res/ui/mainwindow.ui rename to src/sakia/gui/main_window/mainwindow.ui index d92357d930a1c8257d97ae0eaa38dbe2dfe9b4f5..47a6ed732665c0bcb738dfd657fd4bd7d7572934 100644 --- a/res/ui/mainwindow.ui +++ b/src/sakia/gui/main_window/mainwindow.ui @@ -14,78 +14,15 @@ <string notr="true">Sakia</string> </property> <widget class="QWidget" name="centralwidget"> - <layout class="QVBoxLayout" name="verticalLayout_6"/> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <layout class="QHBoxLayout" name="top_layout"/> + </item> + <item> + <layout class="QHBoxLayout" name="bottom_layout"/> + </item> + </layout> </widget> - <widget class="QMenuBar" name="menubar"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>900</width> - <height>30</height> - </rect> - </property> - <widget class="QMenu" name="menu_file"> - <property name="title"> - <string>Fi&le</string> - </property> - <addaction name="action_import"/> - <addaction name="action_export"/> - <addaction name="separator"/> - <addaction name="actionPreferences"/> - <addaction name="action_quit"/> - </widget> - <widget class="QMenu" name="menu_account"> - <property name="title"> - <string>Acco&unt</string> - </property> - <widget class="QMenu" name="menu_contacts_list"> - <property name="title"> - <string>Co&ntacts</string> - </property> - <addaction name="separator"/> - </widget> - <widget class="QMenu" name="menu_change_account"> - <property name="title"> - <string>&Open</string> - </property> - </widget> - <widget class="QMenu" name="menuAdvanced"> - <property name="title"> - <string>Advanced</string> - </property> - <addaction name="action_revoke_identity"/> - </widget> - <addaction name="menu_change_account"/> - <addaction name="action_configure_parameters"/> - <addaction name="action_add_account"/> - <addaction name="separator"/> - <addaction name="actionCertification"/> - <addaction name="actionTransfer_money"/> - <addaction name="separator"/> - <addaction name="action_add_a_contact"/> - <addaction name="menu_contacts_list"/> - <addaction name="separator"/> - <addaction name="menuAdvanced"/> - </widget> - <widget class="QMenu" name="menu_help"> - <property name="title"> - <string>&Help</string> - </property> - <addaction name="actionAbout"/> - </widget> - <widget class="QMenu" name="menu_duniter"> - <property name="title"> - <string>&Duniter</string> - </property> - <addaction name="actionManage_local_node"/> - </widget> - <addaction name="menu_file"/> - <addaction name="menu_account"/> - <addaction name="menu_duniter"/> - <addaction name="menu_help"/> - </widget> - <widget class="QStatusBar" name="statusbar"/> <action name="actionManage_accounts"> <property name="text"> <string>Manage accounts</string> @@ -198,12 +135,12 @@ </action> <action name="action_revoke_identity"> <property name="text"> - <string>Revoke an identity</string> + <string>&Revoke an identity</string> </property> </action> </widget> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../res/icons/icons.qrc"/> </resources> <connections/> <slots> diff --git a/src/sakia/gui/main_window/model.py b/src/sakia/gui/main_window/model.py new file mode 100644 index 0000000000000000000000000000000000000000..81714afea5e593e32bdb1aea37d63e4d47cef4f4 --- /dev/null +++ b/src/sakia/gui/main_window/model.py @@ -0,0 +1,12 @@ +from PyQt5.QtCore import QObject + + +class MainWindowModel(QObject): + """ + The model of Navigation component + """ + + def __init__(self, parent, app): + super().__init__(parent) + self.app = app + diff --git a/src/sakia/tests/functional/identities_tab/__init__.py b/src/sakia/gui/main_window/status_bar/__init__.py similarity index 100% rename from src/sakia/tests/functional/identities_tab/__init__.py rename to src/sakia/gui/main_window/status_bar/__init__.py diff --git a/src/sakia/gui/main_window/status_bar/controller.py b/src/sakia/gui/main_window/status_bar/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..16c4e6d2eeed9c59e2955864dd530be3b750323e --- /dev/null +++ b/src/sakia/gui/main_window/status_bar/controller.py @@ -0,0 +1,65 @@ +from PyQt5.QtCore import QLocale, pyqtSlot, QDateTime, QTimer, QObject +from .model import StatusBarModel +from .view import StatusBarView +import logging + + +class StatusBarController(QObject): + """ + The navigation panel + """ + + def __init__(self, view, model): + """ + Constructor of the navigation component + + :param sakia.gui.status_bar.view.StatusBarView view: the presentation + :param sakia.core.status_bar.model.StatusBarModel model: the model + """ + super().__init__() + self.view = view + self.model = model + view.combo_referential.currentIndexChanged[int].connect(self.referential_changed) + self.update_time() + + @classmethod + def create(cls, app): + """ + Instanciate a navigation component + :param sakia.gui.main_window.controller.MainWindowController parent: + :return: a new Navigation controller + :rtype: NavigationController + """ + view = StatusBarView(None) + + model = StatusBarModel(None, app) + status_bar = cls(view, model) + return status_bar + + @pyqtSlot() + def update_time(self): + dateTime = QDateTime.currentDateTime() + self.view.label_time.setText("{0}".format(QLocale.toString( + QLocale(), + QDateTime.currentDateTime(), + QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) + ))) + timer = QTimer() + timer.timeout.connect(self.update_time) + timer.start(1000) + + def refresh(self): + """ + Refresh main window + When the selected account changes, all the widgets + in the window have to be refreshed + """ + logging.debug("Refresh started") + for ref in self.model.referentials(): + self.view.combo_referential.addItem(ref.translated_name()) + + self.view.combo_referential.setCurrentIndex(self.model.default_referential()) + + @pyqtSlot(int) + def referential_changed(self, index): + self.model.app.change_referential(index) \ No newline at end of file diff --git a/src/sakia/gui/main_window/status_bar/model.py b/src/sakia/gui/main_window/status_bar/model.py new file mode 100644 index 0000000000000000000000000000000000000000..f55202aa76019d2a6af704a4a6f2bbb24e4e993a --- /dev/null +++ b/src/sakia/gui/main_window/status_bar/model.py @@ -0,0 +1,23 @@ +from PyQt5.QtCore import QObject +from sakia.money import Referentials + + +class StatusBarModel(QObject): + """ + The model of status bar component + """ + + def __init__(self, parent, app): + """ + The status bar model + :param parent: + :param sakia.app.Application app: the app + """ + super().__init__(parent) + self.app = app + + def referentials(self): + return Referentials + + def default_referential(self): + return self.app.parameters.referential diff --git a/src/sakia/gui/main_window/status_bar/view.py b/src/sakia/gui/main_window/status_bar/view.py new file mode 100644 index 0000000000000000000000000000000000000000..f2751520667c30895b4d71d841838f169456010a --- /dev/null +++ b/src/sakia/gui/main_window/status_bar/view.py @@ -0,0 +1,25 @@ +from PyQt5.QtWidgets import QStatusBar +from PyQt5.QtWidgets import QLabel, QComboBox +from PyQt5.QtCore import Qt + + +class StatusBarView(QStatusBar): + """ + The model of Navigation component + """ + + def __init__(self, parent): + super().__init__(parent) + self.label_icon = QLabel("", parent) + + self.status_label = QLabel("", parent) + self.status_label.setTextFormat(Qt.RichText) + + self.label_time = QLabel("", parent) + + self.combo_referential = QComboBox(parent) + + self.addPermanentWidget(self.label_icon, 1) + self.addPermanentWidget(self.status_label, 2) + self.addPermanentWidget(self.label_time) + self.addPermanentWidget(self.combo_referential) \ No newline at end of file diff --git a/src/sakia/tests/functional/main_window/__init__.py b/src/sakia/gui/main_window/toolbar/__init__.py similarity index 100% rename from src/sakia/tests/functional/main_window/__init__.py rename to src/sakia/gui/main_window/toolbar/__init__.py diff --git a/res/ui/about.ui b/src/sakia/gui/main_window/toolbar/about.ui similarity index 100% rename from res/ui/about.ui rename to src/sakia/gui/main_window/toolbar/about.ui diff --git a/src/sakia/gui/main_window/toolbar/controller.py b/src/sakia/gui/main_window/toolbar/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..8c3fc1966867de4e07f6cc59ae5387bc04704633 --- /dev/null +++ b/src/sakia/gui/main_window/toolbar/controller.py @@ -0,0 +1,115 @@ +from PyQt5.QtCore import Qt, QObject +from PyQt5.QtWidgets import QDialog, QMessageBox + +from sakia.decorators import asyncify +from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController +from sakia.gui.dialogs.certification.controller import CertificationController +from sakia.gui.dialogs.revocation.controller import RevocationController +from sakia.gui.dialogs.transfer.controller import TransferController +from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox, QAsyncFileDialog, dialog_async_exec +from sakia.gui.password_asker import PasswordAskerDialog +from sakia.gui.preferences import PreferencesDialog +from .model import ToolbarModel +from .view import ToolbarView + + +class ToolbarController(QObject): + """ + The navigation panel + """ + + def __init__(self, view, model): + """ + :param sakia.gui.component.controller.ComponentController parent: the parent + :param sakia.gui.main_window.toolbar.view.ToolbarView view: + :param sakia.gui.main_window.toolbar.model.ToolbarModel model: + """ + super().__init__() + self.view = view + self.model = model + self.view.button_certification.clicked.connect(self.open_certification_dialog) + self.view.button_send_money.clicked.connect(self.open_transfer_money_dialog) + self.view.button_membership.clicked.connect(self.send_join_demand) + self.view.action_add_connection.triggered.connect(self.open_add_connection_dialog) + self.view.action_parameters.triggered.connect(self.open_settings_dialog) + self.view.action_about.triggered.connect(self.open_about_dialog) + self.view.action_revoke_uid.triggered.connect(self.open_revocation_dialog) + + @classmethod + def create(cls, app, navigation): + """ + Instanciate a navigation component + :param sakia.app.Application app: + :param sakia.gui.navigation.controller.NavigationController navigation: + :return: a new Navigation controller + :rtype: NavigationController + """ + view = ToolbarView(None) + model = ToolbarModel(app, navigation.model) + toolbar = cls(view, model) + return toolbar + + def enable_actions(self, enabled): + self.view.button_certification.setEnabled(enabled) + self.view.button_send_money.setEnabled(enabled) + self.view.button_membership.setEnabled(enabled) + + @asyncify + async def send_join_demand(self, checked=False): + connection = await self.view.ask_for_connection(self.model.connections()) + if not connection: + return + password = await PasswordAskerDialog(connection).async_exec() + if not password: + return + result = await self.model.send_join(connection, password) + if result[0]: + if self.model.notifications(): + toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) + else: + await QAsyncMessageBox.information(self, self.tr("Membership"), + self.tr("Success sending Membership demand")) + else: + if self.model.notifications(): + toast.display(self.tr("Membership"), result[1]) + else: + await QAsyncMessageBox.critical(self, self.tr("Membership"), + result[1]) + + def open_certification_dialog(self): + CertificationController.open_dialog(self, self.model.app, + self.model.navigation_model.current_connection()) + + def open_revocation_dialog(self): + RevocationController.open_dialog(self, self.model.app, self.model.navigation_model.current_connection()) + + def open_transfer_money_dialog(self): + TransferController.open_dialog(self, self.model.app, self.model.navigation_model.current_connection()) + + def open_settings_dialog(self): + PreferencesDialog(self.model.app).exec() + + def open_add_connection_dialog(self): + connection_config = ConnectionConfigController.create_connection(self, self.model.app) + connection_config.exec() + if connection_config.view.result() == QDialog.Accepted: + self.model.app.instanciate_services() + self.model.app.start_coroutines() + self.model.app.new_connection.emit(connection_config.model.connection) + self.enable_actions(True) + + def open_about_dialog(self): + text = self.model.about_text() + self.view.show_about(text) + + def retranslateUi(self, widget): + """ + Method to complete translations missing from generated code + :param widget: + :return: + """ + self.action_publish_uid.setText(self.tr(ToolbarController.action_publish_uid_text)) + self.action_revoke_uid.setText(self.tr(ToolbarController.action_revoke_uid_text)) + self.action_showinfo.setText(self.tr(ToolbarController.action_showinfo_text)) + super().retranslateUi(self) diff --git a/src/sakia/gui/main_window/toolbar/model.py b/src/sakia/gui/main_window/toolbar/model.py new file mode 100644 index 0000000000000000000000000000000000000000..abf8adb0500a8f67df99214671bc8179c0067d16 --- /dev/null +++ b/src/sakia/gui/main_window/toolbar/model.py @@ -0,0 +1,62 @@ +from PyQt5.QtCore import QObject +from sakia.data.processors import ConnectionsProcessor +import attr +from sakia import __version__ + + +@attr.s() +class ToolbarModel(QObject): + """ + The model of Navigation component + + :param sakia.app.Application app: the application + :param sakia.gui.navigation.model.NavigationModel navigation_model: The navigation model + """ + + app = attr.ib() + navigation_model = attr.ib() + + def __attrs_post_init__(self): + super().__init__() + + async def send_join(self, connection, password): + return await self.app.documents_service.send_membership(connection, password, "IN") + + def notifications(self): + return self.app.parameters.notifications + + def connections(self): + return ConnectionsProcessor.instanciate(self.app).connections() + + def about_text(self): + latest = self.app.available_version + version_info = "" + version_url = "" + if not latest[0]: + version_info = "Latest release : {version}" \ + .format(version=latest[1]) + version_url = latest[2] + + new_version_text = """ + <p><b>{version_info}</b></p> + <p><a href={version_url}>Download link</a></p> + """.format(version_info=version_info, + version_url=version_url) + return """ + <h1>Sakia</h1> + + <p>Python/Qt Duniter client</p> + + <p>Version : {:}</p> + {new_version_text} + + <p>License : GPLv3</p> + + <p><b>Authors</b></p> + + <p>inso</p> + <p>vit</p> + <p>canercandan</p> + <p>Moul</p> + """.format(__version__, + new_version_text=new_version_text) \ No newline at end of file diff --git a/src/sakia/gui/main_window/toolbar/toolbar.ui b/src/sakia/gui/main_window/toolbar/toolbar.ui new file mode 100644 index 0000000000000000000000000000000000000000..39850025c90911ad23c57e76d28c2c2c7c1480a8 --- /dev/null +++ b/src/sakia/gui/main_window/toolbar/toolbar.ui @@ -0,0 +1,125 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SakiaToolbar</class> + <widget class="QFrame" name="SakiaToolbar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>666</width> + <height>237</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Frame</string> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="button_send_money"> + <property name="text"> + <string>Send money</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/payment_icon</normaloff>:/icons/payment_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_certification"> + <property name="text"> + <string>Certification</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/certification_icon</normaloff>:/icons/certification_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_membership"> + <property name="text"> + <string>Renew membership</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>1000</width> + <height>221</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="toolbutton_menu"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/menu_icon</normaloff>:/icons/menu_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + <property name="popupMode"> + <enum>QToolButton::InstantPopup</enum> + </property> + <property name="autoRaise"> + <bool>false</bool> + </property> + <property name="arrowType"> + <enum>Qt::NoArrow</enum> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../res/icons/icons.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/sakia/gui/main_window/toolbar/view.py b/src/sakia/gui/main_window/toolbar/view.py new file mode 100644 index 0000000000000000000000000000000000000000..aff49df9eaeec92d5644b6a5d35c78b9bf0aebba --- /dev/null +++ b/src/sakia/gui/main_window/toolbar/view.py @@ -0,0 +1,56 @@ +from PyQt5.QtWidgets import QFrame, QAction, QMenu, QSizePolicy, QInputDialog, QDialog +from sakia.gui.widgets.dialogs import dialog_async_exec +from PyQt5.QtCore import QObject, QT_TRANSLATE_NOOP, Qt +from .toolbar_uic import Ui_SakiaToolbar +from .about_uic import Ui_AboutPopup + + +class ToolbarView(QFrame, Ui_SakiaToolbar): + """ + The model of Navigation component + """ + _action_publish_uid_text = QT_TRANSLATE_NOOP("ToolbarView", "Publish UID") + _action_revoke_uid_text = QT_TRANSLATE_NOOP("ToolbarView", "Revoke UID") + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) + + tool_menu = QMenu(self.tr("Tools"), self.toolbutton_menu) + self.toolbutton_menu.setMenu(tool_menu) + + self.action_revoke_uid = QAction(self.tr(ToolbarView._action_revoke_uid_text), self) + tool_menu.addAction(self.action_revoke_uid) + + self.action_add_connection = QAction(self.tr("Add a connection"), tool_menu) + tool_menu.addAction(self.action_add_connection) + + self.action_parameters = QAction(self.tr("Settings"), tool_menu) + tool_menu.addAction(self.action_parameters) + + self.action_about = QAction(self.tr("About"), tool_menu) + tool_menu.addAction(self.action_about) + + self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Minimum) + self.setMaximumHeight(60) + + async def ask_for_connection(self, connections): + connections_titles = [c.title() for c in connections] + input_dialog = QInputDialog() + input_dialog.setComboBoxItems(connections_titles) + input_dialog.setWindowTitle(self.tr("Membership")) + input_dialog.setLabelText(self.tr("Select a connection")) + await dialog_async_exec(input_dialog) + result = input_dialog.textValue() + + if input_dialog.result() == QDialog.Accepted: + for c in connections: + if c.title() == result: + return c + + def show_about(self, text): + dialog = QDialog(self) + about_dialog = Ui_AboutPopup() + about_dialog.setupUi(dialog) + about_dialog.label.setText(text) + dialog.exec() diff --git a/src/sakia/gui/main_window/view.py b/src/sakia/gui/main_window/view.py new file mode 100644 index 0000000000000000000000000000000000000000..024a392eabe5ec46580a1084c58aa25674210443 --- /dev/null +++ b/src/sakia/gui/main_window/view.py @@ -0,0 +1,13 @@ +from PyQt5.QtWidgets import QMainWindow +from .mainwindow_uic import Ui_MainWindow + + +class MainWindowView(QMainWindow, Ui_MainWindow): + """ + The model of Navigation component + """ + + def __init__(self): + super().__init__(None) + self.setupUi(self) + diff --git a/src/sakia/gui/mainwindow.py b/src/sakia/gui/mainwindow.py deleted file mode 100644 index 5d66b78e060f2d8ad39799d10ec09d94e7944b9a..0000000000000000000000000000000000000000 --- a/src/sakia/gui/mainwindow.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" -import aiohttp -import logging -import traceback - -from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog, \ - QMessageBox, QLabel, QComboBox, QDialog, QApplication, QErrorMessage -from PyQt5.QtCore import QLocale, QEvent, \ - pyqtSlot, QDateTime, QTimer, Qt, QObject, QUrl -from PyQt5.QtGui import QIcon - -from ..gen_resources.mainwindow_uic import Ui_MainWindow -from ..gen_resources.about_uic import Ui_AboutPopup -from .process_cfg_account import ProcessConfigureAccount -from .transfer import TransferMoneyDialog -from .community_view import CommunityWidget -from .contact import ConfigureContactDialog -from .import_account import ImportAccountDialog -from .certification import CertificationDialog -from .revocation import RevocationDialog -from .password_asker import PasswordAskerDialog -from .preferences import PreferencesDialog -from .process_cfg_community import ProcessConfigureCommunity -from .homescreen import HomeScreenWidget -from .node_manager import NodeManager -from ..core import money -from ..core.community import Community -from ..tools.decorators import asyncify -from ..__init__ import __version__ -from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox - - -class MainWindow(QObject): - """ - classdocs - """ - - def __init__(self, app, account, homescreen, community_view, node_manager, - widget, ui, - label_icon, label_status, label_time, combo_referential, - password_asker): - """ - Init - :param sakia.core.app.Application app: application - :param sakia.core.Account account: the account - :param HomeScreenWidgetcreen homescreen: the homescreen - :param CommunityView community_view: the community view - :param NodeManager node_manager: the local node manager dialog - :param QMainWindow widget: the widget of the main window - :param Ui_MainWindow ui: the ui of the widget - :param QLabel label_icon: the label of the icon in the statusbar - :param QLabel label_status: the label of the status in the statusbar - :param QLabel label_time: the label of the time in the statusbar - :param QCombobox combo_referential: the combo of the referentials in the statusbar - :param PasswordAsker password_asker: the password asker of the application - :type: sakia.core.app.Application - """ - # Set up the user interface from Designer. - super().__init__() - self.app = app - self.account = account - self.initialized = False - self.password_asker = password_asker - self.import_dialog = None - self.widget = widget - self.ui = ui - self.ui.setupUi(self.widget) - self.widget.installEventFilter(self) - - QApplication.setWindowIcon(QIcon(":/icons/sakia_logo")) - - self.label_icon = label_icon - - self.status_label = label_status - self.status_label.setTextFormat(Qt.RichText) - - self.label_time = label_time - - self.combo_referential = combo_referential - self.combo_referential.setEnabled(False) - self.combo_referential.currentIndexChanged[int].connect(self.referential_changed) - - self.homescreen = homescreen - - self.community_view = community_view - - self.node_manager = node_manager - - - def _init_ui(self): - """ - Connects elements of the UI to the local slots - """ - self.ui.statusbar.addPermanentWidget(self.label_icon, 1) - self.ui.statusbar.addPermanentWidget(self.status_label, 2) - self.ui.statusbar.addPermanentWidget(self.label_time) - self.ui.statusbar.addPermanentWidget(self.combo_referential) - - self.ui.action_add_account.triggered.connect(self.open_add_account_dialog) - self.ui.action_quit.triggered.connect(self.widget.close) - self.ui.actionTransfer_money.triggered.connect(self.open_transfer_money_dialog) - self.ui.action_add_a_contact.triggered.connect(self.open_add_contact_dialog) - self.ui.action_configure_parameters.triggered.connect(self.open_configure_account_dialog) - self.ui.action_import.triggered.connect(self.import_account) - self.ui.action_export.triggered.connect(self.export_account) - self.ui.actionCertification.triggered.connect(self.open_certification_dialog) - self.ui.actionPreferences.triggered.connect(self.open_preferences_dialog) - self.ui.actionAbout.triggered.connect(self.open_about_popup) - self.ui.action_revoke_identity.triggered.connect(self.open_revocation_dialog) - - self.ui.actionManage_local_node.triggered.connect(self.open_duniter_ui) - self.ui.menu_duniter.setDisabled(True) - - def _init_homescreen(self): - """ - Initialize homescreen signals/slots and data - :return: - """ - self.homescreen.status_label = self.status_label - self.homescreen.frame_communities.community_tile_clicked.connect(self.change_community) - self.homescreen.toolbutton_new_account.clicked.connect(self.open_add_account_dialog) - self.homescreen.toolbutton_new_account.addAction(self.ui.action_add_account) - self.homescreen.toolbutton_new_account.addAction(self.ui.action_import) - self.homescreen.button_add_community.clicked.connect(self.action_open_add_community) - self.homescreen.button_disconnect.clicked.connect(lambda :self.action_change_account("")) - self.widget.centralWidget().layout().addWidget(self.homescreen) - self.homescreen.toolbutton_connect.setMenu(self.ui.menu_change_account) - - def _init_community_view(self): - """ - Initialize the community view signals/slots and data - :return: - """ - self.community_view.status_label = self.status_label - self.community_view.label_icon = self.label_icon - self.community_view.button_home.clicked.connect(lambda: self.change_community(None)) - self.community_view.button_certification.clicked.connect(self.open_certification_dialog) - self.community_view.button_send_money.clicked.connect(self.open_transfer_money_dialog) - self.widget.centralWidget().layout().addWidget(self.community_view) - - @classmethod - def startup(cls, app): - qmainwindow = QMainWindow() - - main_window = cls(app, None, HomeScreenWidget(app, None), - CommunityWidget(app, None, None), - None, #NodeManager.create(qmainwindow), - qmainwindow, Ui_MainWindow(), - QLabel("", qmainwindow), QLabel("", qmainwindow), - QLabel("", qmainwindow), QComboBox(qmainwindow), - PasswordAskerDialog(None)) - app.version_requested.connect(main_window.latest_version_requested) - app.account_imported.connect(main_window.import_account_accepted) - app.account_changed.connect(main_window.change_account) - main_window._init_ui() - main_window._init_homescreen() - main_window._init_community_view() - - main_window.update_time() - if app.preferences['maximized']: - main_window.widget.showMaximized() - else: - main_window.widget.show() - if app.current_account: - main_window.password_asker = PasswordAskerDialog(app.current_account) - main_window.community_view.change_account(app.current_account, main_window.password_asker) - main_window.app.current_account.contacts_changed.connect(main_window.refresh_contacts) - main_window.refresh() - return main_window - - def change_account(self): - if self.account: - self.account.contacts_changed.disconnect(self.refresh_contacts) - self.account = self.app.current_account - self.password_asker.change_account(self.account) - self.community_view.change_account(self.account, self.password_asker) - if self.account: - self.account.contacts_changed.connect(self.refresh_contacts) - self.refresh() - - @asyncify - async def open_add_account_dialog(self, checked=False): - dialog = ProcessConfigureAccount(self.app, None) - result = await dialog.async_exec() - if result == QDialog.Accepted: - self.action_change_account(self.account.name) - - @asyncify - async def open_configure_account_dialog(self, checked=False): - dialog = ProcessConfigureAccount(self.app, self.account) - result = await dialog.async_exec() - if result == QDialog.Accepted: - if self.account: - self.action_change_account(self.account.name) - else: - self.refresh() - - @pyqtSlot(str) - def display_error(self, error): - QMessageBox.critical(self, ":(", - error, - QMessageBox.Ok) - - @pyqtSlot(int) - def referential_changed(self, index): - if self.account: - self.account.set_display_referential(index) - if self.community_view: - self.community_view.referential_changed() - self.homescreen.referential_changed() - - @pyqtSlot() - def update_time(self): - dateTime = QDateTime.currentDateTime() - self.label_time.setText("{0}".format(QLocale.toString( - QLocale(), - QDateTime.currentDateTime(), - QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) - ))) - timer = QTimer() - timer.timeout.connect(self.update_time) - timer.start(1000) - - @pyqtSlot() - def delete_contact(self): - contact = self.sender().data() - self.account.remove_contact(contact) - - @pyqtSlot() - def edit_contact(self): - index = self.sender().data() - dialog = ConfigureContactDialog.edit_contact(self.app, self.account, self.widget, index) - dialog.exec_() - - def action_change_account(self, account_name): - self.app.change_current_account(self.app.get_account(account_name)) - - @pyqtSlot() - def action_open_add_community(self): - dialog = ProcessConfigureCommunity(self.app, - self.account, None, - self.password_asker) - if dialog.exec_() == QDialog.Accepted: - self.app.save(self.account) - dialog.community.start_coroutines() - self.homescreen.refresh() - - def open_transfer_money_dialog(self): - dialog = TransferMoneyDialog(self.app, - self.account, - self.password_asker, - self.community_view.community, - None) - if dialog.exec() == QDialog.Accepted: - self.community_view.tab_history.table_history.model().sourceModel().refresh_transfers() - - def open_certification_dialog(self): - CertificationDialog.open_dialog(self.app, - self.account, - self.community_view.community, - self.password_asker) - - def open_revocation_dialog(self): - RevocationDialog.open_dialog(self.app, - self.account) - - def open_add_contact_dialog(self): - dialog = ConfigureContactDialog.new_contact(self.app, self.account, self.widget) - dialog.exec_() - - def open_preferences_dialog(self): - dialog = PreferencesDialog(self.app) - dialog.exec_() - - def open_about_popup(self): - """ - Open about popup window - """ - aboutDialog = QDialog(self.widget) - aboutUi = Ui_AboutPopup() - aboutUi.setupUi(aboutDialog) - - latest = self.app.available_version - version_info = "" - version_url = "" - if not latest[0]: - version_info = self.tr("Latest release : {version}") \ - .format(version=latest[1]) - version_url = latest[2] - - new_version_text = """ - <p><b>{version_info}</b></p> - <p><a href="{version_url}">{link_text}</a></p> - """.format(version_info=version_info, - version_url=version_url, - link_text=self.tr("Download link")) - else: - new_version_text = "" - - text = self.tr(""" - <h1>sakia</h1> - - <p>Python/Qt duniter client</p> - <p><a href="https://github.com/duniter/sakia">https://github.com/duniter/sakia</a></p> - - <p>Version : {:}</p> - {new_version_text} - - <p>License : GPLv3</p> - - <p><b>Authors</b></p> - - <p>inso</p> - <p>vit</p> - <p>Moul</p> - <p>canercandan</p> - """).format(__version__, new_version_text=new_version_text) - - aboutUi.label.setText(text) - aboutDialog.show() - - @pyqtSlot() - def latest_version_requested(self): - latest = self.app.available_version - logging.debug("Latest version requested") - if not latest[0]: - version_info = self.tr("Please get the latest release {version}") \ - .format(version=latest[1]) - version_url = latest[2] - - if self.app.preferences['notifications']: - toast.display("sakia", """{version_info}""".format( - version_info=version_info, - version_url=version_url)) - - @pyqtSlot(Community) - def change_community(self, community): - if community: - self.homescreen.hide() - self.community_view.show() - else: - self.community_view.hide() - self.homescreen.show() - - self.community_view.change_community(community) - - def refresh_accounts(self): - self.ui.menu_change_account.clear() - for account_name in sorted(self.app.accounts.keys()): - action = QAction(account_name, self.widget) - action.triggered.connect(lambda checked, account_name=account_name: self.action_change_account(account_name)) - self.ui.menu_change_account.addAction(action) - - def refresh_contacts(self): - self.ui.menu_contacts_list.clear() - if self.account: - for index, contact in enumerate(self.account.contacts): - contact_menu = self.ui.menu_contacts_list.addMenu(contact['name']) - edit_action = contact_menu.addAction(self.tr("Edit")) - edit_action.triggered.connect(self.edit_contact) - edit_action.setData(index) - delete_action = contact_menu.addAction(self.tr("Delete")) - delete_action.setData(contact) - delete_action.triggered.connect(self.delete_contact) - - @asyncify - async def open_duniter_ui(self, checked=False): - if not self.node_manager.widget.isVisible(): - self.node_manager.open_home_page() - - def refresh(self): - """ - Refresh main window - When the selected account changes, all the widgets - in the window have to be refreshed - """ - logging.debug("Refresh started") - self.refresh_accounts() - self.community_view.hide() - self.homescreen.show() - self.homescreen.refresh() - - if self.account is None: - self.widget.setWindowTitle(self.tr("sakia {0}").format(__version__)) - self.ui.action_add_a_contact.setEnabled(False) - self.ui.actionCertification.setEnabled(False) - self.ui.actionTransfer_money.setEnabled(False) - self.ui.action_configure_parameters.setEnabled(False) - self.ui.action_set_as_default.setEnabled(False) - self.ui.menu_contacts_list.setEnabled(False) - self.combo_referential.setEnabled(False) - self.status_label.setText(self.tr("")) - else: - self.combo_referential.blockSignals(True) - self.combo_referential.clear() - for ref in money.Referentials: - self.combo_referential.addItem(ref.translated_name()) - logging.debug(self.app.preferences) - - self.combo_referential.setEnabled(True) - self.combo_referential.blockSignals(False) - self.combo_referential.setCurrentIndex(self.app.preferences['ref']) - self.ui.action_add_a_contact.setEnabled(True) - self.ui.actionCertification.setEnabled(True) - self.ui.actionTransfer_money.setEnabled(True) - self.ui.menu_contacts_list.setEnabled(True) - self.ui.action_configure_parameters.setEnabled(True) - self.widget.setWindowTitle(self.tr("sakia {0} - Account : {1}").format(__version__, - self.account.name)) - - self.refresh_contacts() - - def import_account(self): - import_dialog = ImportAccountDialog(self.app, self.widget) - import_dialog.exec_() - - def import_account_accepted(self, account_name): - # open account after import - self.action_change_account(account_name) - - def export_account(self): - # Testable way of using a QFileDialog - export_dialog = QFileDialog(self.widget) - export_dialog.setObjectName('ExportFileDialog') - export_dialog.setWindowTitle(self.tr("Export an account")) - export_dialog.setNameFilter(self.tr("All account files (*.acc)")) - export_dialog.setLabelText(QFileDialog.Accept, self.tr('Export')) - export_dialog.setOption(QFileDialog.DontUseNativeDialog, True) - export_dialog.accepted.connect(self.export_account_accepted) - export_dialog.show() - - def export_account_accepted(self): - export_dialog = self.sender() - selected_file = export_dialog.selectedFiles() - if selected_file: - if selected_file[0][-4:] == ".acc": - path = selected_file[0] - else: - path = selected_file[0] + ".acc" - self.app.export_account(path, self.account) - - def eventFilter(self, target, event): - """ - Event filter on the widget - :param QObject target: the target of the event - :param QEvent event: the event - :return: bool - """ - if target == self.widget: - if event.type() == QEvent.LanguageChange: - self.ui.retranslateUi(self) - self.refresh() - return self.widget.eventFilter(target, event) - return False - diff --git a/src/sakia/gui/member.py b/src/sakia/gui/member.py deleted file mode 100644 index 5d2744dea1d3e3e9953e514727da816f423814bd..0000000000000000000000000000000000000000 --- a/src/sakia/gui/member.py +++ /dev/null @@ -1,155 +0,0 @@ -import datetime - -from PyQt5.QtCore import QObject, QEvent, QLocale, QDateTime -from PyQt5.QtWidgets import QDialog, QWidget - -from ..core.graph import WoTGraph -from .widgets.busy import Busy -from ..tools.decorators import asyncify -from ..gen_resources.member_uic import Ui_MemberView -from ..tools.exceptions import MembershipNotFoundError, LookupFailureError, NoPeerAvailable -from ..core.registry import LocalState - - -class MemberDialog(QObject): - """ - A widget showing informations about a member - """ - - def __init__(self, app, account, community, identity, widget, ui): - """ - Init MemberDialog - - :param sakia.core.app.Application app: Application instance - :param sakia.core.account.Account account: Account instance - :param sakia.core.community.Community community: Community instance - :param sakia.core.registry.identity.Identity identity: Identity instance - :param PyQt5.QtWidget widget: The class of the widget - :param sakia.gen_resources.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget - :return: - """ - super().__init__() - self.widget = widget - self.ui = ui - self.ui.setupUi(self.widget) - self.ui.busy = Busy(self.widget) - self.widget.installEventFilter(self) - self.app = app - self.community = community - self.account = account - self.identity = identity - - @classmethod - def open_dialog(cls, app, account, community, identity): - dialog = cls(app, account, community, identity, QDialog(), Ui_MemberView()) - dialog.refresh() - dialog.refresh_path() - dialog.exec() - - @classmethod - def as_widget(cls, parent_widget, app, account, community, identity): - return cls(app, account, community, identity, QWidget(parent_widget), Ui_MemberView()) - - def change_community(self, community): - """ - Change current community - :param sakia.core.Community community: the new community - """ - self.community = community - self.refresh() - - @asyncify - async def refresh(self): - if self.identity and self.identity.local_state != LocalState.NOT_FOUND: - self.ui.busy.show() - self.ui.label_uid.setText(self.identity.uid) - self.ui.label_properties.setText("") - try: - identity_selfcert = await self.identity.selfcert(self.community) - publish_time = await self.community.time(identity_selfcert.timestamp.number) - - join_date = await self.identity.get_join_date(self.community) - if join_date is None: - join_date = self.tr('not a member') - else: - join_date = datetime.datetime.fromtimestamp(join_date).strftime("%d/%m/%Y %I:%M") - - except MembershipNotFoundError: - join_date = "###" - except (LookupFailureError, NoPeerAvailable): - publish_time = None - join_date = "###" - - if publish_time: - uid_publish_date = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(publish_time), - QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) - ) - else: - uid_publish_date = "###" - - text = self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> - """).format( - self.tr('Public key'), - self.identity.pubkey, - self.tr('UID Published on'), - uid_publish_date, - self.tr('Join date'), - join_date - ) - # close html text - text += "</table>" - - # set text in label - self.ui.label_properties.setText(text) - self.ui.busy.hide() - - @asyncify - async def refresh_path(self): - text = "" - self.ui.label_path.setText("") - # calculate path to account member - graph = WoTGraph(self.app, self.community) - path = None - # if selected member is not the account member... - if self.identity.pubkey != self.account.pubkey: - # add path from us to him - account_identity = await self.account.identity(self.community) - path = await graph.get_shortest_path_to_identity(self.identity, - account_identity) - nodes = graph.nx_graph.nodes(data=True) - - if path: - distance = len(path) - 1 - text += self.tr( - """<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""" - .format(self.tr('Distance'), distance)) - if distance > 1: - index = 0 - for node_id in path: - node = [n for n in nodes if n[0] == node_id][0] - if index == 0: - text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""")\ - .format(self.tr('Path'), node[1]['text']) - else: - text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""")\ - .format('', node[1]['text'] ) - if index == distance and node_id != self.account.pubkey: - text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""")\ - .format('', self.account.name) - index += 1 - self.ui.label_path.setText(text) - - def eventFilter(self, source, event): - if event.type() == QEvent.Resize: - self.ui.busy.resize(event.size()) - self.widget.resizeEvent(event) - return self.widget.eventFilter(source, event) - - def exec(self): - self.widget.exec() diff --git a/src/sakia/tests/functional/preferences/__init__.py b/src/sakia/gui/navigation/__init__.py similarity index 100% rename from src/sakia/tests/functional/preferences/__init__.py rename to src/sakia/gui/navigation/__init__.py diff --git a/src/sakia/gui/navigation/controller.py b/src/sakia/gui/navigation/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..e2d21ba33b97028eb4b4ce69185f037f944d052c --- /dev/null +++ b/src/sakia/gui/navigation/controller.py @@ -0,0 +1,214 @@ +from .model import NavigationModel +from .view import NavigationView +from sakia.models.generic_tree import GenericTreeModel +from .txhistory.controller import TxHistoryController +from .homescreen.controller import HomeScreenController +from .network.controller import NetworkController +from .identities.controller import IdentitiesController +from .informations.controller import InformationsController +from .graphs.wot.controller import WotController +from sakia.data.entities import Connection +from PyQt5.QtCore import pyqtSignal, QObject, Qt +from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QDialog, QFileDialog +from PyQt5.QtGui import QCursor +from sakia.decorators import asyncify +from sakia.gui.password_asker import PasswordAskerDialog +from sakia.gui.widgets.dialogs import QAsyncMessageBox +from sakia.gui.widgets import toast + + +class NavigationController(QObject): + """ + The navigation panel + """ + currency_changed = pyqtSignal(str) + connection_changed = pyqtSignal(Connection) + + def __init__(self, parent, view, model): + """ + Constructor of the navigation component + + :param sakia.gui.navigation.view.NavigationView view: the view + :param sakia.gui.navigation.model.NavigationModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + self.components = { + 'TxHistory': TxHistoryController, + 'HomeScreen': HomeScreenController, + 'Network': NetworkController, + 'Identities': IdentitiesController, + 'Informations': InformationsController, + 'Wot': WotController + } + self.view.current_view_changed.connect(self.handle_view_change) + self.view.setContextMenuPolicy(Qt.CustomContextMenu) + self.view.customContextMenuRequested.connect(self.tree_context_menu) + self._components_controllers = [] + + @classmethod + def create(cls, parent, app): + """ + Instanciate a navigation component + :param sakia.app.Application app: the application + :return: a new Navigation controller + :rtype: NavigationController + """ + view = NavigationView(None) + model = NavigationModel(None, app) + navigation = cls(parent, view, model) + model.setParent(navigation) + navigation.init_navigation() + app.new_connection.connect(navigation.add_connection) + return navigation + + def parse_node(self, node_data): + if 'component' in node_data: + component_class = self.components[node_data['component']] + component = component_class.create(self, self.model.app, **node_data['dependencies']) + self._components_controllers.append(component) + widget = self.view.add_widget(component.view) + node_data['widget'] = widget + if 'children' in node_data: + for child in node_data['children']: + self.parse_node(child) + + def init_navigation(self): + self.model.init_navigation_data() + + for node in self.model.navigation: + self.parse_node(node) + + self.view.set_model(self.model) + + def handle_view_change(self, raw_data): + """ + Handle view change + :param dict raw_data: + :return: + """ + user_identity = raw_data.get('user_identity', None) + currency = raw_data.get('currency', None) + if user_identity != self.model.current_data('user_identity'): + self.account_changed.emit(user_identity) + if currency != self.model.current_data('currency'): + self.currency_changed.emit(currency) + self.model.set_current_data(raw_data) + + def add_connection(self, connection): + raw_node = self.model.add_connection(connection) + self.view.add_connection(raw_node) + self.parse_node(raw_node) + + def tree_context_menu(self, point): + mapped = self.view.tree_view.mapFromParent(point) + index = self.view.tree_view.indexAt(mapped) + raw_data = self.view.tree_view.model().data(index, GenericTreeModel.ROLE_RAW_DATA) + if raw_data and raw_data["component"] == "Informations": + menu = QMenu(self.view) + if raw_data['misc']['connection'].uid: + action_gen_revokation = QAction(self.tr("Save revokation document"), menu) + menu.addAction(action_gen_revokation) + action_gen_revokation.triggered.connect(lambda c: + self.action_save_revokation(raw_data['misc']['connection'])) + + action_publish_uid = QAction(self.tr("Publish UID"), menu) + menu.addAction(action_publish_uid) + action_publish_uid.triggered.connect(lambda c: + self.publish_uid(raw_data['misc']['connection'])) + action_publish_uid.setEnabled(self.model.identity_published(raw_data['misc']['connection'])) + + action_leave = QAction(self.tr("Leave the currency"), menu) + menu.addAction(action_leave) + action_leave.triggered.connect(lambda c: self.send_leave(raw_data['misc']['connection'])) + action_leave.setEnabled(self.model.identity_is_member(raw_data['misc']['connection'])) + + copy_pubkey = QAction(menu.tr("Copy pubkey to clipboard"), menu.parent()) + copy_pubkey.triggered.connect(lambda checked, + c=raw_data['misc']['connection']: \ + NavigationModel.copy_pubkey_to_clipboard(c)) + menu.addAction(copy_pubkey) + + action_remove = QAction(self.tr("Remove the connection"), menu) + menu.addAction(action_remove) + action_remove.triggered.connect(lambda c: self.remove_connection(raw_data['misc']['connection'])) + # Show the context menu. + + menu.popup(QCursor.pos()) + + @asyncify + async def publish_uid(self, connection): + password = await self.password_asker.async_exec() + if self.password_asker.result() == QDialog.Rejected: + return + result = await self.account.send_selfcert(password, self.community) + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("UID"), self.tr("Success publishing your UID")) + else: + await QAsyncMessageBox.information(self, self.tr("Membership"), + self.tr("Success publishing your UID")) + else: + if self.app.preferences['notifications']: + toast.display(self.tr("UID"), result[1]) + else: + await QAsyncMessageBox.critical(self, self.tr("UID"), + result[1]) + + @asyncify + async def send_leave(self): + reply = await QAsyncMessageBox.warning(self, self.tr("Warning"), + self.tr("""Are you sure ? +Sending a leaving demand cannot be canceled. +The process to join back the community later will have to be done again.""") + .format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + password = PasswordAskerDialog(self.model.navigation_model.navigation.current_connection()).async_exec() + if not password: + return + result = await self.model.send_leave(password) + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("Revoke"), self.tr("Success sending Revoke demand")) + else: + await QAsyncMessageBox.information(self, self.tr("Revoke"), + self.tr("Success sending Revoke demand")) + else: + if self.app.preferences['notifications']: + toast.display(self.tr("Revoke"), result[1]) + else: + await QAsyncMessageBox.critical(self, self.tr("Revoke"), + result[1]) + + @asyncify + async def remove_connection(self, connection): + reply = await QAsyncMessageBox.question(self.view, self.tr("Removing the connection"), + self.tr("""Are you sure ? This won't remove your money" +neither your identity from the network."""), QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + await self.model.remove_connection(connection) + self.init_navigation() + + def action_save_revokation(self, connection): + password = PasswordAskerDialog(connection).exec() + if not password: + return + + raw_document = self.model.generate_revokation(connection, password) + # Testable way of using a QFileDialog + selected_files = QFileDialog.getSaveFileName(self.view, self.tr("Save a revokation document"), + "", self.tr("All text files (*.txt)")) + if selected_files: + path = selected_files[0] + if not path.endswith('.txt'): + path = "{0}.txt".format(path) + with open(path, 'w') as save_file: + save_file.write(raw_document) + + dialog = QMessageBox(QMessageBox.Information, self.tr("Revokation file"), + self.tr("""<div>Your revokation document has been saved.</div> +<div><b>Please keep it in a safe place.</b></div> +The publication of this document will remove your identity from the network.</p>"""), QMessageBox.Ok) + dialog.setTextFormat(Qt.RichText) + dialog.exec() diff --git a/src/sakia/tests/functional/process_cfg_account/__init__.py b/src/sakia/gui/navigation/graphs/__init__.py similarity index 100% rename from src/sakia/tests/functional/process_cfg_account/__init__.py rename to src/sakia/gui/navigation/graphs/__init__.py diff --git a/src/sakia/tests/functional/process_cfg_community/__init__.py b/src/sakia/gui/navigation/graphs/base/__init__.py similarity index 100% rename from src/sakia/tests/functional/process_cfg_community/__init__.py rename to src/sakia/gui/navigation/graphs/base/__init__.py diff --git a/src/sakia/gui/navigation/graphs/base/controller.py b/src/sakia/gui/navigation/graphs/base/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..e1af19acbd1f9485bd71ac5a975fb3a8d638bfc0 --- /dev/null +++ b/src/sakia/gui/navigation/graphs/base/controller.py @@ -0,0 +1,74 @@ +import asyncio + +from PyQt5.QtCore import pyqtSlot, QObject +from PyQt5.QtGui import QCursor + +from sakia.decorators import asyncify, once_at_a_time +from sakia.gui.widgets.context_menu import ContextMenu + + +class BaseGraphController(QObject): + """ + The homescreen view + """ + + def __init__(self, parent, view, model, password_asker): + """ + Constructor of the homescreen component + + :param sakia.gui.homescreen.view.HomeScreenView: the view + :param sakia.gui.homescreen.model.HomeScreenModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + self.password_asker = password_asker + + def set_scene(self, scene): + """ + Set the scene and connects the signals + :param sakia.gui.views.scenes.base_scene.BaseScene scene: the scene + :return: + """ + # add scene events + scene.node_context_menu_requested.connect(self.node_context_menu) + scene.node_clicked.connect(self.handle_node_click) + + @pyqtSlot(str, dict) + def handle_node_click(self, pubkey, metadata): + asyncio.ensure_future(self.draw_graph(metadata['identity'])) + + async def draw_graph(self, identity): + """ + Draw community graph centered on the identity + + :param sakia.core.registry.Identity identity: Graph node identity + """ + raise NotImplementedError("draw_graph not implemented") + + @once_at_a_time + @asyncify + async def reset(self, checked=False): + """ + Reset graph scene to wallet identity + """ + raise NotImplementedError("reset not implemented") + + @once_at_a_time + @asyncify + def refresh(self): + """ + Refresh graph scene to current metadata + """ + raise NotImplementedError("refresh not implemented") + + def node_context_menu(self, identity): + """ + Open the node context menu + :param sakia.data.entities.Identity identity: the identity of the node to open + """ + menu = ContextMenu.from_data(self.view, self.model.app, self.model.connection, (identity,)) + menu.view_identity_in_wot.connect(self.draw_graph) + + # Show the context menu. + menu.qmenu.popup(QCursor.pos()) diff --git a/src/sakia/gui/views/edges/base_edge.py b/src/sakia/gui/navigation/graphs/base/edge.py similarity index 100% rename from src/sakia/gui/views/edges/base_edge.py rename to src/sakia/gui/navigation/graphs/base/edge.py diff --git a/src/sakia/gui/navigation/graphs/base/model.py b/src/sakia/gui/navigation/graphs/base/model.py new file mode 100644 index 0000000000000000000000000000000000000000..12771e47fd8bdae35bc5b993a4a66d0638f699b8 --- /dev/null +++ b/src/sakia/gui/navigation/graphs/base/model.py @@ -0,0 +1,30 @@ +from PyQt5.QtCore import QObject + +class BaseGraphModel(QObject): + """ + The model of Navigation component + """ + + def __init__(self, parent, app, connection, blockchain_service, identities_service): + """ + Constructor of a model of WoT component + + :param sakia.gui.identities.controller.IdentitiesController parent: the controller + :param sakia.app.Application app: the app + :param sakia.data.entities.Connection connection: the connection + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.blockchain_service = blockchain_service + self.identities_service = identities_service + + def get_identity(self, pubkey): + """ + Get identity from pubkey + :param str pubkey: Identity pubkey + :rtype: sakia.core.registry.Identity + """ + return self.identities_service.get_identity(pubkey, self.connection.currency) diff --git a/src/sakia/gui/views/nodes/base_node.py b/src/sakia/gui/navigation/graphs/base/node.py similarity index 94% rename from src/sakia/gui/views/nodes/base_node.py rename to src/sakia/gui/navigation/graphs/base/node.py index 8c1d3c39a2bfd181664c5cc38692b2d366b5382e..f07802526c77144ced6941477173af888b6590c5 100644 --- a/src/sakia/gui/views/nodes/base_node.py +++ b/src/sakia/gui/navigation/graphs/base/node.py @@ -1,8 +1,9 @@ -from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsSceneHoverEvent, \ - QGraphicsSceneContextMenuEvent from PyQt5.QtCore import Qt from PyQt5.QtGui import QMouseEvent -from ....core.graph.constants import NodeStatus +from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsSceneHoverEvent, \ + QGraphicsSceneContextMenuEvent + +from sakia.data.graphs.constants import NodeStatus class BaseNode(QGraphicsEllipseItem): @@ -65,5 +66,5 @@ class BaseNode(QGraphicsEllipseItem): :param event: scene context menu event """ - self.scene().node_context_menu_requested.emit(self.id) + self.scene().node_context_menu_requested.emit(self.metadata['identity']) diff --git a/src/sakia/gui/views/scenes/base_scene.py b/src/sakia/gui/navigation/graphs/base/scene.py similarity index 78% rename from src/sakia/gui/views/scenes/base_scene.py rename to src/sakia/gui/navigation/graphs/base/scene.py index 36cd72880a4def8abc332cd47f0b5c06e68aaede..3c5a46e58bddacffc84eb12d74c0a3f54527154d 100644 --- a/src/sakia/gui/views/scenes/base_scene.py +++ b/src/sakia/gui/navigation/graphs/base/scene.py @@ -1,10 +1,11 @@ +from sakia.data.entities import Identity from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneContextMenuEvent class BaseScene(QGraphicsScene): # This defines signals taking string arguments - node_context_menu_requested = pyqtSignal(str) + node_context_menu_requested = pyqtSignal(Identity) node_hovered = pyqtSignal(str) node_clicked = pyqtSignal(str, dict) diff --git a/src/sakia/gui/navigation/graphs/base/view.py b/src/sakia/gui/navigation/graphs/base/view.py new file mode 100644 index 0000000000000000000000000000000000000000..4358c5e7c787dd617dc69170e6a8d9196f54ed1a --- /dev/null +++ b/src/sakia/gui/navigation/graphs/base/view.py @@ -0,0 +1,25 @@ +from PyQt5.QtWidgets import QWidget +from PyQt5.QtCore import QEvent + + +class BaseGraphView(QWidget): + """ + Base graph view + """ + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh() + return super().changeEvent(event) \ No newline at end of file diff --git a/src/sakia/tests/functional/transfer/__init__.py b/src/sakia/gui/navigation/graphs/wot/__init__.py similarity index 100% rename from src/sakia/tests/functional/transfer/__init__.py rename to src/sakia/gui/navigation/graphs/wot/__init__.py diff --git a/src/sakia/gui/navigation/graphs/wot/controller.py b/src/sakia/gui/navigation/graphs/wot/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..b7e7b11f4ca6df713895152888865a3c6618c4ba --- /dev/null +++ b/src/sakia/gui/navigation/graphs/wot/controller.py @@ -0,0 +1,70 @@ +import asyncio + +from sakia.decorators import asyncify, once_at_a_time +from sakia.gui.sub.search_user.controller import SearchUserController +from .model import WotModel +from .view import WotView +from ..base.controller import BaseGraphController + + +class WotController(BaseGraphController): + """ + The homescreen view + """ + + def __init__(self, parent, view, model, password_asker=None): + """ + Constructor of the homescreen component + + :param sakia.gui.homescreen.view.HomeScreenView: the view + :param sakia.gui.homescreen.model.HomeScreenModel model: the model + """ + super().__init__(parent, view, model, password_asker) + self.set_scene(view.scene()) + self.reset() + + @classmethod + def create(cls, parent, app, connection, blockchain_service, identities_service): + view = WotView(parent.view) + model = WotModel(None, app, connection, blockchain_service, identities_service) + wot = cls(parent, view, model) + model.setParent(wot) + search_user = SearchUserController.create(wot, app, currency=connection.currency) + wot.view.set_search_user(search_user.view) + search_user.identity_selected.connect(wot.center_on_identity) + search_user.view.button_reset.clicked.connect(wot.reset) + return wot + + def center_on_identity(self, identity): + """ + Draw community graph centered on the identity + + :param sakia.core.registry.Identity identity: Center identity + """ + self.draw_graph(identity) + + @once_at_a_time + @asyncify + async def draw_graph(self, identity): + """ + Draw community graph centered on the identity + + :param sakia.core.registry.Identity identity: Center identity + """ + self.view.busy.show() + await self.model.set_identity(identity) + self.refresh() + self.view.busy.hide() + + def refresh(self): + """ + Refresh graph scene to current metadata + """ + nx_graph = self.model.get_nx_graph() + self.view.display_wot(nx_graph, self.model.identity) + + def reset(self, checked=False): + """ + Reset graph scene to wallet identity + """ + self.draw_graph(None) diff --git a/src/sakia/gui/views/edges/wot_edge.py b/src/sakia/gui/navigation/graphs/wot/edge.py similarity index 97% rename from src/sakia/gui/views/edges/wot_edge.py rename to src/sakia/gui/navigation/graphs/wot/edge.py index 50fcf150c949e89e3f2d468606fdc16c63eb9da7..29053a06f7522e2b0a107f02d11ef3d64f4c384c 100644 --- a/src/sakia/gui/views/edges/wot_edge.py +++ b/src/sakia/gui/navigation/graphs/wot/edge.py @@ -1,9 +1,11 @@ +import math + from PyQt5.QtCore import Qt, QRectF, QLineF, QPointF, QSizeF, \ qFuzzyCompare from PyQt5.QtGui import QColor, QPen, QPolygonF -import math -from .base_edge import BaseEdge -from ....core.graph.constants import EdgeStatus + +from sakia.data.graphs.constants import EdgeStatus +from ..base.edge import BaseEdge class WotEdge(BaseEdge): diff --git a/src/sakia/gui/views/wot.py b/src/sakia/gui/navigation/graphs/wot/graphics_view.py similarity index 95% rename from src/sakia/gui/views/wot.py rename to src/sakia/gui/navigation/graphs/wot/graphics_view.py index 056962d84469f4000daec002861fac40f9066ad9..95aaae1bde9224aad4539dc1b6d311de9bc1a0c7 100644 --- a/src/sakia/gui/views/wot.py +++ b/src/sakia/gui/navigation/graphs/wot/graphics_view.py @@ -1,10 +1,10 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QPainter, QWheelEvent from PyQt5.QtWidgets import QGraphicsView -from .scenes import WotScene +from .scene import WotScene -class WotView(QGraphicsView): +class WotGraphicsView(QGraphicsView): def __init__(self, parent=None): """ Create View to display scene diff --git a/src/sakia/gui/navigation/graphs/wot/model.py b/src/sakia/gui/navigation/graphs/wot/model.py new file mode 100644 index 0000000000000000000000000000000000000000..80ea9038d469e204c0e03bf8465c14c440a041b6 --- /dev/null +++ b/src/sakia/gui/navigation/graphs/wot/model.py @@ -0,0 +1,50 @@ +from sakia.data.graphs import WoTGraph +from ..base.model import BaseGraphModel + + +class WotModel(BaseGraphModel): + """ + The model of Navigation component + """ + + def __init__(self, parent, app, connection, blockchain_service, identities_service): + """ + Constructor of a model of WoT component + + :param sakia.gui.identities.controller.IdentitiesController parent: the controller + :param sakia.app.Application app: the app + :param sakia.data.entities.Connection connection: the connection + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + """ + super().__init__(parent, app, connection, blockchain_service, identities_service) + self.app = app + self.connection = connection + self.blockchain_service = blockchain_service + self.identities_service = identities_service + self.wot_graph = WoTGraph(self.app, self.blockchain_service, self.identities_service) + self.identity = None + + async def set_identity(self, identity=None): + """ + Change current identity + If identity is None, it defaults to account identity + :param sakia.core.registry.Identity identity: the new identity to show + :return: + """ + connection_identity = self.identities_service.get_identity(self.connection.pubkey) + # create empty graph instance + if identity: + self.identity = identity + await self.wot_graph.initialize(self.identity, connection_identity) + else: + self.identity = connection_identity + # create empty graph instance + self.wot_graph.offline_init(connection_identity, connection_identity) + + def get_nx_graph(self): + """ + Get nx graph of current identity wot graph + :rtype: sakia.core.registry.Identity + """ + return self.wot_graph.nx_graph diff --git a/src/sakia/gui/views/nodes/wot_node.py b/src/sakia/gui/navigation/graphs/wot/node.py similarity index 96% rename from src/sakia/gui/views/nodes/wot_node.py rename to src/sakia/gui/navigation/graphs/wot/node.py index ef7ba7dc1e0c43b631b05c28d032dbf0228cd646..0d99b01eb9540b26cd70864637eb34a975bd7010 100644 --- a/src/sakia/gui/views/nodes/wot_node.py +++ b/src/sakia/gui/navigation/graphs/wot/node.py @@ -1,8 +1,9 @@ -from PyQt5.QtWidgets import QGraphicsSimpleTextItem from PyQt5.QtCore import Qt, QPointF from PyQt5.QtGui import QTransform, QColor, QPen, QBrush, QRadialGradient -from ....core.graph.constants import NodeStatus -from .base_node import BaseNode +from PyQt5.QtWidgets import QGraphicsSimpleTextItem + +from sakia.data.graphs.constants import NodeStatus +from ..base.node import BaseNode class WotNode(BaseNode): diff --git a/src/sakia/gui/views/scenes/wot_scene.py b/src/sakia/gui/navigation/graphs/wot/scene.py similarity index 98% rename from src/sakia/gui/views/scenes/wot_scene.py rename to src/sakia/gui/navigation/graphs/wot/scene.py index 743a044b537cdee0acda72a0a1ffe3394aa5ead8..d8c67d4597faa3254d86a5b3b810104131627e28 100644 --- a/src/sakia/gui/views/scenes/wot_scene.py +++ b/src/sakia/gui/navigation/graphs/wot/scene.py @@ -2,10 +2,10 @@ import networkx from PyQt5.QtCore import QPoint, pyqtSignal from PyQt5.QtWidgets import QGraphicsScene -from ..edges import WotEdge -from ..nodes import WotNode +from .edge import WotEdge +from .node import WotNode -from .base_scene import BaseScene +from ..base.scene import BaseScene class WotScene(BaseScene): diff --git a/src/sakia/gui/navigation/graphs/wot/view.py b/src/sakia/gui/navigation/graphs/wot/view.py new file mode 100644 index 0000000000000000000000000000000000000000..14c9fa19efcc51e6c0831b6c53cd5309196c41f9 --- /dev/null +++ b/src/sakia/gui/navigation/graphs/wot/view.py @@ -0,0 +1,48 @@ +from ..base.view import BaseGraphView +from .wot_tab_uic import Ui_WotWidget +from sakia.gui.widgets.busy import Busy +from PyQt5.QtCore import QEvent + + +class WotView(BaseGraphView, Ui_WotWidget): + """ + Wot graph view + """ + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + self.setupUi(self) + self.busy = Busy(self.graphics_view) + self.busy.hide() + + def set_search_user(self, search_user): + """ + Set the search user view in the gui + :param sakia.gui.search_user.view.SearchUserView search_user: the view + :return: + """ + self.layout().insertWidget(0, search_user) + + def scene(self): + """ + Get the scene of the underlying graphics view + :return: + """ + return self.graphics_view.scene() + + def display_wot(self, nx_graph, identity): + """ + Display given wot around given identity + :param nx_graph: + :param identity: + :return: + """ + # draw graph in qt scene + self.graphics_view.scene().update_wot(nx_graph, identity) + + def resizeEvent(self, event): + self.busy.resize(event.size()) + super().resizeEvent(event) diff --git a/res/ui/wot_tab.ui b/src/sakia/gui/navigation/graphs/wot/wot_tab.ui similarity index 52% rename from res/ui/wot_tab.ui rename to src/sakia/gui/navigation/graphs/wot/wot_tab.ui index d71654c11480961021b3679b84a89243a1b1e3b2..7bddf7cbfa3102c58d9b38710d1c30d85e78e956 100644 --- a/res/ui/wot_tab.ui +++ b/src/sakia/gui/navigation/graphs/wot/wot_tab.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>WotTabWidget</class> - <widget class="QWidget" name="WotTabWidget"> + <class>WotWidget</class> + <widget class="QWidget" name="WotWidget"> <property name="geometry"> <rect> <x>0</x> @@ -13,34 +13,25 @@ <property name="windowTitle"> <string>Form</string> </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="1" column="0" colspan="2"> - <widget class="WotView" name="graphicsView"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="WotGraphicsView" name="graphics_view"> <property name="viewportUpdateMode"> <enum>QGraphicsView::BoundingRectViewportUpdate</enum> </property> </widget> </item> - <item row="0" column="0" colspan="2"> - <widget class="SearchUserWidget" name="search_user_widget" native="true"/> - </item> </layout> </widget> <customwidgets> <customwidget> - <class>WotView</class> + <class>WotGraphicsView</class> <extends>QGraphicsView</extends> - <header>sakia.gui.views.wot</header> - </customwidget> - <customwidget> - <class>SearchUserWidget</class> - <extends>QWidget</extends> - <header>sakia.gui.widgets.search_user</header> - <container>1</container> + <header>sakia.gui.navigation.graphs.wot.graphics_view</header> </customwidget> </customwidgets> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../../../res/icons/icons.qrc"/> </resources> <connections/> <slots> diff --git a/src/sakia/tests/mocks/__init__.py b/src/sakia/gui/navigation/homescreen/__init__.py similarity index 100% rename from src/sakia/tests/mocks/__init__.py rename to src/sakia/gui/navigation/homescreen/__init__.py diff --git a/src/sakia/gui/navigation/homescreen/controller.py b/src/sakia/gui/navigation/homescreen/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..d499a8cbefb74b9ce49b1e189e542229da0954a6 --- /dev/null +++ b/src/sakia/gui/navigation/homescreen/controller.py @@ -0,0 +1,35 @@ +from PyQt5.QtCore import QObject +from .view import HomeScreenView +from .model import HomeScreenModel + + +class HomeScreenController(QObject): + """ + The homescreen view + """ + + def __init__(self, parent, view, model): + """ + Constructor of the homescreen component + + :param sakia.gui.homescreen.view.HomeScreenView: the view + :param sakia.gui.homescreen.model.HomeScreenModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + + @classmethod + def create(cls, parent, app): + """ + Instanciate a homescreen component + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.core.Application app: + :return: a new Homescreen controller + :rtype: HomeScreenController + """ + view = HomeScreenView(parent.view) + model = HomeScreenModel(None, app) + homescreen = cls(parent, view, model) + model.setParent(homescreen) + return homescreen diff --git a/src/sakia/gui/navigation/homescreen/homescreen.ui b/src/sakia/gui/navigation/homescreen/homescreen.ui new file mode 100644 index 0000000000000000000000000000000000000000..97f5d8bfb627f44bd79556a0ea732b3ceb179b92 --- /dev/null +++ b/src/sakia/gui/navigation/homescreen/homescreen.ui @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>HomescreenWidget</class> + <widget class="QWidget" name="HomescreenWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>648</width> + <height>472</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"/> + </widget> + <resources> + <include location="../../../../res/icons/icons.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/sakia/gui/navigation/homescreen/model.py b/src/sakia/gui/navigation/homescreen/model.py new file mode 100644 index 0000000000000000000000000000000000000000..a9dbae61066814536fbac80e12b5fcf2e15837cb --- /dev/null +++ b/src/sakia/gui/navigation/homescreen/model.py @@ -0,0 +1,15 @@ +from PyQt5.QtCore import QObject + + +class HomeScreenModel(QObject): + """ + The model of HomeScreen component + """ + + def __init__(self, parent, app): + super().__init__(parent) + self.app = app + + @property + def account(self): + return self.app.current_account \ No newline at end of file diff --git a/src/sakia/gui/navigation/homescreen/view.py b/src/sakia/gui/navigation/homescreen/view.py new file mode 100644 index 0000000000000000000000000000000000000000..828780f12b75a11de3d7fee306e38d774fda5949 --- /dev/null +++ b/src/sakia/gui/navigation/homescreen/view.py @@ -0,0 +1,15 @@ +from PyQt5.QtWidgets import QWidget +from .homescreen_uic import Ui_HomescreenWidget + + +class HomeScreenView(QWidget, Ui_HomescreenWidget): + """ + Home screen view + """ + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + self.setupUi(self) diff --git a/src/sakia/tests/unit/__init__.py b/src/sakia/gui/navigation/identities/__init__.py similarity index 100% rename from src/sakia/tests/unit/__init__.py rename to src/sakia/gui/navigation/identities/__init__.py diff --git a/src/sakia/gui/navigation/identities/controller.py b/src/sakia/gui/navigation/identities/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..48a7d107ecff49f86b22cfa97b731aee68d64c04 --- /dev/null +++ b/src/sakia/gui/navigation/identities/controller.py @@ -0,0 +1,79 @@ +import logging + +from PyQt5.QtGui import QCursor +from PyQt5.QtCore import QObject, pyqtSignal +from sakia.errors import NoPeerAvailable + +from duniterpy.api import errors +from sakia.decorators import once_at_a_time, asyncify +from sakia.gui.widgets.context_menu import ContextMenu +from .model import IdentitiesModel +from .view import IdentitiesView + + +class IdentitiesController(QObject): + """ + The navigation panel + """ + view_in_wot = pyqtSignal(object) + + def __init__(self, parent, view, model, password_asker=None): + """ + Constructor of the navigation component + + :param sakia.gui.identities.view.IdentitiesView view: the view + :param sakia.gui.identities.model.IdentitiesModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + self.password_asker = password_asker + self.view.search_by_text_requested.connect(self.search_text) + self.view.search_directly_connected_requested.connect(self.search_direct_connections) + self.view.table_identities.customContextMenuRequested.connect(self.identity_context_menu) + table_model = self.model.init_table_model() + self.view.set_table_identities_model(table_model) + + @classmethod + def create(cls, parent, app, connection, blockchain_service, identities_service): + view = IdentitiesView(parent.view) + model = IdentitiesModel(None, app, connection, blockchain_service, identities_service) + identities = cls(parent, view, model) + model.setParent(identities) + return identities + + def identity_context_menu(self, point): + index = self.view.table_identities.indexAt(point) + valid, identity = self.model.table_data(index) + if valid: + menu = ContextMenu.from_data(self.view, self.model.app, self.model.connection, (identity,)) + menu.view_identity_in_wot.connect(self.view_in_wot) + menu.identity_information_loaded.connect(self.model.table_model.sourceModel().identity_loaded) + + # Show the context menu. + menu.qmenu.popup(QCursor.pos()) + + @once_at_a_time + @asyncify + async def search_text(self, text): + """ + Search identities using given text + :param str text: text to search + :return: + """ + try: + identities = await self.model.lookup_identities(text) + self.model.refresh_identities(identities) + except errors.DuniterError as e: + if e.ucode == errors.BLOCK_NOT_FOUND: + logging.debug(str(e)) + except NoPeerAvailable as e: + logging.debug(str(e)) + + def search_direct_connections(self): + """ + Search identities directly connected to account + :return: + """ + identities = self.model.linked_identities() + self.model.refresh_identities(identities) diff --git a/res/ui/identities_tab.ui b/src/sakia/gui/navigation/identities/identities.ui similarity index 96% rename from res/ui/identities_tab.ui rename to src/sakia/gui/navigation/identities/identities.ui index 0dab4a2d41ed7e07fe2da897b2dc8bad20aa7496..7d21354e111e0506633e54f3c3577e9b0d840007 100644 --- a/res/ui/identities_tab.ui +++ b/src/sakia/gui/navigation/identities/identities.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>IdentitiesTab</class> - <widget class="QWidget" name="IdentitiesTab"> + <class>IdentitiesWidget</class> + <widget class="QWidget" name="IdentitiesWidget"> <property name="geometry"> <rect> <x>0</x> diff --git a/src/sakia/gui/navigation/identities/model.py b/src/sakia/gui/navigation/identities/model.py new file mode 100644 index 0000000000000000000000000000000000000000..e91df361dcbf30100f396403fee2819427ebd3ae --- /dev/null +++ b/src/sakia/gui/navigation/identities/model.py @@ -0,0 +1,80 @@ +import asyncio +from PyQt5.QtCore import Qt, QObject +from .table_model import IdentitiesFilterProxyModel, IdentitiesTableModel + + +class IdentitiesModel(QObject): + """ + The model of the identities component + """ + + def __init__(self, parent, app, connection, blockchain_service, identities_service): + """ + Constructor of a model of Identities component + + :param sakia.gui.identities.controller.IdentitiesController parent: the controller + :param sakia.app.Application app: the app + :param sakia.data.entities.Connection connection: the connection + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.blockchain_service = blockchain_service + self.identities_service = identities_service + + self.table_model = None + + def init_table_model(self): + """ + Instanciate the table model of the view + """ + identities_model = IdentitiesTableModel(self, self.blockchain_service, self.identities_service) + proxy = IdentitiesFilterProxyModel() + proxy.setSourceModel(identities_model) + self.table_model = proxy + return self.table_model + + def table_data(self, index): + """ + Get table data at given point + :param PyQt5.QtCore.QModelIndex index: + :return: a tuple containing information of the table + """ + if index.isValid() and index.row() < self.table_model.rowCount(): + source_index = self.table_model.mapToSource(index) + identity_col = self.table_model.sourceModel().columns_ids.index('identity') + identity_index = self.table_model.sourceModel().index(source_index.row(), identity_col) + identity = self.table_model.sourceModel().data(identity_index, Qt.DisplayRole) + return True, identity + return False, None + + async def lookup_identities(self, text): + """ + Lookup for identities + :param str text: text contained in the identities to lookup + """ + return await self.identities_service.lookup(text) + + def refresh_identities(self, identities): + """ + Refresh the table with specified identities. + If no identities is passed, use the account connections. + """ + self.table_model.sourceModel().refresh_identities(identities) + + def linked_identities(self): + + # create Identity from node metadata + connection_identity = self.identities_service.get_identity(self.connection.pubkey) + linked = [] + certifier_list = self.identities_service.certifications_received(connection_identity.pubkey) + for certification in tuple(certifier_list): + linked.append(self.identities_service.get_identity(certification.certifier)) + + certified_list = self.identities_service.certifications_sent(connection_identity.pubkey) + for certification in tuple(certified_list): + linked.append(self.identities_service.get_identity(certification.certified)) + + return linked diff --git a/src/sakia/models/identities.py b/src/sakia/gui/navigation/identities/table_model.py similarity index 55% rename from src/sakia/models/identities.py rename to src/sakia/gui/navigation/identities/table_model.py index b6fea90292dbf9c5b1df10e4f31576a538f0e830..0b6651f814132bac1668f12e9a01a7b838702d47 100644 --- a/src/sakia/models/identities.py +++ b/src/sakia/gui/navigation/identities/table_model.py @@ -1,13 +1,7 @@ -""" -Created on 5 févr. 2014 - -@author: inso -""" - -from ..tools.exceptions import NoPeerAvailable, MembershipNotFoundError +from sakia.errors import NoPeerAvailable from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, \ - QDateTime, QModelIndex, QLocale, QEvent -from PyQt5.QtGui import QColor, QIcon + QDateTime, QModelIndex, QLocale, QT_TRANSLATE_NOOP +from PyQt5.QtGui import QColor, QIcon, QFont import logging import asyncio @@ -15,16 +9,10 @@ import asyncio class IdentitiesFilterProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) - self.community = None - - def setSourceModel(self, sourceModel): - self.community = sourceModel.community - super().setSourceModel(sourceModel) - - def change_community(self, community): - self.community = community - self.sourceModel().change_community(community) + def columnCount(self, parent): + return len(IdentitiesTableModel.columns_ids) - 1 + def lessThan(self, left, right): """ Sort table by given column number. @@ -40,11 +28,12 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): source_index = self.mapToSource(index) if source_index.isValid(): source_data = self.sourceModel().data(source_index, role) - expiration_col = self.sourceModel().columns_ids.index('expiration') + expiration_col = IdentitiesTableModel.columns_ids.index('expiration') expiration_index = self.sourceModel().index(source_index.row(), expiration_col) STATUS_NOT_MEMBER = 0 STATUS_MEMBER = 1 + STATUS_UNKNOWN = 2 STATUS_EXPIRE_SOON = 3 status = STATUS_NOT_MEMBER expiration_data = self.sourceModel().data(expiration_index, Qt.DisplayRole) @@ -53,7 +42,9 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): warning_expiration_time = int(sig_validity / 3) #logging.debug("{0} > {1}".format(current_time, expiration_data)) - if expiration_data is not None: + if expiration_data == 0: + status = STATUS_UNKNOWN + elif expiration_data is not None: status = STATUS_MEMBER if current_time > (expiration_data*1000): status = STATUS_NOT_MEMBER @@ -61,9 +52,9 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): status = STATUS_EXPIRE_SOON if role == Qt.DisplayRole: - if source_index.column() in (self.sourceModel().columns_ids.index('renewed'), - self.sourceModel().columns_ids.index('expiration')): - if source_data is not None: + if source_index.column() in (IdentitiesTableModel.columns_ids.index('renewed'), + IdentitiesTableModel.columns_ids.index('expiration')): + if source_data: return QLocale.toString( QLocale(), QDateTime.fromTime_t(source_data).date(), @@ -71,8 +62,8 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): ) else: return "" - if source_index.column() == self.sourceModel().columns_ids.index('publication'): - if source_data is not None: + if source_index.column() == IdentitiesTableModel.columns_ids.index('publication'): + if source_data: return QLocale.toString( QLocale(), QDateTime.fromTime_t(source_data), @@ -80,17 +71,27 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): ) else: return "" - if source_index.column() == self.sourceModel().columns_ids.index('pubkey'): - return "pub:{0}".format(source_data[:5]) + if source_index.column() == IdentitiesTableModel.columns_ids.index('pubkey'): + return source_data + if source_index.column() == IdentitiesTableModel.columns_ids.index('block'): + return str(source_data)[:20] if role == Qt.ForegroundRole: if status == STATUS_EXPIRE_SOON: return QColor("darkorange").darker(120) elif status == STATUS_NOT_MEMBER: return QColor(Qt.red) + elif status == STATUS_UNKNOWN: + return QColor(Qt.black) else: return QColor(Qt.blue) - if role == Qt.DecorationRole and source_index.column() == self.sourceModel().columns_ids.index('uid'): + + if role == Qt.FontRole and status == STATUS_UNKNOWN: + font = QFont() + font.setItalic(True) + return font + + if role == Qt.DecorationRole and source_index.column() == IdentitiesTableModel.columns_ids.index('uid'): if status == STATUS_NOT_MEMBER: return QIcon(":/icons/not_member") elif status == STATUS_MEMBER: @@ -107,116 +108,97 @@ class IdentitiesTableModel(QAbstractTableModel): A Qt abstract item model to display communities in a tree """ - def __init__(self, parent=None): + columns_titles = {'uid': lambda: QT_TRANSLATE_NOOP("IdentitiesTableModel", 'UID'), + 'pubkey': lambda: QT_TRANSLATE_NOOP("IdentitiesTableModel", 'Pubkey'), + 'renewed': lambda: QT_TRANSLATE_NOOP("IdentitiesTableModel", 'Renewed'), + 'expiration': lambda: QT_TRANSLATE_NOOP("IdentitiesTableModel", 'Expiration'), + 'publication': lambda: QT_TRANSLATE_NOOP("IdentitiesTableModel", 'Publication Date'), + 'block': lambda: QT_TRANSLATE_NOOP("IdentitiesTableModel", 'Publication Block'), } + columns_ids = ('uid', 'pubkey', 'renewed', 'expiration', 'publication', 'block', 'identity') + + def __init__(self, parent, blockchain_service, identities_service): """ Constructor + :param parent: + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service """ super().__init__(parent) - self.community = None - self.columns_titles = {'uid': lambda: self.tr('UID'), - 'pubkey': lambda: self.tr('Pubkey'), - 'renewed': lambda: self.tr('Renewed'), - 'expiration': lambda: self.tr('Expiration'), - 'publication': lambda: self.tr('Publication Date'), - 'block': lambda: self.tr('Publication Block'),} - self.columns_ids = ('uid', 'pubkey', 'renewed', 'expiration', 'publication', 'block') + self.blockchain_service = blockchain_service + self.identities_service = identities_service self.identities_data = [] self._sig_validity = 0 - def change_community(self, community): - self.community = community - def sig_validity(self): return self._sig_validity - + @property def pubkeys(self): """ - Ge - def resizeEvent(self, event): - self.busy.resize(event.size()) - super().resizeEvent(event)t pubkeys of displayed identities + Get pubkeys of displayed identities """ return [i[1] for i in self.identities_data] - async def identity_data(self, identity): + def identity_data(self, identity): """ Return the identity in the form a tuple to display - :param sakia.core.registry.Identity identity: The identity to get data from + :param sakia.data.entities.Identity identity: The identity to get data from :return: The identity data in the form of a tuple :rtype: tuple """ - try: - join_date = await identity.get_join_date(self.community) - expiration_date = await identity.get_expiration_date(self.community) - except MembershipNotFoundError: - join_date = None - expiration_date = None - - if identity.sigdate: - sigdate_ts = await self.community.time(identity.sigdate.number) - sigdate_block = identity.sigdate.sha_hash[:7] - else: - sigdate_ts = None - sigdate_block = None - - return identity.uid, identity.pubkey, join_date, expiration_date, sigdate_ts, sigdate_block - - async def refresh_identities(self, identities): + join_date = identity.membership_timestamp + expiration_date = self.identities_service.expiration_date(identity) + sigdate_ts = identity.timestamp + sigdate_block = identity.blockstamp + + return identity.uid, identity.pubkey, join_date, expiration_date, sigdate_ts, sigdate_block, identity + + def refresh_identities(self, identities): """ Change the identities to display - :param sakia.core.registry.IdentitiesRegistry identities: The new identities to display + :param list[sakia.data.entities.Identity] identities: The new identities to display """ logging.debug("Refresh {0} identities".format(len(identities))) self.beginResetModel() - self.identities_data = [] - self.endResetModel() - self.beginResetModel() identities_data = [] - requests_coro = [] for identity in identities: - coro = asyncio.ensure_future(self.identity_data(identity)) - requests_coro.append(coro) - - identities_data = await asyncio.gather(*requests_coro) + identities_data.append(self.identity_data(identity)) if len(identities) > 0: try: - parameters = await self.community.parameters() - self._sig_validity = parameters['sigValidity'] + parameters = self.blockchain_service.parameters() + self._sig_validity = parameters.sig_validity except NoPeerAvailable as e: logging.debug(str(e)) self._sig_validity = 0 self.identities_data = identities_data self.endResetModel() + def identity_loaded(self, identity): + for i, idty in enumerate(self.identities_data): + if idty[IdentitiesTableModel.columns_ids.index('identity')] == identity: + self.identities_data[i] = self.identity_data(identity) + self.dataChanged.emit(self.index(i, 0), self.index(i, len(IdentitiesTableModel.columns_ids))) + return + def rowCount(self, parent): return len(self.identities_data) def columnCount(self, parent): - return len(self.columns_ids) + return len(IdentitiesTableModel.columns_ids) def headerData(self, section, orientation, role): if role == Qt.DisplayRole: - col_id = self.columns_ids[section] - return self.columns_titles[col_id]() + col_id = IdentitiesTableModel.columns_ids[section] + return IdentitiesTableModel.columns_titles[col_id]() def data(self, index, role): - if role == Qt.DisplayRole: + if index.isValid() and role == Qt.DisplayRole: row = index.row() col = index.column() identity_data = self.identities_data[row] return identity_data[col] - def identity_index(self, pubkey): - try: - row = self.pubkeys.index(pubkey) - index_start = self.index(row, 0) - index_end = self.index(row, len(self.columns_ids)) - return (index_start, index_end) - except ValueError: - return (QModelIndex(), QModelIndex()) - def flags(self, index): return Qt.ItemIsSelectable | Qt.ItemIsEnabled diff --git a/src/sakia/gui/navigation/identities/view.py b/src/sakia/gui/navigation/identities/view.py new file mode 100644 index 0000000000000000000000000000000000000000..c5011b16d8ee0a5c03d61773d436038eb353d8a8 --- /dev/null +++ b/src/sakia/gui/navigation/identities/view.py @@ -0,0 +1,73 @@ +from PyQt5.QtCore import pyqtSignal, QT_TRANSLATE_NOOP, Qt, QEvent +from PyQt5.QtWidgets import QWidget, QAbstractItemView, QAction +from .identities_uic import Ui_IdentitiesWidget + + +class IdentitiesView(QWidget, Ui_IdentitiesWidget): + """ + View of the Identities component + """ + view_in_wot = pyqtSignal(object) + money_sent = pyqtSignal() + search_by_text_requested = pyqtSignal(str) + search_directly_connected_requested = pyqtSignal() + + _direct_connections_text = QT_TRANSLATE_NOOP("IdentitiesView", "Search direct certifications") + _search_placeholder = QT_TRANSLATE_NOOP("IdentitiesView", "Research a pubkey, an uid...") + + def __init__(self, parent): + super().__init__(parent) + + self.direct_connections = QAction(self.tr(IdentitiesView._direct_connections_text), self) + self.direct_connections.triggered.connect(self.request_search_direct_connections) + self.setupUi(self) + + self.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_identities.sortByColumn(0, Qt.AscendingOrder) + self.table_identities.resizeColumnsToContents() + self.edit_textsearch.setPlaceholderText(self.tr(IdentitiesView._search_placeholder)) + self.button_search.addAction(self.direct_connections) + self.button_search.clicked.connect(self.request_search_by_text) + + def set_table_identities_model(self, model): + """ + Set the model of the table view + :param PyQt5.QtCore.QAbstractItemModel model: the model of the table view + """ + self.table_identities.setModel(model) + model.modelAboutToBeReset.connect(lambda: self.table_identities.setEnabled(False)) + model.modelReset.connect(lambda: self.table_identities.setEnabled(True)) + + def request_search_by_text(self): + text = self.edit_textsearch.text() + if len(text) < 2: + return + self.edit_textsearch.clear() + self.edit_textsearch.setPlaceholderText(text) + self.search_by_text_requested.emit(text) + + def request_search_direct_connections(self): + """ + Search members of community and display found members + """ + self.edit_textsearch.setPlaceholderText(self.tr(IdentitiesView._search_placeholder)) + self.search_directly_connected_requested.emit() + + def retranslateUi(self, widget): + self.direct_connections.setText(self.tr(IdentitiesView._direct_connections_text)) + super().retranslateUi(self) + + def resizeEvent(self, event): + self.busy.resize(event.size()) + super().resizeEvent(event) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + return super().changeEvent(event) + diff --git a/src/sakia/tests/unit/core/graph/__init__.py b/src/sakia/gui/navigation/informations/__init__.py similarity index 100% rename from src/sakia/tests/unit/core/graph/__init__.py rename to src/sakia/gui/navigation/informations/__init__.py diff --git a/src/sakia/gui/navigation/informations/controller.py b/src/sakia/gui/navigation/informations/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..074699c162bba386ad12f271e4de6a62c618fda5 --- /dev/null +++ b/src/sakia/gui/navigation/informations/controller.py @@ -0,0 +1,98 @@ +import logging + +from PyQt5.QtCore import QObject +from sakia.errors import NoPeerAvailable + +from duniterpy.api import errors +from sakia.decorators import asyncify +from .model import InformationsModel +from .view import InformationsView + + +class InformationsController(QObject): + """ + The informations component + """ + + def __init__(self, parent, view, model): + """ + Constructor of the informations component + + :param sakia.gui.informations.view.InformationsView view: the view + :param sakia.gui.informations.model.InformationsModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + + @property + def informations_view(self): + """ + :rtype: sakia.gui.informations.view.InformationsView + """ + return self.view + + @classmethod + def create(cls, parent, app, connection, blockchain_service, identities_service, sources_service): + """ + + :param parent: + :param sakia.app.Application app: + :param connection: + :param blockchain_service: + :param identities_service: + :param sources_service: + :return: + """ + view = InformationsView(parent.view) + model = InformationsModel(None, app, connection, blockchain_service, identities_service, sources_service) + informations = cls(parent, view, model) + model.setParent(informations) + informations.init_view_text() + app.identity_changed.connect(informations.handle_identity_change) + app.new_transfer.connect(informations.refresh_localized_data) + app.new_dividend.connect(informations.refresh_localized_data) + app.referential_changed.connect(informations.refresh_localized_data) + app.sources_refreshed.connect(informations.refresh_localized_data) + return informations + + @asyncify + async def init_view_text(self): + """ + Initialization of text in informations view + """ + referentials = self.model.referentials() + self.view.set_rules_text_no_dividend() + self.view.set_general_text_no_dividend() + self.view.set_text_referentials(referentials) + params = self.model.parameters() + if params: + self.view.set_money_text(params, self.model.short_currency()) + self.view.set_wot_text(params) + self.refresh_localized_data() + + def handle_identity_change(self, identity): + if identity.pubkey == self.model.connection.pubkey and identity.uid == self.model.connection.uid: + self.refresh_localized_data() + + def refresh_localized_data(self): + """ + Refresh localized data in view + """ + localized_data = self.model.get_localized_data() + try: + simple_data = self.model.get_identity_data() + all_data = {**simple_data, **localized_data} + self.view.set_simple_informations(all_data, InformationsView.CommunityState.READY) + except NoPeerAvailable as e: + logging.debug(str(e)) + self.view.set_simple_informations(all_data, InformationsView.CommunityState.OFFLINE) + except errors.DuniterError as e: + if e.ucode == errors.BLOCK_NOT_FOUND: + self.view.set_simple_informations(all_data, InformationsView.CommunityState.NOT_INIT) + else: + raise + + self.view.set_general_text(localized_data) + self.view.set_rules_text(localized_data) + diff --git a/res/ui/informations_tab.ui b/src/sakia/gui/navigation/informations/informations.ui similarity index 69% rename from res/ui/informations_tab.ui rename to src/sakia/gui/navigation/informations/informations.ui index 46ed1d421ce25daa318873f2b5b548f42e23a40b..920da0a0356356bd02d576bc429b1841c01ac67e 100644 --- a/res/ui/informations_tab.ui +++ b/src/sakia/gui/navigation/informations/informations.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>InformationsTabWidget</class> - <widget class="QWidget" name="InformationsTabWidget"> + <class>InformationsWidget</class> + <widget class="QWidget" name="InformationsWidget"> <property name="geometry"> <rect> <x>0</x> @@ -13,11 +13,8 @@ <property name="windowTitle"> <string>Form</string> </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QScrollArea" name="scrollArea"> - <property name="styleSheet"> - <string notr="true">QGroupBox { + <property name="styleSheet"> + <string notr="true">QGroupBox { border: 1px solid gray; border-radius: 9px; margin-top: 0.5em; @@ -29,6 +26,12 @@ QGroupBox::title { padding: 0 3px 0 3px; font-weight: bold; }</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="0"> + <widget class="QScrollArea" name="scrollarea"> + <property name="styleSheet"> + <string notr="true"/> </property> <property name="widgetResizable"> <bool>true</bool> @@ -38,8 +41,8 @@ QGroupBox::title { <rect> <x>0</x> <y>0</y> - <width>518</width> - <height>717</height> + <width>522</width> + <height>308</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_5"> @@ -148,10 +151,69 @@ QGroupBox::title { </widget> </widget> </item> + <item row="1" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_details"> + <property name="text"> + <string>Details</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QGroupBox" name="group_uid_state"> + <property name="title"> + <string>UID</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_simple"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="2" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> </layout> </widget> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../res/icons/icons.qrc"/> </resources> <connections/> </ui> diff --git a/src/sakia/gui/navigation/informations/model.py b/src/sakia/gui/navigation/informations/model.py new file mode 100644 index 0000000000000000000000000000000000000000..729eade25b96b9c935f3bdb035835d0bf5b6826e --- /dev/null +++ b/src/sakia/gui/navigation/informations/model.py @@ -0,0 +1,179 @@ +import logging +import math + +from PyQt5.QtCore import QLocale, QDateTime, pyqtSignal, QObject +from sakia.errors import NoPeerAvailable + +from sakia.money.currency import shortened +from sakia.money import Referentials +from duniterpy.api import errors + + +class InformationsModel(QObject): + """ + An component + """ + localized_data_changed = pyqtSignal(dict) + + def __init__(self, parent, app, connection, blockchain_service, identities_service, sources_service): + """ + Constructor of an component + + :param sakia.gui.informations.controller.InformationsController parent: the controller + :param sakia.app.Application app: the app + :param sakia.data.entities.Connection connection: the user connection of this node + :param sakia.services.BlockchainService blockchain_service: the service watching the blockchain state + :param sakia.services.IdentitiesService identities_service: the service watching the identities state + :param sakia.services.SourcesService sources_service: the service watching the sources states + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.blockchain_service = blockchain_service + self.identities_service = identities_service + self.sources_service = sources_service + self._logger = logging.getLogger('sakia') + + def get_localized_data(self): + localized_data = {} + #  try to request money parameters + try: + params = self.blockchain_service.parameters() + except NoPeerAvailable as e: + logging.debug('community parameters error : ' + str(e)) + return None + + localized_data['growth'] = params.c + localized_data['days_per_dividend'] = params.dt / 86400 + + last_ud, last_ud_base = self.blockchain_service.last_ud() + members_count = self.blockchain_service.last_members_count() + previous_ud, previous_ud_base = self.blockchain_service.previous_ud() + previous_ud_time = self.blockchain_service.previous_ud_time() + previous_monetary_mass = self.blockchain_service.previous_monetary_mass() + previous_members_count = self.blockchain_service.previous_members_count() + + localized_data['units'] = self.app.current_ref.instance(0, + self.connection.currency, + self.app, None).units + localized_data['diff_units'] = self.app.current_ref.instance(0, + self.connection.currency, + self.app, None).diff_units + + if last_ud: + # display float values + localized_data['ud'] = self.app.current_ref.instance(last_ud * math.pow(10, last_ud_base), + self.connection.currency, + self.app).diff_localized(False, True) + + localized_data['members_count'] = self.blockchain_service.current_members_count() + + computed_dividend = self.blockchain_service.computed_dividend() + # display float values + localized_data['ud_plus_1'] = self.app.current_ref.instance(computed_dividend, + self.connection.currency, self.app).diff_localized(False, True) + + localized_data['mass'] = self.app.current_ref.instance(self.blockchain_service.current_mass(), + self.connection.currency, self.app).diff_localized(False, True) + + localized_data['ud_median_time'] = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(self.blockchain_service.last_ud_time()), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + + localized_data['next_ud_median_time'] = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(self.blockchain_service.last_ud_time() + params.dt), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + + if previous_ud: + mass_minus_1_per_member = (float(0) if previous_ud == 0 else + previous_monetary_mass / previous_members_count) + localized_data['mass_minus_1_per_member'] = self.app.current_ref.instance(mass_minus_1_per_member, + self.connection.currency, self.app) \ + .diff_localized(False, True) + localized_data['mass_minus_1'] = self.app.current_ref.instance(previous_monetary_mass, + self.connection.currency, self.app) \ + .diff_localized(False, True) + # avoid divide by zero ! + if members_count == 0 or previous_members_count == 0: + localized_data['actual_growth'] = float(0) + else: + localized_data['actual_growth'] = (last_ud * math.pow(10, last_ud_base)) / ( + previous_monetary_mass / members_count) + + localized_data['ud_median_time_minus_1'] = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(previous_ud_time), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + return localized_data + + def get_identity_data(self): + amount = self.sources_service.amount(self.connection.pubkey) + localized_amount = self.app.current_ref.instance(amount, + self.connection.currency, + self.app).localized(False, True) + mstime_remaining_text = self.tr("Expired or never published") + outdistanced_text = self.tr("Outdistanced") + is_member = False + nb_certs = 0 + + if self.connection.uid: + try: + identity = self.identities_service.get_identity(self.connection.pubkey, self.connection.uid) + mstime_remaining = self.identities_service.ms_time_remaining(identity) + is_member = identity.member + nb_certs = len(self.identities_service.certifications_received(identity.pubkey)) + if not identity.outdistanced: + outdistanced_text = self.tr("In WoT range") + + if mstime_remaining > 0: + days, remainder = divmod(mstime_remaining, 3600 * 24) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + mstime_remaining_text = self.tr("Expires in ") + if days > 0: + mstime_remaining_text += "{days} days".format(days=days) + else: + mstime_remaining_text += "{hours} hours and {min} min.".format(hours=hours, + min=minutes) + except errors.DuniterError as e: + if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: + pass + else: + self._logger.error(str(e)) + + return { + 'amount': localized_amount, + 'outdistanced': outdistanced_text, + 'nb_certs': nb_certs, + 'mstime': mstime_remaining_text, + 'membership_state': is_member + } + + def parameters(self): + """ + Get community parameters + """ + return self.blockchain_service.parameters() + + def referentials(self): + """ + Get referentials + :return: The list of instances of all referentials + :rtype: list + """ + refs_instances = [] + for ref_class in Referentials: + refs_instances.append(ref_class(0, self.connection.currency, self.app, None)) + return refs_instances + + def short_currency(self): + """ + Get community currency + :return: the community in short currency format + """ + return shortened(self.connection.currency) \ No newline at end of file diff --git a/src/sakia/gui/navigation/informations/view.py b/src/sakia/gui/navigation/informations/view.py new file mode 100644 index 0000000000000000000000000000000000000000..cc8bae79cc966ba1e3575eab97cdd122d42ba0f2 --- /dev/null +++ b/src/sakia/gui/navigation/informations/view.py @@ -0,0 +1,274 @@ +from PyQt5.QtWidgets import QWidget +from PyQt5.QtCore import QEvent +from .informations_uic import Ui_InformationsWidget +from enum import Enum + + +class InformationsView(QWidget, Ui_InformationsWidget): + """ + The view of navigation panel + """ + + class CommunityState(Enum): + NOT_INIT = 0 + OFFLINE = 1 + READY = 2 + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) + self.scrollarea.hide() + self.button_details.clicked.connect(self.handle_details_click) + + def handle_details_click(self): + if self.button_details.isChecked(): + self.scrollarea.show() + else: + self.scrollarea.hide() + + def set_simple_informations(self, data, state): + if state in (InformationsView.CommunityState.NOT_INIT, InformationsView.CommunityState.OFFLINE): + self.label_simple.setText("""<html> + <body> + <p> + <span style=" font-size:16pt; font-weight:600;">{currency}</span> + </p> + <p>{message}</p> + </body> + </html>""".format(currency=data['currency'], + message=InformationsView.simple_message[state])) + else: + status_value = self.tr("Member") if data['membership_state'] else self.tr("Non-Member") + status_color = '#00AA00' if data['membership_state'] else self.tr('#FF0000') + description = """<html> + <body> + <p> + <span style=" font-size:16pt; font-weight:600;">{currency}</span> + </p> + <p>{nb_members} {members_label}</p> + <p><span style="font-weight:600;">{monetary_mass_label}</span> : {monetary_mass}</p> + <p><span style="font-weight:600;">{status_label}</span> : <span style="color:{status_color};">{status}</span></p> + <p><span style="font-weight:600;">{nb_certs_label}</span> : {nb_certs} ({outdistanced_text})</p> + <p><span style="font-weight:600;">{mstime_remaining_label}</span> : {mstime_remaining}</p> + <p><span style="font-weight:600;">{balance_label}</span> : {balance}</p> + </body> + </html>""".format(currency=data['units'], + nb_members=data['members_count'], + members_label=self.tr("members"), + monetary_mass_label=self.tr("Monetary mass"), + monetary_mass=data['mass'], + status_color=status_color, + status_label=self.tr("Status"), + status=status_value, + nb_certs_label=self.tr("Certs. received"), + nb_certs=data['nb_certs'], + outdistanced_text=data['outdistanced'], + mstime_remaining_label=self.tr("Membership"), + mstime_remaining=data['mstime'], + balance_label=self.tr("Balance"), + balance=data['amount']) + self.label_simple.setText(description) + + def set_general_text_no_dividend(self): + """ + Set the general text when there is no dividend + """ + self.label_general.setText(self.tr('No Universal Dividend created yet.')) + + def set_general_text(self, localized_data): + """ + Fill the general text with given informations + :return: + """ + # set infos in label + self.label_general.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></div></td><td>{:} {:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:} {:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:} {:}</td></tr> + <tr><td align="right"><b>{:2.2%} / {:} days</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + </table> + """).format( + localized_data.get('ud', '####'), + self.tr('Universal Dividend UD(t) in'), + localized_data['diff_units'], + localized_data['mass_minus_1'], + self.tr('Monetary Mass M(t-1) in'), + localized_data['units'], + localized_data.get('members_count', '####'), + self.tr('Members N(t)'), + localized_data.get('mass_minus_1_per_member', '####'), + self.tr('Monetary Mass per member M(t-1)/N(t) in'), + localized_data['diff_units'], + localized_data.get('actual_growth', 0), + localized_data.get('days_per_dividend', '####'), + self.tr('Actual growth c = UD(t)/[M(t-1)/N(t)]'), + localized_data.get('ud_median_time_minus_1', '####'), + self.tr('Penultimate UD date and time (t-1)'), + localized_data.get('ud_median_time', '####'), + self.tr('Last UD date and time (t)'), + localized_data.get('next_ud_median_time', '####'), + self.tr('Next UD date and time (t+1)') + ) + ) + + def set_rules_text_no_dividend(self): + """ + Set text when no dividends was generated yet + """ + self.label_rules.setText(self.tr('No Universal Dividend created yet.')) + + def set_rules_text(self, localized_data): + """ + Set text in rules + :param dict localized_data: + :return: + """ + # set infos in label + self.label_rules.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + </table> + """).format( + self.tr('{:2.0%} / {:} days').format(localized_data['growth'], localized_data['days_per_dividend']), + self.tr('Fundamental growth (c) / Delta time (dt)'), + self.tr('UD(t+1) = MAX { UD(t) ; c × M(t) / N(t+1) }'), + self.tr('Universal Dividend (formula)'), + self.tr('{:} = MAX {{ {:} {:} ; {:2.0%} × {:} {:} / {:} }}').format( + localized_data.get('ud_plus_1', '####'), + localized_data.get('ud', '####'), + localized_data['diff_units'], + localized_data.get('growth', '####'), + localized_data.get('mass', '####'), + localized_data['diff_units'], + localized_data.get('members_count', '####') + ), + self.tr('Universal Dividend (computed)') + ) + ) + + def set_text_referentials(self, referentials): + """ + Set text from referentials + :param list referentials: list of referentials + """ + # set infos in label + ref_template = """ + <table cellpadding="5"> + <tr><th>{:}</th><td>{:}</td></tr> + <tr><th>{:}</th><td>{:}</td></tr> + <tr><th>{:}</th><td>{:}</td></tr> + <tr><th>{:}</th><td>{:}</td></tr> + </table> + """ + templates = [] + for ref in referentials: + # print(ref_class.__class__.__name__) + # if ref_class.__class__.__name__ == 'RelativeToPast': + # continue + templates.append(ref_template.format(self.tr('Name'), ref.translated_name(), + self.tr('Units'), ref.units, + self.tr('Formula'), ref.formula, + self.tr('Description'), ref.description + ) + ) + + self.label_referentials.setText('<hr>'.join(templates)) + + def set_money_text(self, params, currency): + """ + Set text from money parameters + :param sakia.data.entities.BlockchainParameters params: Parameters of the currency + :param str currency: The currency + """ + + # set infos in label + self.label_money.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:2.0%} / {:} days</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:} {:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:2.0%}</b></td><td>{:}</td></tr> + </table> + """).format( + params.c, + params.dt / 86400, + self.tr('Fundamental growth (c)'), + params.ud0, + self.tr('Initial Universal Dividend UD(0) in'), + currency, + params.dt / 86400, + self.tr('Time period (dt) in days (86400 seconds) between two UD'), + params.median_time_blocks, + self.tr('Number of blocks used for calculating median time'), + params.avg_gen_time, + self.tr('The average time in seconds for writing 1 block (wished time)'), + params.dt_diff_eval, + self.tr('The number of blocks required to evaluate again PoWMin value'), + params.percent_rot, + self.tr('The percent of previous issuers to reach for personalized difficulty') + ) + ) + + def set_wot_text(self, params): + """ + Set wot text from currency parameters + :param sakia.data.entities.BlockchainParameters params: Parameters of the currency + :return: + """ + + # set infos in label + self.label_wot.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + </table> + """).format( + params.sig_period / 86400, + self.tr('Minimum delay between 2 certifications (in days)'), + params.sig_validity / 86400, + self.tr('Maximum age of a valid signature (in days)'), + params.sig_qty, + self.tr('Minimum quantity of signatures to be part of the WoT'), + params.sig_stock, + self.tr('Maximum quantity of active certifications made by member.'), + params.sig_window, + self.tr('Maximum delay a certification can wait before being expired for non-writing.'), + params.xpercent, + self.tr('Minimum percent of sentries to reach to match the distance rule'), + params.ms_validity / 86400, + self.tr('Maximum age of a valid membership (in days)'), + params.step_max, + self.tr('Maximum distance between each WoT member and a newcomer'), + ) + ) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh() + return super().changeEvent(event) diff --git a/src/sakia/gui/navigation/model.py b/src/sakia/gui/navigation/model.py new file mode 100644 index 0000000000000000000000000000000000000000..7f2387246f1f1d75816bc9936e6e5a350bb7df11 --- /dev/null +++ b/src/sakia/gui/navigation/model.py @@ -0,0 +1,157 @@ +from PyQt5.QtCore import QObject, pyqtSignal +from PyQt5.QtWidgets import QApplication +from sakia.models.generic_tree import GenericTreeModel +from sakia.data.processors import ConnectionsProcessor + + +class NavigationModel(QObject): + """ + The model of Navigation component + """ + navigation_changed = pyqtSignal(GenericTreeModel) + + def __init__(self, parent, app): + """ + + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.app.Application app: + """ + super().__init__(parent) + self.app = app + self.navigation = [] + self._current_data = None + + def init_navigation_data(self): + currencies = ConnectionsProcessor.instanciate(self.app).currencies() + if currencies: + self.navigation = [ + { + 'title': self.tr('Network'), + 'icon': ':/icons/network_icon', + 'component': "Network", + 'dependencies': { + 'network_service': self.app.network_services[currencies[0]], + }, + 'misc': { + }, + 'children': [] + } + ] + else: + self.navigation = [ + { + 'title': self.tr("No connection configured"), + 'component': "HomeScreen", + 'parameters': self.app.parameters, + 'dependencies': {}, + 'misc': {}, + 'children': [] + } + ] + + self._current_data = self.navigation[0] + for connection in self.app.db.connections_repo.get_all(): + self.navigation[0]['children'].append(self.create_node(connection)) + return self.navigation + + def create_node(self, connection): + node = { + 'title': connection.title(), + 'component': "Informations", + 'dependencies': { + 'blockchain_service': self.app.blockchain_services[connection.currency], + 'identities_service': self.app.identities_services[connection.currency], + 'sources_service': self.app.sources_services[connection.currency], + 'connection': connection, + }, + 'misc': { + 'connection': connection + }, + 'children': [ + { + 'title': self.tr('Transfers'), + 'icon': ':/icons/tx_icon', + 'component': "TxHistory", + 'dependencies': { + 'connection': connection, + 'identities_service': self.app.identities_services[connection.currency], + 'blockchain_service': self.app.blockchain_services[connection.currency], + 'transactions_service': self.app.transactions_services[connection.currency], + "sources_service": self.app.sources_services[connection.currency] + }, + 'misc': { + 'connection': connection + } + } + ] + } + if connection.uid: + node["children"] += [{ + 'title': self.tr('Identities'), + 'icon': ':/icons/members_icon', + 'component': "Identities", + 'dependencies': { + 'connection': connection, + 'blockchain_service': self.app.blockchain_services[connection.currency], + 'identities_service': self.app.identities_services[connection.currency], + }, + 'misc': { + 'connection': connection + } + }, + { + 'title': self.tr('Web of Trust'), + 'icon': ':/icons/wot_icon', + 'component': "Wot", + 'dependencies': { + 'connection': connection, + 'blockchain_service': self.app.blockchain_services[connection.currency], + 'identities_service': self.app.identities_services[connection.currency], + }, + 'misc': { + 'connection': connection + } + }] + return node + + def generic_tree(self): + return GenericTreeModel.create("Navigation", self.navigation) + + def add_connection(self, connection): + raw_node = self.create_node(connection) + self.navigation[0]["children"].append(raw_node) + return raw_node + + def set_current_data(self, raw_data): + self._current_data = raw_data + + def current_data(self, key): + return self._current_data.get(key, None) + + def current_connection(self): + if self._current_data: + return self._current_data['misc'].get('connection', None) + else: + return None + + def generate_revokation(self, connection, password): + return self.app.documents_service.generate_revokation(connection, password) + + def identity_published(self, connection): + identities_services = self.app.identities_services[connection.currency] + return identities_services.get_identity(connection.pubkey, connection.uid).written_on != 0 + + def identity_is_member(self, connection): + identities_services = self.app.identities_services[connection.currency] + return identities_services.get_identity(connection.pubkey, connection.uid).member + + async def remove_connection(self, connection): + await self.app.remove_connection(connection) + + async def send_leave(self, connection, password): + return await self.app.documents_service.send_membership(connection, password, "OUT") + + @staticmethod + def copy_pubkey_to_clipboard(connection): + clipboard = QApplication.clipboard() + clipboard.setText(connection.pubkey) \ No newline at end of file diff --git a/src/sakia/gui/navigation/navigation.ui b/src/sakia/gui/navigation/navigation.ui new file mode 100644 index 0000000000000000000000000000000000000000..74af0ce9b1d4976fa1a730e53d8c580594a83953 --- /dev/null +++ b/src/sakia/gui/navigation/navigation.ui @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Navigation</class> + <widget class="QFrame" name="Navigation"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>685</width> + <height>477</height> + </rect> + </property> + <property name="windowTitle"> + <string>Frame</string> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QTreeView" name="tree_view"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>150</width> + <height>0</height> + </size> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>true</bool> + </property> + <property name="headerHidden"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QStackedWidget" name="stacked_widget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/sakia/tests/unit/core/money/__init__.py b/src/sakia/gui/navigation/network/__init__.py similarity index 100% rename from src/sakia/tests/unit/core/money/__init__.py rename to src/sakia/gui/navigation/network/__init__.py diff --git a/src/sakia/gui/navigation/network/controller.py b/src/sakia/gui/navigation/network/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..f610d90f02cbb7d662bfc6fc28cf57086693600d --- /dev/null +++ b/src/sakia/gui/navigation/network/controller.py @@ -0,0 +1,89 @@ +from .model import NetworkModel +from .view import NetworkView +from PyQt5.QtWidgets import QAction, QMenu +from PyQt5.QtGui import QCursor, QDesktopServices +from PyQt5.QtCore import pyqtSlot, QUrl, QObject +from duniterpy.api import bma +from sakia.data.processors import ConnectionsProcessor + + +class NetworkController(QObject): + """ + The network panel + """ + + def __init__(self, parent, view, model): + """ + Constructor of the navigation component + + :param sakia.gui.network.view.NetworkView: the view + :param sakia.gui.network.model.NetworkModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + table_model = self.model.init_network_table_model() + self.view.set_network_table_model(table_model) + self.view.manual_refresh_clicked.connect(self.refresh_nodes_manually) + + @classmethod + def create(cls, parent, app, network_service): + """ + + :param PyQt5.QObject parent: + :param sakia.app.Application app: + :param sakia.services.NetworkService network_service: + :return: + """ + view = NetworkView(parent.view,) + model = NetworkModel(None, app, network_service) + txhistory = cls(parent, view, model) + model.setParent(txhistory) + return txhistory + + def refresh_nodes_manually(self): + self.model.refresh_nodes_once() + + def node_context_menu(self, point): + index = self.view.table_network.indexAt(point) + valid, node, is_root = self.model.table_model_data(index) + if valid: + self.view.show_menu(point, is_root) + menu = QMenu() + if is_root: + unset_root = QAction(self.tr("Unset root node"), self) + unset_root.triggered.connect(self.unset_root_node) + unset_root.setData(node) + if len(self.community.network.root_nodes) > 1: + menu.addAction(unset_root) + else: + set_root = QAction(self.tr("Set as root node"), self) + set_root.triggered.connect(self.set_root_node) + set_root.setData(node) + menu.addAction(set_root) + + if self.app.preferences['expert_mode']: + open_in_browser = QAction(self.tr("Open in browser"), self) + open_in_browser.triggered.connect(self.open_node_in_browser) + open_in_browser.setData(node) + menu.addAction(open_in_browser) + + # Show the context menu. + menu.exec_(QCursor.pos()) + + @pyqtSlot() + def set_root_node(self): + node = self.sender().data() + self.model.add_root_node(node) + + @pyqtSlot() + def unset_root_node(self): + node = self.sender().data() + self.model.unset_root_node(node) + + @pyqtSlot() + def open_node_in_browser(self): + node = self.sender().data() + peering = bma.network.Peering(node.endpoint.conn_handler()) + url = QUrl(peering.reverse_url("http", "/peering")) + QDesktopServices.openUrl(url) \ No newline at end of file diff --git a/src/sakia/gui/navigation/network/model.py b/src/sakia/gui/navigation/network/model.py new file mode 100644 index 0000000000000000000000000000000000000000..4e8c810601f49d478a4eb9cf1856115a2984fb92 --- /dev/null +++ b/src/sakia/gui/navigation/network/model.py @@ -0,0 +1,68 @@ +from .table_model import NetworkTableModel, NetworkFilterProxyModel +from PyQt5.QtCore import QModelIndex, Qt, QObject + + +class NetworkModel(QObject): + """ + A network model + """ + + def __init__(self, parent, app, network_service): + """ + Constructor of an network model + + :param sakia.gui.network.controller.NetworkController parent: the controller + :param sakia.app.Application app: the app + :param sakia.services.NetworkService network_service: the service handling network state + """ + super().__init__(parent) + self.app = app + self.network_service = network_service + self.table_model = None + + def init_network_table_model(self): + model = NetworkTableModel(self.network_service) + proxy = NetworkFilterProxyModel() + proxy.setSourceModel(model) + self.table_model = proxy + model.refresh_nodes() + return self.table_model + + def refresh_nodes_once(self): + """ + Start the refresh of the nodes + :return: + """ + self.network_service.refresh_once() + + def table_model_data(self, index): + """ + Get data at given index + :param PyQt5.QtCore.QModelIndex index: + :return: + """ + if index.isValid() and index.row() < self.table_model.rowCount(QModelIndex()): + source_index = self.table_model.mapToSource(index) + is_root_col = self.table_model.sourceModel().columns_types.index('is_root') + is_root_index = self.table_model.sourceModel().index(source_index.row(), is_root_col) + is_root = self.table_model.sourceModel().data(is_root_index, Qt.DisplayRole) + node = self.community.network.nodes()[source_index.row()] + return True, node, is_root + return False, None, None + + def add_root_node(self, node): + """ + The node set as root + :param sakia.data.entities.Node node: the node + """ + node.root = True + self.network_service.commit_node(node) + + def unset_root_node(self, node): + """ + The node set as root + :param sakia.data.entities.Node node: the node + """ + node.root = False + self.network_service.commit_node(node) + diff --git a/res/ui/network_tab.ui b/src/sakia/gui/navigation/network/network.ui similarity index 82% rename from res/ui/network_tab.ui rename to src/sakia/gui/navigation/network/network.ui index 25edc66568e46a69ea74f9ce6314559b6fa5075b..c36278aa0ca3ec6518b57fe9550007c86fb40d39 100644 --- a/res/ui/network_tab.ui +++ b/src/sakia/gui/navigation/network/network.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>NetworkTabWidget</class> - <widget class="QWidget" name="NetworkTabWidget"> + <class>NetworkWidget</class> + <widget class="QWidget" name="NetworkWidget"> <property name="geometry"> <rect> <x>0</x> @@ -40,7 +40,7 @@ <string/> </property> <property name="icon"> - <iconset resource="../icons/icons.qrc"> + <iconset resource="../../../../res/icons/icons.qrc"> <normaloff>:/icons/refresh_icon</normaloff>:/icons/refresh_icon</iconset> </property> <property name="iconSize"> @@ -86,29 +86,13 @@ </layout> </widget> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../res/icons/icons.qrc"/> </resources> <connections> - <connection> - <sender>table_network</sender> - <signal>customContextMenuRequested(QPoint)</signal> - <receiver>NetworkTabWidget</receiver> - <slot>node_context_menu()</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>149</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> <connection> <sender>button_manual_refresh</sender> <signal>clicked()</signal> - <receiver>NetworkTabWidget</receiver> + <receiver>NetworkWidget</receiver> <slot>manual_nodes_refresh()</slot> <hints> <hint type="sourcelabel"> diff --git a/src/sakia/models/network.py b/src/sakia/gui/navigation/network/table_model.py similarity index 74% rename from src/sakia/models/network.py rename to src/sakia/gui/navigation/network/table_model.py index 07c6c20b3cc1f967a1196e488df65cec6b296d4b..570313a0ee9b3fd932d5bc75d3ba0a884d74dec5 100644 --- a/src/sakia/models/network.py +++ b/src/sakia/gui/navigation/network/table_model.py @@ -4,39 +4,19 @@ Created on 5 févr. 2014 @author: inso """ -import logging -import asyncio from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, QDateTime, QLocale from PyQt5.QtGui import QColor, QFont, QIcon - -from ..tools.exceptions import NoPeerAvailable -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from sakia.core.net.node import Node - +from sakia.data.entities import Node +from duniterpy.documents import BMAEndpoint, SecuredBMAEndpoint class NetworkFilterProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) - self.community = None def columnCount(self, parent): return self.sourceModel().columnCount(None) - 2 - def change_community(self, community): - """ - Change current community and returns refresh task - :param sakia.core.Community community: - :return: the refresh task - :rtype: asyncio.Task - """ - self.community = community - return self.sourceModel().change_community(community) - - def setSourceModel(self, sourceModel): - self.community = sourceModel.community - super().setSourceModel(sourceModel) - def lessThan(self, left, right): """ Sort table by given column number. @@ -124,12 +104,14 @@ class NetworkTableModel(QAbstractTableModel): A Qt abstract item model to display """ - def __init__(self, community, parent=None): + def __init__(self, network_service, parent=None): """ - Constructor + The table showing nodes + :param sakia.services.NetworkService network_service: + :param parent: """ super().__init__(parent) - self.community = community + self.network_service = network_service self.columns_types = ( 'address', 'port', @@ -163,58 +145,43 @@ class NetworkTableModel(QAbstractTableModel): Node.CORRUPTED: lambda: self.tr('Corrupted') } self.nodes_data = [] + self.network_service.nodes_changed.connect(self.refresh_nodes) - def change_community(self, community): - """ - Change current community displayed in network and refresh the nodes - :param sakia.core.Community community: the new community - :return: the refresh task - :rtype: asyncio.Task - """ - cancel_once_task(self, self.refresh_nodes) - self.community = community - return self.refresh_nodes() - - async def data_node(self, node: Node) -> tuple: + def data_node(self, node: Node) -> tuple: """ Return node data tuple :param ..core.net.node.Node node: Network node :return: """ - try: - members_pubkey = await self.community.members_pubkeys() - is_member = node.pubkey in members_pubkey - except NoPeerAvailable as e: - logging.error(e) - is_member = None - - address = "" - if node.endpoint.server: - address = node.endpoint.server - elif node.endpoint.ipv4: - address = node.endpoint.ipv4 - elif node.endpoint.ipv6: - address = node.endpoint.ipv6 - port = node.endpoint.port - - is_root = self.community.network.is_root_node(node) - if node.block: - number, block_hash, block_time = node.block['number'], node.block['hash'], node.block['medianTime'] + + addresses = [] + ports = [] + for e in node.endpoints: + if type(e) in (BMAEndpoint, SecuredBMAEndpoint): + if e.server: + addresses.append(e.server) + elif e.ipv4: + addresses.append(e.ipv4) + elif e.ipv6: + addresses.append(e.ipv6) + ports.append(str(e.port)) + address = "\n".join(addresses) + port = "\n".join(ports) + + if node.current_buid: + number, block_hash, block_time = node.current_buid.number, node.current_buid.sha_hash, node.current_ts else: number, block_hash, block_time = "", "", "" return (address, port, number, block_hash, block_time, node.uid, - is_member, node.pubkey, node.software, node.version, is_root, node.state) + node.member, node.pubkey, node.software, node.version, node.root, node.state) - @once_at_a_time - @asyncify - async def refresh_nodes(self): + def refresh_nodes(self): self.beginResetModel() self.nodes_data = [] nodes_data = [] - if self.community: - for node in self.community.network.nodes: - data = await self.data_node(node) - nodes_data.append(data) + for node in self.network_service.nodes(): + data = self.data_node(node) + nodes_data.append(data) self.nodes_data = nodes_data self.endResetModel() diff --git a/src/sakia/gui/navigation/network/view.py b/src/sakia/gui/navigation/network/view.py new file mode 100644 index 0000000000000000000000000000000000000000..f956145b553784133261178c1ef03db03d1f8268 --- /dev/null +++ b/src/sakia/gui/navigation/network/view.py @@ -0,0 +1,47 @@ +from PyQt5.QtWidgets import QWidget +from PyQt5.QtCore import Qt, QEvent, pyqtSignal +from .network_uic import Ui_NetworkWidget +import asyncio + + +class NetworkView(QWidget, Ui_NetworkWidget): + """ + The view of Network component + """ + manual_refresh_clicked = pyqtSignal() + + def __init__(self, parent): + """ + + :param sakia.gui.network.controller parent: + """ + super().__init__(parent) + self.setupUi(self) + + def set_network_table_model(self, model): + """ + Set the table view model + :param PyQt5.QtCore.QAbstractItemModel model: the model of the table view + """ + self.table_network.setModel(model) + self.table_network.sortByColumn(2, Qt.DescendingOrder) + self.table_network.resizeColumnsToContents() + model.modelAboutToBeReset.connect(lambda: self.table_network.setEnabled(False)) + model.modelReset.connect(lambda: self.table_network.setEnabled(True)) + model.modelReset.connect(self.table_network.resizeColumnsToContents) + + def manual_nodes_refresh(self): + self.button_manual_refresh.setEnabled(False) + asyncio.get_event_loop().call_later(15, lambda: self.button_manual_refresh.setEnabled(True)) + self.manual_refresh_clicked.emit() + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh_nodes() + return super().changeEvent(event) diff --git a/src/sakia/tests/unit/gui/__init__.py b/src/sakia/gui/navigation/txhistory/__init__.py similarity index 100% rename from src/sakia/tests/unit/gui/__init__.py rename to src/sakia/gui/navigation/txhistory/__init__.py diff --git a/src/sakia/gui/navigation/txhistory/controller.py b/src/sakia/gui/navigation/txhistory/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..314ba91c972eb7323fa4882c2bdcd890fe5e308a --- /dev/null +++ b/src/sakia/gui/navigation/txhistory/controller.py @@ -0,0 +1,99 @@ +import logging + +from PyQt5.QtCore import QTime, pyqtSignal, QObject +from PyQt5.QtGui import QCursor + +from sakia.decorators import asyncify +from sakia.gui.widgets import toast +from sakia.gui.widgets.context_menu import ContextMenu +from sakia.data.entities import Identity +from .model import TxHistoryModel +from .view import TxHistoryView + + +class TxHistoryController(QObject): + """ + Transfer history component controller + """ + view_in_wot = pyqtSignal(object) + + def __init__(self, view, model): + super().__init__() + self.view = view + self.model = model + self._logger = logging.getLogger('sakia') + ts_from, ts_to = self.view.get_time_frame() + model = self.model.init_history_table_model(ts_from, ts_to) + self.view.set_table_history_model(model) + + self.view.date_from.dateChanged.connect(self.dates_changed) + self.view.date_to.dateChanged.connect(self.dates_changed) + self.view.table_history.customContextMenuRequested['QPoint'].connect(self.history_context_menu) + self.refresh() + + @classmethod + def create(cls, parent, app, connection, + identities_service, blockchain_service, transactions_service, sources_service): + + view = TxHistoryView(parent.view) + model = TxHistoryModel(None, app, connection, blockchain_service, identities_service, + transactions_service, sources_service) + txhistory = cls(view, model) + model.setParent(txhistory) + app.referential_changed.connect(txhistory.refresh_balance) + app.sources_refreshed.connect(txhistory.refresh_balance) + return txhistory + + def refresh_minimum_maximum(self): + """ + Refresh minimum and maximum datetime + """ + minimum, maximum = self.model.minimum_maximum_datetime() + self.view.set_minimum_maximum_datetime(minimum, maximum) + + def refresh(self): + self.refresh_minimum_maximum() + self.refresh_balance() + + @asyncify + async def notification_reception(self, received_list): + if len(received_list) > 0: + localized_amount = await self.model.received_amount(received_list) + text = self.tr("Received {amount} from {number} transfers").format(amount=localized_amount, + number=len(received_list)) + if self.model.notifications(): + toast.display(self.tr("New transactions received"), text) + + def refresh_balance(self): + localized_amount = self.model.localized_balance() + self.view.set_balance(localized_amount) + + def history_context_menu(self, point): + index = self.view.table_history.indexAt(point) + valid, identity, transfer = self.model.table_data(index) + if valid: + if identity is None: + if transfer.issuer != self.model.connection.pubkey: + pubkey = transfer.issuer + else: + pubkey = transfer.receiver + identity = Identity(currency=transfer.currency, pubkey=pubkey) + menu = ContextMenu.from_data(self.view, self.model.app, self.model.connection, (identity, transfer)) + menu.view_identity_in_wot.connect(self.view_in_wot) + + # Show the context menu. + menu.qmenu.popup(QCursor.pos()) + + def dates_changed(self): + self._logger.debug("Changed dates") + if self.view.table_history.model(): + qdate_from = self.view.date_from + qdate_from.setTime(QTime(0, 0, 0)) + qdate_to = self.view.date_to + qdate_to.setTime(QTime(0, 0, 0)) + ts_from = qdate_from.dateTime().toTime_t() + ts_to = qdate_to.dateTime().toTime_t() + + self.view.table_history.model().set_period(ts_from, ts_to) + + self.refresh_balance() diff --git a/src/sakia/gui/navigation/txhistory/model.py b/src/sakia/gui/navigation/txhistory/model.py new file mode 100644 index 0000000000000000000000000000000000000000..f856c80690968df1ae3a320354210420f6819602 --- /dev/null +++ b/src/sakia/gui/navigation/txhistory/model.py @@ -0,0 +1,124 @@ +from PyQt5.QtCore import QObject +from .table_model import HistoryTableModel, TxFilterProxyModel +from PyQt5.QtCore import Qt, QDateTime, QTime, pyqtSignal, QModelIndex +from sakia.errors import NoPeerAvailable +from duniterpy.api import errors + +import logging + + +class TxHistoryModel(QObject): + """ + The model of Navigation component + """ + def __init__(self, parent, app, connection, blockchain_service, identities_service, + transactions_service, sources_service): + """ + + :param sakia.gui.txhistory.TxHistoryParent parent: the parent controller + :param sakia.app.Application app: the app + :param sakia.data.entities.Connection connection: the connection + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + :param sakia.services.TransactionsService transactions_service: the identities service + :param sakia.services.SourcesService sources_service: the sources service + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.blockchain_service = blockchain_service + self.identities_service = identities_service + self.transactions_service = transactions_service + self.sources_service = sources_service + self._model = None + self._proxy = None + + def init_history_table_model(self, ts_from, ts_to): + """ + Generates a history table model + :param int ts_from: date from where to filter tx + :param int ts_to: date to where to filter tx + :return: + """ + self._model = HistoryTableModel(self, self.app, self.connection, + self.identities_service, self.transactions_service) + self._proxy = TxFilterProxyModel(self, ts_from, ts_to, self.blockchain_service) + self._proxy.setSourceModel(self._model) + self._proxy.setDynamicSortFilter(True) + self._proxy.setSortRole(Qt.DisplayRole) + self._model.init_transfers() + self.app.new_transfer.connect(self._model.add_transfer) + self.app.new_dividend.connect(self._model.add_dividend) + self.app.transaction_state_changed.connect(self._model.change_transfer) + self.app.referential_changed.connect(self._model.modelReset) + + return self._proxy + + def table_data(self, index): + """ + Gets available table data at given index + :param index: + :return: tuple containing (Identity, Transfer) + """ + if index.isValid() and index.row() < self.table_model.rowCount(QModelIndex()): + source_index = self.table_model.mapToSource(index) + + pubkey_col = self.table_model.sourceModel().columns_types.index('pubkey') + pubkey_index = self.table_model.sourceModel().index(source_index.row(), pubkey_col) + pubkey = self.table_model.sourceModel().data(pubkey_index, Qt.DisplayRole) + + identity = self.identities_service.get_identity(pubkey) + + transfer = self.table_model.sourceModel().transfers()[source_index.row()] + return True, identity, transfer + return False, None, None + + def minimum_maximum_datetime(self): + """ + Get minimum and maximum datetime + :rtype: Tuple[PyQt5.QtCore.QDateTime, PyQt5.QtCore.QDateTime] + :return: minimum and maximum datetime + """ + minimum_datetime = QDateTime() + minimum_datetime.setTime_t(0) + tomorrow_datetime = QDateTime().currentDateTime().addDays(1) + return minimum_datetime, tomorrow_datetime + + async def received_amount(self, received_list): + """ + Converts a list of transactions to an amount + :param list received_list: + :return: the amount, localized + """ + amount = 0 + for r in received_list: + amount += r.metadata['amount'] + localized_amount = await self.app.current_ref.instance(amount, + self.connection.currency, + self.app).localized(True, True) + return localized_amount + + def localized_balance(self): + """ + Get the localized amount of the given tx history + :return: the localized amount of given account in given community + :rtype: int + """ + try: + amount = self.sources_service.amount(self.connection.pubkey) + localized_amount = self.app.current_ref.instance(amount, + self.connection.currency, + self.app).localized(False, True) + return localized_amount + except NoPeerAvailable as e: + logging.debug(str(e)) + except errors.DuniterError as e: + logging.debug(str(e)) + return self.tr("Loading...") + + @property + def table_model(self): + return self._proxy + + def notifications(self): + return self.app.parameters.notifications \ No newline at end of file diff --git a/src/sakia/gui/navigation/txhistory/table_model.py b/src/sakia/gui/navigation/txhistory/table_model.py new file mode 100644 index 0000000000000000000000000000000000000000..40d588d8f3fe9ae5f3f811bf3b1602634bcfe1b6 --- /dev/null +++ b/src/sakia/gui/navigation/txhistory/table_model.py @@ -0,0 +1,347 @@ +import datetime +import logging + +from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ + QDateTime, QLocale, QModelIndex +from PyQt5.QtGui import QFont, QColor +from sakia.data.entities import Transaction +from sakia.constants import MAX_CONFIRMATIONS +from sakia.data.processors import BlockchainProcessor + + +class TxFilterProxyModel(QSortFilterProxyModel): + def __init__(self, parent, ts_from, ts_to, blockchain_service): + """ + History of all transactions + :param PyQt5.QtWidgets.QWidget parent: parent widget + :param int ts_from: the min timestamp of latest tx + :param in ts_to: the max timestamp of most recent tx + :param sakia.services.BlockchainService blockchain_service: the blockchain service + """ + super().__init__(parent) + self.app = None + self.ts_from = ts_from + self.ts_to = ts_to + self.blockchain_service = blockchain_service + + def set_period(self, ts_from, ts_to): + """ + Filter table by given timestamps + """ + logging.debug("Filtering from {0} to {1}".format( + datetime.datetime.fromtimestamp(ts_from).isoformat(' '), + datetime.datetime.fromtimestamp(ts_to).isoformat(' ')) + ) + self.beginResetModel() + self.ts_from = ts_from + self.ts_to = ts_to + self.endResetModel() + + def filterAcceptsRow(self, sourceRow, sourceParent): + def in_period(date_ts): + return date_ts in range(self.ts_from, self.ts_to) + + source_model = self.sourceModel() + date_col = source_model.columns_types.index('date') + source_index = source_model.index(sourceRow, date_col) + date = source_model.data(source_index, Qt.DisplayRole) + + return in_period(date) + + def columnCount(self, parent): + return self.sourceModel().columnCount(None) - 5 + + def setSourceModel(self, source_model): + self.app = source_model.app + super().setSourceModel(source_model) + + def lessThan(self, left, right): + """ + Sort table by given column number. + """ + source_model = self.sourceModel() + left_data = source_model.data(left, Qt.DisplayRole) + right_data = source_model.data(right, Qt.DisplayRole) + if left_data == "": + return self.sortOrder() == Qt.DescendingOrder + elif right_data == "": + return self.sortOrder() == Qt.AscendingOrder + if left_data == right_data: + txid_col = source_model.columns_types.index('txid') + txid_left = source_model.index(left.row(), txid_col) + txid_right = source_model.index(right.row(), txid_col) + return txid_left < txid_right + + return left_data < right_data + + def data(self, index, role): + source_index = self.mapToSource(index) + model = self.sourceModel() + source_data = model.data(source_index, role) + state_col = model.columns_types.index('state') + state_index = model.index(source_index.row(), state_col) + state_data = model.data(state_index, Qt.DisplayRole) + + block_col = model.columns_types.index('block_number') + block_index = model.index(source_index.row(), block_col) + block_data = model.data(block_index, Qt.DisplayRole) + + if state_data == Transaction.VALIDATED: + current_confirmations = self.blockchain_service.current_buid().number - block_data + else: + current_confirmations = 0 + + if role == Qt.DisplayRole: + if source_index.column() == model.columns_types.index('uid'): + return source_data + if source_index.column() == model.columns_types.index('date'): + return QLocale.toString( + QLocale(), + QDateTime.fromTime_t(source_data).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ) + if source_index.column() == model.columns_types.index('amount'): + amount = self.app.current_ref.instance(source_data, model.connection.currency, + self.app, block_data).diff_localized(False, True) + return amount + + if role == Qt.FontRole: + font = QFont() + if state_data == Transaction.AWAITING or \ + (state_data == Transaction.VALIDATED and current_confirmations < MAX_CONFIRMATIONS): + font.setItalic(True) + elif state_data == Transaction.REFUSED: + font.setItalic(True) + elif state_data == Transaction.TO_SEND: + font.setBold(True) + else: + font.setItalic(False) + return font + + if role == Qt.ForegroundRole: + if state_data == Transaction.REFUSED: + return QColor(Qt.darkGray) + elif state_data == Transaction.TO_SEND: + return QColor(Qt.blue) + if source_index.column() == model.columns_types.index('amount'): + if source_data < 0: + return QColor(Qt.darkRed) + elif state_data == HistoryTableModel.DIVIDEND: + return QColor(Qt.darkBlue) + + if role == Qt.TextAlignmentRole: + if self.sourceModel().columns_types.index('amount'): + return Qt.AlignRight | Qt.AlignVCenter + if source_index.column() == model.columns_types.index('date'): + return Qt.AlignCenter + + if role == Qt.ToolTipRole: + if source_index.column() == model.columns_types.index('date'): + return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate) + + if state_data == Transaction.VALIDATED or state_data == Transaction.AWAITING: + if current_confirmations >= MAX_CONFIRMATIONS: + return None + elif self.app.parameters.expert_mode: + return self.tr("{0} / {1} confirmations").format(current_confirmations, MAX_CONFIRMATIONS) + else: + confirmation = current_confirmations / MAX_CONFIRMATIONS * 100 + confirmation = 100 if confirmation > 100 else confirmation + return self.tr("Confirming... {0} %").format(QLocale().toString(float(confirmation), 'f', 0)) + + return None + return source_data + + +class HistoryTableModel(QAbstractTableModel): + """ + A Qt abstract item model to display communities in a tree + """ + + DIVIDEND = 32 + + def __init__(self, parent, app, connection, identities_service, transactions_service): + """ + History of all transactions + :param PyQt5.QtWidgets.QWidget parent: parent widget + :param sakia.app.Application app: the main application + :param sakia.data.entities.Connection connection: the connection + :param sakia.services.IdentitiesService identities_service: the identities service + :param sakia.services.TransactionsService transactions_service: the transactions service + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.blockchain_processor = BlockchainProcessor.instanciate(app) + self.identities_service = identities_service + self.transactions_service = transactions_service + self.transfers_data = [] + + self.columns_types = ( + 'date', + 'uid', + 'amount', + 'comment', + 'state', + 'txid', + 'pubkey', + 'block_number', + 'txhash' + ) + + self.column_headers = ( + lambda: self.tr('Date'), + lambda: self.tr('UID/Public key'), + lambda: self.tr('Amount'), + lambda: self.tr('Comment') + ) + + def transfers(self): + """ + Transfer + :rtype: List[sakia.data.entities.Transfer] + """ + return self.transactions_service.transfers(self.connection.pubkey) + + def dividends(self): + """ + Transfer + :rtype: List[sakia.data.entities.Dividend] + """ + return self.transactions_service.dividends(self.connection.pubkey) + + def add_transfer(self, transfer): + self.beginInsertRows(QModelIndex(), len(self.transfers_data), len(self.transfers_data)) + if transfer.issuer == self.connection.pubkey: + self.transfers_data.append(self.data_sent(transfer)) + else: + self.transfers_data.append(self.data_received(transfer)) + self.endInsertRows() + + def add_dividend(self, dividend): + self.beginInsertRows(QModelIndex(), 0, 0) + self.transfers_data.append(self.data_dividend(dividend)) + self.endInsertRows() + + def change_transfer(self, transfer): + for i, data in enumerate(self.transfers_data): + if data[self.columns_types.index('txhash')] == transfer.sha_hash: + if transfer.state == Transaction.DROPPED: + self.beginRemoveRows(QModelIndex(), i, i) + self.transfers_data.pop(i) + self.endRemoveRows() + elif transfer.issuer == self.connection.pubkey: + self.transfers_data[i] = self.data_sent(transfer) + self.dataChanged.emit(self.index(i, 0), self.index(i, len(self.columns_types))) + else: + self.transfers_data[i] = self.data_received(transfer) + self.dataChanged.emit(self.index(i, 0), self.index(i, len(self.columns_types))) + return + + def data_received(self, transfer): + """ + Converts a transaction to table data + :param sakia.data.entities.Transaction transfer: the transaction + :return: data as tuple + """ + block_number = transfer.written_block + + amount = transfer.amount * 10**transfer.amount_base + + identity = self.identities_service.get_identity(transfer.issuer) + if identity: + sender = identity.uid + else: + sender = transfer.issuer + + date_ts = transfer.timestamp + txid = transfer.txid + + return (date_ts, sender, amount, + transfer.comment, transfer.state, txid, + transfer.issuer, block_number, transfer.sha_hash) + + def data_sent(self, transfer): + """ + Converts a transaction to table data + :param sakia.data.entities.Transaction transfer: the transaction + :return: data as tuple + """ + block_number = transfer.written_block + + amount = transfer.amount * 10**transfer.amount_base * -1 + identity = self.identities_service.get_identity(transfer.receiver) + if identity: + receiver = identity.uid + else: + receiver = transfer.receiver + + date_ts = transfer.timestamp + txid = transfer.txid + return (date_ts, receiver, amount, transfer.comment, transfer.state, txid, + transfer.receiver, block_number, transfer.sha_hash) + + def data_dividend(self, dividend): + """ + Converts a transaction to table data + :param sakia.data.entities.Dividend dividend: the dividend + :return: data as tuple + """ + block_number = dividend.block_number + + amount = dividend.amount * 10**dividend.base + identity = self.identities_service.get_identity(dividend.pubkey) + if identity: + receiver = identity.uid + else: + receiver = dividend.pubkey + + date_ts = dividend.timestamp + return (date_ts, receiver, amount, "", HistoryTableModel.DIVIDEND, 0, + receiver, block_number, "") + + def init_transfers(self): + self.beginResetModel() + self.transfers_data = [] + transfers = self.transfers() + for transfer in transfers: + if transfer.state != Transaction.DROPPED: + if transfer.issuer == self.connection.pubkey: + self.transfers_data.append(self.data_sent(transfer)) + else: + self.transfers_data.append(self.data_received(transfer)) + dividends = self.dividends() + for dividend in dividends: + self.transfers_data.append(self.data_dividend(dividend)) + self.endResetModel() + + def rowCount(self, parent): + return len(self.transfers_data) + + def columnCount(self, parent): + return len(self.columns_types) + + def headerData(self, section, orientation, role): + if role == Qt.DisplayRole: + if self.columns_types[section] == 'amount': + dividend, base = self.blockchain_processor.last_ud(self.transactions_service.currency) + header = '{:}'.format(self.column_headers[section]()) + if self.app.current_ref.base_str(base): + header += "\n({:})".format(self.app.current_ref.base_str(base)) + return header + + return self.column_headers[section]() + + def data(self, index, role): + row = index.row() + col = index.column() + + if not index.isValid(): + return QVariant() + + if role in (Qt.DisplayRole, Qt.ForegroundRole, Qt.ToolTipRole): + return self.transfers_data[row][col] + + def flags(self, index): + return Qt.ItemIsSelectable | Qt.ItemIsEnabled + diff --git a/res/ui/transactions_tab.ui b/src/sakia/gui/navigation/txhistory/txhistory.ui similarity index 88% rename from res/ui/transactions_tab.ui rename to src/sakia/gui/navigation/txhistory/txhistory.ui index 737ecfa4aa93d0e094da40eed917ae4b71e16457..838345c54cfeb45b29caea9b3b1dc9f9d1f905d1 100644 --- a/res/ui/transactions_tab.ui +++ b/src/sakia/gui/navigation/txhistory/txhistory.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>transactionsTabWidget</class> - <widget class="QWidget" name="transactionsTabWidget"> + <class>TxHistoryWidget</class> + <widget class="QWidget" name="TxHistoryWidget"> <property name="geometry"> <rect> <x>0</x> @@ -72,16 +72,6 @@ </item> </layout> </item> - <item> - <widget class="QProgressBar" name="progressbar"> - <property name="maximum"> - <number>0</number> - </property> - <property name="value"> - <number>-1</number> - </property> - </widget> - </item> <item> <widget class="QTableView" name="table_history"> <property name="contextMenuPolicy"> @@ -117,7 +107,7 @@ </customwidget> </customwidgets> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../../res/icons/icons.qrc"/> </resources> <connections/> </ui> diff --git a/src/sakia/gui/navigation/txhistory/view.py b/src/sakia/gui/navigation/txhistory/view.py new file mode 100644 index 0000000000000000000000000000000000000000..6a67135394c1e8addc65620c1f778ba907a4e6d1 --- /dev/null +++ b/src/sakia/gui/navigation/txhistory/view.py @@ -0,0 +1,67 @@ +from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView +from PyQt5.QtCore import QDateTime, QEvent, Qt +from .txhistory_uic import Ui_TxHistoryWidget + + +class TxHistoryView(QWidget, Ui_TxHistoryWidget): + """ + The view of TxHistory component + """ + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) + + def get_time_frame(self): + """ + Get the time frame of date filters + :return: the timestamps of the date filters + """ + return self.date_from.dateTime().toTime_t(), self.date_to.dateTime().toTime_t() + + def set_table_history_model(self, model): + """ + Define the table history model + :param QAbstractItemModel model: + :return: + """ + self.table_history.setModel(model) + self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_history.setSortingEnabled(True) + self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) + model.modelReset.connect(self.table_history.resizeColumnsToContents) + + def set_minimum_maximum_datetime(self, minimum, maximum): + """ + Configure minimum and maximum datetime in date filter + :param PyQt5.QtCore.QDateTime minimum: the minimum + :param PyQt5.QtCore.QDateTime maximum: the maximum + """ + self.date_from.setMinimumDateTime(minimum) + self.date_from.setDateTime(minimum) + self.date_from.setMaximumDateTime(QDateTime().currentDateTime()) + + self.date_to.setMinimumDateTime(minimum) + self.date_to.setDateTime(maximum) + self.date_to.setMaximumDateTime(maximum) + + def set_balance(self, balance): + """ + Display given balance + :param balance: the given balance to display + :return: + """ + # set infos in label + self.label_balance.setText( + "{:}".format(balance) + ) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + super().changeEvent(event) \ No newline at end of file diff --git a/src/sakia/gui/navigation/view.py b/src/sakia/gui/navigation/view.py new file mode 100644 index 0000000000000000000000000000000000000000..6c5acc029ba1ffda077d4f10c3fe6e8e33dfe1cb --- /dev/null +++ b/src/sakia/gui/navigation/view.py @@ -0,0 +1,55 @@ +from PyQt5.QtWidgets import QFrame, QSizePolicy +from PyQt5.QtCore import pyqtSignal +from .navigation_uic import Ui_Navigation +from sakia.models.generic_tree import GenericTreeModel + + +class NavigationView(QFrame, Ui_Navigation): + """ + The view of navigation panel + """ + current_view_changed = pyqtSignal(dict) + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) + self.tree_view.clicked.connect(self.handle_click) + self.tree_view.setItemsExpandable(False) + + def set_model(self, model): + """ + Change the navigation pane + :param sakia.gui.navigation.model.NavigationModel + """ + self.tree_view.setModel(model.generic_tree()) + self.tree_view.expandAll() + + def add_widget(self, widget): + """ + Add a widget to the stacked_widget + :param PyQt5.QtWidgets.QWidget widget: the new widget + """ + self.stacked_widget.addWidget(widget) + return widget + + def handle_click(self, index): + """ + Click on the navigation pane + :param PyQt5.QtCore.QModelIndex index: the index + :return: + """ + if index.isValid(): + raw_data = self.tree_view.model().data(index, GenericTreeModel.ROLE_RAW_DATA) + if 'widget' in raw_data: + widget = raw_data['widget'] + if self.stacked_widget.indexOf(widget) != -1: + self.stacked_widget.setCurrentWidget(widget) + self.current_view_changed.emit(raw_data) + + def add_connection(self, raw_data): + self.tree_view.model().insert_node(raw_data) + self.tree_view.expandAll() + + def remove_connection(self, raw_data): + self.tree_view.model().remove_node(raw_data) + self.tree_view.expandAll() diff --git a/src/sakia/gui/network_tab.py b/src/sakia/gui/network_tab.py deleted file mode 100644 index ace7c59e9a5febad3462debcbb5a8735c9ebcc3a..0000000000000000000000000000000000000000 --- a/src/sakia/gui/network_tab.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Created on 20 févr. 2015 - -@author: inso -""" - -import logging -import asyncio - -from PyQt5.QtGui import QCursor, QDesktopServices -from PyQt5.QtWidgets import QWidget, QMenu, QAction -from PyQt5.QtCore import Qt, QModelIndex, pyqtSlot, QUrl, QEvent -from ..models.network import NetworkTableModel, NetworkFilterProxyModel -from duniterpy.api import bma -from ..gen_resources.network_tab_uic import Ui_NetworkTabWidget - - -class NetworkTabWidget(QWidget, Ui_NetworkTabWidget): - """ - classdocs - """ - - def __init__(self, app): - """ - Constructore of a network tab. - - :param sakia.core.Application app: The application - :return: A new network tab. - :rtype: NetworkTabWidget - """ - super().__init__() - self.app = app - self.community = None - - self.setupUi(self) - model = NetworkTableModel(self.community) - proxy = NetworkFilterProxyModel() - proxy.setSourceModel(model) - self.table_network.setModel(proxy) - self.table_network.sortByColumn(2, Qt.DescendingOrder) - self.table_network.resizeColumnsToContents() - model.modelAboutToBeReset.connect(lambda: self.table_network.setEnabled(False)) - model.modelReset.connect(lambda: self.table_network.setEnabled(True)) - - def change_community(self, community): - if self.community: - self.community.network.nodes_changed.disconnect(self.refresh_nodes) - if community: - community.network.nodes_changed.connect(self.refresh_nodes) - - self.community = community - refresh_task = self.table_network.model().change_community(community) - refresh_task.add_done_callback(lambda fut: self.table_network.resizeColumnsToContents()) - - @pyqtSlot() - def refresh_nodes(self): - logging.debug("Refresh nodes") - refresh_task = self.table_network.model().sourceModel().refresh_nodes() - refresh_task.add_done_callback(lambda fut: self.table_network.resizeColumnsToContents()) - - def node_context_menu(self, point): - index = self.table_network.indexAt(point) - model = self.table_network.model() - if index.isValid() and index.row() < model.rowCount(QModelIndex()): - source_index = model.mapToSource(index) - is_root_col = model.sourceModel().columns_types.index('is_root') - is_root_index = model.sourceModel().index(source_index.row(), is_root_col) - is_root = model.sourceModel().data(is_root_index, Qt.DisplayRole) - - menu = QMenu() - if is_root: - unset_root = QAction(self.tr("Unset root node"), self) - unset_root.triggered.connect(self.unset_root_node) - unset_root.setData(self.community.network.root_node_index(source_index.row())) - if len(self.community.network.root_nodes) > 1: - menu.addAction(unset_root) - else: - set_root = QAction(self.tr("Set as root node"), self) - set_root.triggered.connect(self.set_root_node) - set_root.setData(self.community.network.nodes[source_index.row()]) - menu.addAction(set_root) - - if self.app.preferences['expert_mode']: - open_in_browser = QAction(self.tr("Open in browser"), self) - open_in_browser.triggered.connect(self.open_node_in_browser) - open_in_browser.setData(self.community.network.nodes[source_index.row()]) - menu.addAction(open_in_browser) - - # Show the context menu. - menu.exec_(QCursor.pos()) - - @pyqtSlot() - def set_root_node(self): - node = self.sender().data() - self.community.network.add_root_node(node) - self.table_network.model().sourceModel().refresh_nodes() - - @pyqtSlot() - def unset_root_node(self): - index = self.sender().data() - self.community.network.remove_root_node(index) - self.table_network.model().sourceModel().refresh_nodes() - - @pyqtSlot() - def open_node_in_browser(self): - node = self.sender().data() - peering = bma.network.Peering(node.endpoint.conn_handler()) - url = QUrl(peering.reverse_url("http", "/peering")) - QDesktopServices.openUrl(url) - - def manual_nodes_refresh(self): - self.community.network.refresh_once() - self.button_manual_refresh.setEnabled(False) - asyncio.get_event_loop().call_later(15, lambda: self.button_manual_refresh.setEnabled(True)) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh_nodes() - return super(NetworkTabWidget, self).changeEvent(event) diff --git a/src/sakia/gui/node_manager.py b/src/sakia/gui/node_manager.py deleted file mode 100644 index 7dd8432442563ffc58754f7c52ccc8a2102f0ace..0000000000000000000000000000000000000000 --- a/src/sakia/gui/node_manager.py +++ /dev/null @@ -1,58 +0,0 @@ -import aiohttp - -from PyQt5.QtCore import QObject, QEvent, QUrl -from PyQt5.QtWidgets import QDialog - -#from ..gen_resources.node_manager_uic import Ui_NodeManager -from .widgets.dialogs import QAsyncMessageBox -from ..tools.decorators import asyncify - - -class NodeManager(QObject): - """ - A widget showing informations about a member - """ - - def __init__(self, widget, ui): - """ - Init MemberDialog - - :param PyQt5.QtWidget widget: The class of the widget - :param sakia.gen_resources.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget - :return: - """ - super().__init__() - self.widget = widget - self.ui = ui - self.ui.setupUi(self.widget) - self.widget.installEventFilter(self) - - @classmethod - def create(cls, parent): - raise TypeError("Not implemented ( https://github.com/duniter/sakia/issues/399 )") - #dialog = cls(QDialog(parent), Ui_NodeManager()) - #return dialog - - @asyncify - async def open_home_page(self): - try: - with aiohttp.ClientSession() as session: - response = await session.get("http://127.0.0.1:9220") - if response.status == 200: - self.ui.web_view.load(QUrl("http://127.0.0.1:9220")) - self.ui.web_view.show() - self.widget.show() - else: - await QAsyncMessageBox.critical(self.widget, "Local node manager", - "Could not access to local node ui.") - except aiohttp.ClientError: - await QAsyncMessageBox.critical(self.widget, "Local node manager", - "Could not connect to node. Please make sure it's running.") - - def eventFilter(self, source, event): - if event.type() == QEvent.Resize: - self.widget.resizeEvent(event) - return self.widget.eventFilter(source, event) - - def exec(self): - self.widget.exec() diff --git a/src/sakia/gui/password_asker.py b/src/sakia/gui/password_asker.py index be1b97c3a64a2e4e09ff76e638f8797f94aeb707..865bf2baf7efe7b6741b76c709650491dc54ce34 100644 --- a/src/sakia/gui/password_asker.py +++ b/src/sakia/gui/password_asker.py @@ -7,10 +7,11 @@ Created on 24 dec. 2014 import logging import re import asyncio +from duniterpy.key import SigningKey from PyQt5.QtCore import QEvent from PyQt5.QtWidgets import QDialog, QMessageBox -from ..gen_resources.password_asker_uic import Ui_PasswordAskerDialog +from .password_asker_uic import Ui_PasswordAskerDialog class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): @@ -19,47 +20,44 @@ class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): A dialog to get password. """ - def __init__(self, account): + def __init__(self, connection): """ Constructor + + :param sakia.data.entities.Connection connection: a given connection """ super().__init__() self.setupUi(self) - self.account = account self.password = "" + self.connection = connection self.remember = False - def change_account(self, account): - self.remember = False - self.password = "" - self.account = account - def async_exec(self): future = asyncio.Future() - if not self.remember: + if not self.connection.password: def future_show(): pwd = self.password - if not self.remember: - self.password = "" + if self.remember: + self.connection.password = self.password self.finished.disconnect(future_show) future.set_result(pwd) self.open() self.finished.connect(future_show) else: self.setResult(QDialog.Accepted) - future.set_result(self.password) + future.set_result(self.connection.password) return future - def exec_(self): - if not self.remember: + def exec(self): + if not self.connection.password: super().exec_() pwd = self.password - if not self.remember: - self.password = "" + if self.remember: + self.connection.password = self.password return pwd else: self.setResult(QDialog.Accepted) - return self.password + return self.connection.password def accept(self): password = self.edit_password.text() @@ -70,7 +68,7 @@ class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): QMessageBox.Ok) return False - if not self.account.check_password(password): + if SigningKey(self.connection.salt, password, self.connection.scrypt_params).pubkey != self.connection.pubkey: QMessageBox.warning(self, self.tr("Failed to get private key"), self.tr("Wrong password typed. Cannot open the private key"), QMessageBox.Ok) diff --git a/res/ui/password_asker.ui b/src/sakia/gui/password_asker.ui similarity index 100% rename from res/ui/password_asker.ui rename to src/sakia/gui/password_asker.ui diff --git a/src/sakia/gui/preferences.py b/src/sakia/gui/preferences.py index d497cf817233f7b55e6821d868e4572f2165086d..bc8368b738207d0955a334e8790f695d6d653fe1 100644 --- a/src/sakia/gui/preferences.py +++ b/src/sakia/gui/preferences.py @@ -7,8 +7,9 @@ Created on 11 mai 2015 from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import QDialog -from ..core import money -from ..gen_resources.preferences_uic import Ui_PreferencesDialog +from sakia import money +from sakia.data.entities import UserParameters +from .preferences_uic import Ui_PreferencesDialog class PreferencesDialog(QDialog, Ui_PreferencesDialog): @@ -20,62 +21,53 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog): def __init__(self, app): """ Constructor + + :param sakia.app.Application app: """ super().__init__() self.setupUi(self) self.app = app - self.combo_account.addItem("") - for account_name in self.app.accounts.keys(): - self.combo_account.addItem(account_name) - self.combo_account.setCurrentText(self.app.preferences['account']) for ref in money.Referentials: self.combo_referential.addItem(QCoreApplication.translate('Account', ref.translated_name())) - self.combo_referential.setCurrentIndex(self.app.preferences['ref']) - for lang in ('en_GB', 'fr_FR', 'de_DE', 'es_ES', 'it_IT', 'pl_PL', 'pt_BR', 'ru_RU', 'cs_CZ'): + self.combo_referential.setCurrentIndex(self.app.parameters.referential) + for lang in ('en_US', 'fr_FR', 'de_DE', 'es_ES', 'it_IT', 'pl_PL', 'pt_BR', 'ru_RU'): self.combo_language.addItem(lang) - self.combo_language.setCurrentText(self.app.preferences.get('lang', 'en_US')) - self.checkbox_expertmode.setChecked(self.app.preferences.get('expert_mode', False)) - self.checkbox_maximize.setChecked(self.app.preferences.get('maximized', False)) - self.checkbox_notifications.setChecked(self.app.preferences.get('notifications', True)) - self.checkbox_international_system.setChecked(self.app.preferences.get('international_system_of_units', True)) - self.spinbox_digits_comma.setValue(self.app.preferences.get('digits_after_comma', 2)) + self.combo_language.setCurrentText(self.app.parameters.lang) + self.checkbox_expertmode.setChecked(self.app.parameters.expert_mode) + self.checkbox_maximize.setChecked(self.app.parameters.maximized) + self.checkbox_notifications.setChecked(self.app.parameters.notifications) + self.spinbox_digits_comma.setValue(self.app.parameters.digits_after_comma) self.spinbox_digits_comma.setMaximum(12) self.spinbox_digits_comma.setMinimum(1) self.button_app.clicked.connect(lambda: self.stackedWidget.setCurrentIndex(0)) self.button_display.clicked.connect(lambda: self.stackedWidget.setCurrentIndex(1)) self.button_network.clicked.connect(lambda: self.stackedWidget.setCurrentIndex(2)) - self.checkbox_proxy.setChecked(self.app.preferences.get('enable_proxy', False)) + self.checkbox_proxy.setChecked(self.app.parameters.enable_proxy) self.spinbox_proxy_port.setEnabled(self.checkbox_proxy.isChecked()) self.edit_proxy_address.setEnabled(self.checkbox_proxy.isChecked()) self.checkbox_proxy.stateChanged.connect(self.handle_proxy_change) self.spinbox_proxy_port.setMinimum(0) self.spinbox_proxy_port.setMaximum(55636) - self.spinbox_proxy_port.setValue(self.app.preferences.get('proxy_port', 8080)) - self.edit_proxy_address.setText(self.app.preferences.get('proxy_address', "")) - - self.checkbox_forgetfulness.setChecked(self.app.preferences.get('forgetfulness', True)) + self.spinbox_proxy_port.setValue(self.app.parameters.proxy_port) + self.edit_proxy_address.setText(self.app.parameters.proxy_address) def handle_proxy_change(self): self.spinbox_proxy_port.setEnabled(self.checkbox_proxy.isChecked()) self.edit_proxy_address.setEnabled(self.checkbox_proxy.isChecked()) def accept(self): - pref = {'account': self.combo_account.currentText(), - 'lang': self.combo_language.currentText(), - 'ref': self.combo_referential.currentIndex(), - 'expert_mode': self.checkbox_expertmode.isChecked(), - 'maximized': self.checkbox_maximize.isChecked(), - 'digits_after_comma': self.spinbox_digits_comma.value(), - 'notifications': self.checkbox_notifications.isChecked(), - 'enable_proxy': self.checkbox_proxy.isChecked(), - 'proxy_address': self.edit_proxy_address.text(), - 'proxy_port': self.spinbox_proxy_port.value(), - 'international_system_of_units': self.checkbox_international_system.isChecked(), - 'auto_refresh': self.checkbox_auto_refresh.isChecked(), - 'forgetfulness': self.checkbox_forgetfulness.isChecked()} - self.app.save_preferences(pref) + parameters = UserParameters(lang=self.combo_language.currentText(), + referential=self.combo_referential.currentIndex(), + expert_mode=self.checkbox_expertmode.isChecked(), + maximized=self.checkbox_maximize.isChecked(), + digits_after_comma=self.spinbox_digits_comma.value(), + notifications=self.checkbox_notifications.isChecked(), + enable_proxy=self.checkbox_proxy.isChecked(), + proxy_address=self.edit_proxy_address.text(), + proxy_port=self.spinbox_proxy_port.value()) + self.app.save_parameters(parameters) # change UI translation self.app.switch_language() super().accept() diff --git a/res/ui/preferences.ui b/src/sakia/gui/preferences.ui similarity index 79% rename from res/ui/preferences.ui rename to src/sakia/gui/preferences.ui index ccc3312a7605ce316f1e91d60e5fa5021188fe49..6db3496fdd4d6337087a425ab6f8596cca7d606a 100644 --- a/res/ui/preferences.ui +++ b/src/sakia/gui/preferences.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>469</width> - <height>328</height> + <height>330</height> </rect> </property> <property name="windowTitle"> @@ -30,7 +30,7 @@ <string/> </property> <property name="icon"> - <iconset resource="../icons/icons.qrc"> + <iconset resource="../../../res/icons/icons.qrc"> <normaloff>:/icons/settings_app_icon</normaloff>:/icons/settings_app_icon</iconset> </property> <property name="iconSize"> @@ -40,7 +40,7 @@ </size> </property> <property name="flat"> - <bool>true</bool> + <bool>false</bool> </property> </widget> </item> @@ -50,7 +50,7 @@ <string/> </property> <property name="icon"> - <iconset resource="../icons/icons.qrc"> + <iconset resource="../../../res/icons/icons.qrc"> <normaloff>:/icons/settings_display_icon</normaloff>:/icons/settings_display_icon</iconset> </property> <property name="iconSize"> @@ -60,7 +60,7 @@ </size> </property> <property name="flat"> - <bool>true</bool> + <bool>false</bool> </property> </widget> </item> @@ -70,7 +70,7 @@ <string/> </property> <property name="icon"> - <iconset resource="../icons/icons.qrc"> + <iconset resource="../../../res/icons/icons.qrc"> <normaloff>:/icons/settings_network_icon</normaloff>:/icons/settings_network_icon</iconset> </property> <property name="iconSize"> @@ -80,7 +80,7 @@ </size> </property> <property name="flat"> - <bool>true</bool> + <bool>false</bool> </property> </widget> </item> @@ -96,7 +96,7 @@ <item> <widget class="QStackedWidget" name="stackedWidget"> <property name="currentIndex"> - <number>0</number> + <number>1</number> </property> <widget class="QWidget" name="page"> <layout class="QVBoxLayout" name="verticalLayout_7"> @@ -107,20 +107,6 @@ </property> </widget> </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Default account</string> - </property> - </widget> - </item> - <item> - <widget class="QComboBox" name="combo_account"/> - </item> - </layout> - </item> <item> <layout class="QHBoxLayout" name="horizontalLayout_3"> <item> @@ -279,60 +265,6 @@ </item> </layout> </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_10"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QCheckBox" name="checkbox_international_system"> - <property name="text"> - <string>Use International System of Units</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_12"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <spacer name="horizontalSpacer_4"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QCheckBox" name="checkbox_forgetfulness"> - <property name="text"> - <string>Enable forgetfulness</string> - </property> - </widget> - </item> - </layout> - </item> <item> <spacer name="verticalSpacer_2"> <property name="orientation"> @@ -395,20 +327,6 @@ </property> </widget> </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_11"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QCheckBox" name="checkbox_auto_refresh"> - <property name="text"> - <string>Automatically refresh identities informations</string> - </property> - </widget> - </item> - </layout> - </item> <item> <spacer name="verticalSpacer_3"> <property name="orientation"> @@ -441,7 +359,7 @@ </layout> </widget> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../res/icons/icons.qrc"/> </resources> <connections> <connection> diff --git a/src/sakia/gui/process_cfg_account.py b/src/sakia/gui/process_cfg_account.py deleted file mode 100644 index 0eede3a6d870693c2359dba1db7d2d5f5eb2e4d9..0000000000000000000000000000000000000000 --- a/src/sakia/gui/process_cfg_account.py +++ /dev/null @@ -1,322 +0,0 @@ -""" -Created on 6 mars 2014 - -@author: inso -""" -import logging -import asyncio -from math import sqrt, ceil, log, frexp -from duniterpy.key import SigningKey, ScryptParams -from ..gen_resources.account_cfg_uic import Ui_AccountConfigurationDialog -from ..gui.process_cfg_community import ProcessConfigureCommunity -from ..gui.password_asker import PasswordAskerDialog, detect_non_printable -from ..gui.widgets.dialogs import QAsyncMessageBox -from ..models.communities import CommunitiesListModel -from ..tools.exceptions import KeyAlreadyUsed, Error, NoPeerAvailable -from ..tools.decorators import asyncify - -from PyQt5.QtWidgets import QDialog, QMessageBox -from PyQt5.QtCore import QRegExp, pyqtSlot -from PyQt5.QtGui import QRegExpValidator - - -class Step(): - def __init__(self, config_dialog, previous_step=None, next_step=None): - self.previous_step = previous_step - self.next_step = next_step - self.config_dialog = config_dialog - - -class StepPageInit(Step): - """ - First step when adding a community - """ - - def __init__(self, config_dialog): - super().__init__(config_dialog) - - def is_valid(self): - if len(self.config_dialog.edit_account_name.text()) > 2: - return True - else: - return False - - def process_next(self): - if self.config_dialog.account is None: - name = self.config_dialog.edit_account_name.text() - self.config_dialog.account = self.config_dialog.app.create_account(name) - else: - name = self.config_dialog.edit_account_name.text() - self.config_dialog.account.name = name - - def display_page(self): - if self.config_dialog.account is not None: - self.config_dialog.edit_account_name.setText(self.config_dialog.account.name) - model = CommunitiesListModel(self.config_dialog.account) - self.config_dialog.list_communities.setModel(model) - self.config_dialog.password_asker = PasswordAskerDialog(self.config_dialog.account) - - self.config_dialog.button_previous.setEnabled(False) - self.config_dialog.button_next.setEnabled(False) - - -class StepPageKey(Step): - """ - First step when adding a community - """ - - def __init__(self, config_dialog): - super().__init__(config_dialog) - - def is_valid(self): - if self.config_dialog.app.preferences['expert_mode']: - return True - - if len(self.config_dialog.edit_salt.text()) < 6: - self.config_dialog.label_info.setText(self.config_dialog.tr("Forbidden : salt is too short")) - return False - - if len(self.config_dialog.edit_password.text()) < 6: - self.config_dialog.label_info.setText(self.config_dialog.tr("Forbidden : password is too short")) - return False - - if detect_non_printable(self.config_dialog.edit_salt.text()): - self.config_dialog.label_info.setText(self.config_dialog.tr("Forbidden : Invalid characters in salt field")) - return False - - if detect_non_printable(self.config_dialog.edit_password.text()): - self.config_dialog.label_info.setText( - self.config_dialog.tr("Forbidden : Invalid characters in password field")) - return False - - if self.config_dialog.edit_password.text() != \ - self.config_dialog.edit_password_repeat.text(): - self.config_dialog.label_info.setText(self.config_dialog.tr("Error : passwords are different")) - return False - - self.config_dialog.label_info.setText("") - return True - - def process_next(self): - salt = self.config_dialog.edit_salt.text() - password = self.config_dialog.edit_password.text() - self.config_dialog.account.set_scrypt_infos(salt, password, self.config_dialog.scrypt_params) - self.config_dialog.password_asker = PasswordAskerDialog(self.config_dialog.account) - model = CommunitiesListModel(self.config_dialog.account) - self.config_dialog.list_communities.setModel(model) - - def display_page(self): - self.config_dialog.button_previous.setEnabled(False) - self.config_dialog.button_next.setEnabled(False) - - -class StepPageCommunities(Step): - """ - First step when adding a community - """ - - def __init__(self, config_dialog): - super().__init__(config_dialog) - - def is_valid(self): - return True - - def process_next(self): - password = self.config_dialog.password_asker.exec_() - if self.config_dialog.password_asker.result() == QDialog.Rejected: - return - - self.config_dialog.app.add_account(self.config_dialog.account) - if len(self.config_dialog.app.accounts) == 1: - self.config_dialog.app.preferences['account'] = self.config_dialog.account.name - self.config_dialog.app.save(self.config_dialog.account) - self.config_dialog.app.change_current_account(self.config_dialog.account) - - def display_page(self): - logging.debug("Communities DISPLAY") - self.config_dialog.button_previous.setEnabled(False) - self.config_dialog.button_next.setText("Ok") - list_model = CommunitiesListModel(self.config_dialog.account) - self.config_dialog.list_communities.setModel(list_model) - - -class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): - """ - classdocs - """ - - def __init__(self, app, account): - """ - Constructor - """ - # Set up the user interface from Designer. - super().__init__() - self.setupUi(self) - regexp = QRegExp('[A-Za-z0-9_-]*') - validator = QRegExpValidator(regexp) - self.edit_account_name.setValidator(validator) - self.account = account - self.password_asker = None - self.app = app - step_init = StepPageInit(self) - step_key = StepPageKey(self) - step_communities = StepPageCommunities(self) - step_init.next_step = step_key - step_key.next_step = step_communities - self.combo_scrypt.currentIndexChanged.connect(self.handle_combo_change) - self.scrypt_params = ScryptParams(4096, 16, 1) - self.spin_N.setMaximum(2 ** 20) - self.spin_N.setValue(self.scrypt_params.N) - self.spin_N.valueChanged.connect(self.handle_N_change) - self.spin_r.setMaximum(128) - self.spin_r.setValue(self.scrypt_params.r) - self.spin_r.valueChanged.connect(self.handle_r_change) - self.spin_p.setMaximum(128) - self.spin_p.setValue(self.scrypt_params.p) - self.spin_p.valueChanged.connect(self.handle_p_change) - self.step = step_init - self.step.display_page() - if self.account is None: - self.setWindowTitle(self.tr("New account")) - self.button_delete.hide() - else: - self.label_action.setText("Edit account uid") - self.edit_account_name.setPlaceholderText(self.account.name) - self.stacked_pages.removeWidget(self.stacked_pages.widget(1)) - step_init.next_step = step_communities - self.button_next.setEnabled(True) - self.stacked_pages.currentWidget() - - self.setWindowTitle(self.tr("Configure {0}".format(self.account.name))) - - def handle_combo_change(self, index): - strengths = [ - (2**12, 16, 1), - (2**14, 32, 2), - (2**16, 32, 4), - (2**18, 64, 8), - ] - self.spin_N.setValue(strengths[index][0]) - self.spin_r.setValue(strengths[index][1]) - self.spin_p.setValue(strengths[index][2]) - - def handle_N_change(self, value): - spinbox = self.sender() - self.scrypt_params.N = self.compute_power_of_2(spinbox, value, self.scrypt_params.N) - - def handle_r_change(self, value): - spinbox = self.sender() - self.scrypt_params.r = self.compute_power_of_2(spinbox, value, self.scrypt_params.r) - - def handle_p_change(self, value): - spinbox = self.sender() - self.scrypt_params.p = self.compute_power_of_2(spinbox, value, self.scrypt_params.p) - - def compute_power_of_2(self, spinbox, value, param): - if value > 1: - if value > param: - value = pow(2, ceil(log(value) / log(2))) - else: - value -= 1 - value = 2 ** int(log(value, 2)) - else: - value = 1 - - spinbox.blockSignals(True) - spinbox.setValue(value) - spinbox.blockSignals(False) - return value - - def open_process_add_community(self): - logging.debug("Opening configure community dialog") - logging.debug(self.password_asker) - dialog = ProcessConfigureCommunity(self.app, - self.account, None, - self.password_asker) - dialog.accepted.connect(self.action_add_community) - dialog.exec_() - - def action_add_community(self): - logging.debug("Action add community : done") - self.list_communities.setModel(CommunitiesListModel(self.account)) - self.button_next.setEnabled(True) - self.button_next.setText(self.tr("Ok")) - - def action_remove_community(self): - for index in self.list_communities.selectedIndexes(): - self.account.communities.pop(index.row()) - - self.list_communities.setModel(CommunitiesListModel(self.account)) - - def action_edit_community(self): - self.list_communities.setModel(CommunitiesListModel(self.account)) - - def action_edit_account_key(self): - self.button_generate.setEnabled(self.step.is_valid()) - self.button_next.setEnabled(self.step.is_valid()) - - def action_show_pubkey(self): - salt = self.edit_salt.text() - password = self.edit_password.text() - N = self.spin_N.value() - r = self.spin_r.value() - p = self.spin_p.value() - pubkey = SigningKey(salt, password, ScryptParams(N, r, p)).pubkey - self.label_info.setText(pubkey) - - def action_edit_account_parameters(self): - if self.step.is_valid(): - self.button_next.setEnabled(True) - else: - self.button_next.setEnabled(False) - - def open_process_edit_community(self, index): - community = self.account.communities[index.row()] - dialog = ProcessConfigureCommunity(self.app, self.account, community, self.password_asker) - - dialog.accepted.connect(self.action_edit_community) - dialog.exec_() - - @asyncify - async def action_delete_account(self, checked=False): - reply = await QAsyncMessageBox.question(self, self.tr("Warning"), - self.tr("""This action will delete your account ({0}) locally. -Please note your key parameters (salt and password) if you wish to recover it later. -Your account won't be removed from the networks it joined. -Are you sure ?""").format(self.app.current_account.name)) - if reply == QMessageBox.Yes: - account = self.app.current_account - await self.app.delete_account(account) - self.app.save(account) - self.accept() - - def next(self): - if self.step.is_valid(): - try: - self.step.process_next() - if self.step.next_step is not None: - self.step = self.step.next_step - next_index = self.stacked_pages.currentIndex() + 1 - self.stacked_pages.setCurrentIndex(next_index) - self.step.display_page() - else: - self.accept() - except Error as e: - QMessageBox.critical(self, self.tr("Error"), - str(e), QMessageBox.Ok) - - def previous(self): - if self.step.previous_step is not None: - self.step = self.step.previous_step - previous_index = self.stacked_pages.currentIndex() - 1 - self.stacked_pages.setCurrentIndex(previous_index) - self.step.display_page() - - def async_exec(self): - future = asyncio.Future() - self.finished.connect(lambda r: future.set_result(r)) - self.open() - return future - - def accept(self): - super().accept() diff --git a/src/sakia/gui/process_cfg_community.py b/src/sakia/gui/process_cfg_community.py deleted file mode 100644 index 5717579fb5dac384cc8501a94bd48e9f9578c64e..0000000000000000000000000000000000000000 --- a/src/sakia/gui/process_cfg_community.py +++ /dev/null @@ -1,321 +0,0 @@ -""" -Created on 8 mars 2014 - -@author: inso -""" - -import logging -import asyncio - -import aiohttp - -from duniterpy.api import errors -from duniterpy.documents import MalformedDocumentError -from PyQt5.QtWidgets import QDialog, QMenu, QApplication -from PyQt5.QtGui import QCursor -from PyQt5.QtCore import pyqtSignal, QObject - -from ..gen_resources.community_cfg_uic import Ui_CommunityConfigurationDialog -from ..models.peering import PeeringTreeModel -from ..core import Community -from ..core.net import Node -from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox -from ..tools.decorators import asyncify -from ..tools.exceptions import NoPeerAvailable - - -class Step(QObject): - def __init__(self, config_dialog, previous_step=None, next_step=None): - super().__init__() - self.previous_step = previous_step - self.next_step = next_step - self.config_dialog = config_dialog - - -class StepPageInit(Step): - """ - First step when adding a community - """ - def __init__(self, config_dialog): - super().__init__(config_dialog) - self.node = None - logging.debug("Init") - self.config_dialog.button_connect.clicked.connect(self.check_connect) - self.config_dialog.button_register.clicked.connect(self.check_register) - self.config_dialog.button_guest.clicked.connect(self.check_guest) - - @property - def app(self): - return self.config_dialog.app - - @property - def account(self): - return self.config_dialog.account - - @property - def community(self): - return self.config_dialog.community - - @property - def password_asker(self): - return self.config_dialog.password_asker - - @asyncify - async def check_guest(self, checked=False): - server = self.config_dialog.lineedit_server.text() - port = self.config_dialog.spinbox_port.value() - logging.debug("Is valid ? ") - self.config_dialog.label_error.setText(self.tr("connecting...")) - try: - self.node = await Node.from_address(None, server, port, session=aiohttp.ClientSession()) - community = Community.create(self.node) - self.config_dialog.button_connect.setEnabled(False) - self.config_dialog.button_register.setEnabled(False) - self.config_dialog.community = community - self.config_dialog.next() - except aiohttp.errors.DisconnectedError as e: - self.config_dialog.label_error.setText(str(e)) - except aiohttp.errors.ClientError as e: - self.config_dialog.label_error.setText(str(e)) - except (MalformedDocumentError, ValueError) as e: - self.config_dialog.label_error.setText(str(e)) - except aiohttp.errors.TimeoutError: - self.config_dialog.label_error.setText(self.tr("Could not connect. Check hostname, ip address or port")) - - @asyncify - async def check_connect(self, checked=False): - server = self.config_dialog.lineedit_server.text() - port = self.config_dialog.spinbox_port.value() - logging.debug("Is valid ? ") - self.config_dialog.label_error.setText(self.tr("connecting...")) - try: - self.node = await Node.from_address(None, server, port, session=aiohttp.ClientSession()) - community = Community.create(self.node) - self.config_dialog.button_connect.setEnabled(False) - self.config_dialog.button_register.setEnabled(False) - registered = await self.account.check_registered(community) - self.config_dialog.button_connect.setEnabled(True) - self.config_dialog.button_register.setEnabled(True) - if registered[0] is False and registered[2] is None: - self.config_dialog.label_error.setText(self.tr("Could not find your identity on the network.")) - elif registered[0] is False and registered[2]: - self.config_dialog.label_error.setText(self.tr("""Your pubkey or UID is different on the network. -Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) - else: - self.config_dialog.community = community - self.config_dialog.next() - except aiohttp.errors.DisconnectedError as e: - self.config_dialog.label_error.setText(str(e)) - except aiohttp.errors.ClientError as e: - self.config_dialog.label_error.setText(str(e)) - except (MalformedDocumentError, ValueError) as e: - self.config_dialog.label_error.setText(str(e)) - except NoPeerAvailable: - self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) - except aiohttp.errors.TimeoutError: - self.config_dialog.label_error.setText(self.tr("Could not connect. Check hostname, ip address or port")) - - @asyncify - async def check_register(self, checked=False): - server = self.config_dialog.lineedit_server.text() - port = self.config_dialog.spinbox_port.value() - logging.debug("Is valid ? ") - self.config_dialog.label_error.setText(self.tr("connecting...")) - try: - session = aiohttp.ClientSession() - self.node = await Node.from_address(None, server, port, session=session) - community = Community.create(self.node) - self.config_dialog.button_connect.setEnabled(False) - self.config_dialog.button_register.setEnabled(False) - registered = await self.account.check_registered(community) - self.config_dialog.button_connect.setEnabled(True) - self.config_dialog.button_register.setEnabled(True) - if registered[0] is False and registered[2] is None: - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - self.config_dialog.label_error.setText(self.tr("Broadcasting identity...")) - result = await self.account.send_selfcert(password, community) - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) - QApplication.restoreOverrideCursor() - self.config_dialog.next() - else: - self.config_dialog.label_error.setText(self.tr("Error") + " " + \ - self.tr("{0}".format(result[1]))) - if self.app.preferences['notifications']: - toast.display(self.tr("Error"), self.tr("{0}".format(result[1]))) - QApplication.restoreOverrideCursor() - self.config_dialog.community = community - elif registered[0] is False and registered[2]: - self.config_dialog.label_error.setText(self.tr("""Your pubkey or UID was already found on the network. -Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) - else: - self.config_dialog.label_error.setText(self.tr("Your account already exists on the network")) - except (MalformedDocumentError, ValueError, errors.DuniterError, - aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError) as e: - session.close() - self.config_dialog.label_error.setText(str(e)) - except NoPeerAvailable: - self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) - except aiohttp.errors.TimeoutError: - self.config_dialog.label_error.setText(self.tr("Could not connect. Check hostname, ip address or port")) - - def is_valid(self): - return self.node is not None - - def process_next(self): - """ - We create the community - """ - account = self.config_dialog.account - logging.debug("Account : {0}".format(account)) - self.config_dialog.community = Community.create(self.node) - - def display_page(self): - self.config_dialog.button_next.hide() - self.config_dialog.button_previous.hide() - - -class StepPageAddpeers(Step): - """ - The step where the user add peers - """ - def __init__(self, config_dialog): - super().__init__(config_dialog) - - def is_valid(self): - return True - - def process_next(self): - pass - - def display_page(self): - self.config_dialog.button_next.show() - self.config_dialog.button_previous.show() - # We add already known peers to the displayed list - self.config_dialog.nodes = self.config_dialog.community.network.root_nodes - tree_model = PeeringTreeModel(self.config_dialog.community) - - self.config_dialog.tree_peers.setModel(tree_model) - self.config_dialog.button_previous.setEnabled(False) - self.config_dialog.button_next.setText(self.config_dialog.tr("Ok")) - - -class ProcessConfigureCommunity(QDialog, Ui_CommunityConfigurationDialog): - """ - Dialog to configure or add a community - """ - community_added = pyqtSignal() - - def __init__(self, app, account, community, password_asker): - """ - Constructor - - :param sakia.core.Application app: The application - :param sakia.core.Account account: The configured account - :param sakia.core.Community community: The configured community - :param sakia.gui.password_asker.Password_Asker password_asker: The password asker - """ - super().__init__() - self.setupUi(self) - self.app = app - self.community = community - self.account = account - self.password_asker = password_asker - self.step = None - self.nodes = [] - - self.community_added.connect(self.add_community_and_close) - self._step_init = StepPageInit(self) - step_add_peers = StepPageAddpeers(self) - - self._step_init.next_step = step_add_peers - - if self.community is not None: - self.stacked_pages.removeWidget(self.page_node) - self.step = step_add_peers - self.setWindowTitle(self.tr("Configure community {0}").format(self.community.currency)) - else: - self.step = self._step_init - self.setWindowTitle(self.tr("Add a community")) - - self.step.display_page() - - def next(self): - if self.step.next_step is not None: - if self.step.is_valid(): - self.step.process_next() - self.step = self.step.next_step - next_index = self.stacked_pages.currentIndex() + 1 - self.stacked_pages.setCurrentIndex(next_index) - self.step.display_page() - else: - self.add_community_and_close() - - def previous(self): - if self.step.previous_step is not None: - self.step = self.step.previous_step - previous_index = self.stacked_pages.currentIndex() - 1 - self.stacked_pages.setCurrentIndex(previous_index) - self.step.display_page() - - async def start_add_node(self): - """ - Add node slot - """ - server = self.lineedit_add_address.text() - port = self.spinbox_add_port.value() - - try: - node = await Node.from_address(self.community.currency, server, port, session=self.community.network.session) - self.community.add_node(node) - except Exception as e: - await QAsyncMessageBox.critical(self, self.tr("Error"), - str(e)) - self.tree_peers.setModel(PeeringTreeModel(self.community)) - - def add_node(self): - asyncio.ensure_future(self.start_add_node()) - - def remove_node(self): - """ - Remove node slot - """ - logging.debug("Remove node") - index = self.sender().data() - self.community.remove_node(index) - self.tree_peers.setModel(PeeringTreeModel(self.community)) - - @property - def nb_steps(self): - s = self.step - nb_steps = 1 - while s.next_step != None: - s = s.next_step - nb_steps = nb_steps + 1 - return nb_steps - - def showContextMenu(self, point): - if self.stacked_pages.currentIndex() == self.nb_steps - 1: - menu = QMenu() - index = self.tree_peers.indexAt(point) - action = menu.addAction(self.tr("Delete"), self.remove_node) - action.setData(index.row()) - if self.community is not None: - if len(self.nodes) == 1: - action.setEnabled(False) - menu.exec_(QCursor.pos()) - - def async_exec(self): - future = asyncio.Future() - self.finished.connect(lambda r: future.set_result(r)) - self.open() - return future - - def add_community_and_close(self): - if self.community not in self.account.communities: - self.account.add_community(self.community) - self.accept() diff --git a/src/sakia/gui/revocation.py b/src/sakia/gui/revocation.py deleted file mode 100644 index 5de30bf1ef1e64d9a3601ad49fb4340e345da408..0000000000000000000000000000000000000000 --- a/src/sakia/gui/revocation.py +++ /dev/null @@ -1,208 +0,0 @@ -import asyncio -import aiohttp -import logging -from duniterpy.api import errors -from duniterpy.api import bma -from duniterpy.documents import MalformedDocumentError -from duniterpy.documents.certification import Revocation -from sakia.core.net import Node -from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox -from PyQt5.QtCore import QObject - -from .widgets.dialogs import QAsyncMessageBox -from ..tools.decorators import asyncify -from ..gen_resources.revocation_uic import Ui_RevocationDialog - - -class RevocationDialog(QObject): - """ - A dialog to revoke an identity - """ - - def __init__(self, app, account, widget, ui): - """ - Constructor if a certification dialog - - :param sakia.core.Application app: - :param sakia.core.Account account: - :param PyQt5.QtWidgets widget: the widget of the dialog - :param sakia.gen_resources.revocation_uic.Ui_RevocationDialog ui: the view of the certification dialog - :return: - """ - super().__init__() - self.widget = widget - self.ui = ui - self.ui.setupUi(self.widget) - self.app = app - self.account = account - self.revocation_document = None - self.revoked_selfcert = None - self._steps = ( - { - 'page': self.ui.page_load_file, - 'init': self.init_dialog, - 'next': self.revocation_selected - }, - { - 'page': self.ui.page_destination, - 'init': self.init_publication_page, - 'next': self.publish - } - ) - self._current_step = 0 - self.handle_next_step(init=True) - self.ui.button_next.clicked.connect(lambda checked: self.handle_next_step(False)) - - def handle_next_step(self, init=False): - if self._current_step < len(self._steps) - 1: - if not init: - self.ui.button_next.clicked.disconnect(self._steps[self._current_step]['next']) - self._current_step += 1 - self._steps[self._current_step]['init']() - self.ui.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page']) - self.ui.button_next.clicked.connect(self._steps[self._current_step]['next']) - - def init_dialog(self): - self.ui.button_next.setEnabled(False) - self.ui.button_load.clicked.connect(self.load_from_file) - - self.ui.radio_address.toggled.connect(lambda c: self.publication_mode_changed("address")) - self.ui.radio_community.toggled.connect(lambda c: self.publication_mode_changed("community")) - self.ui.edit_address.textChanged.connect(self.refresh) - self.ui.spinbox_port.valueChanged.connect(self.refresh) - self.ui.combo_community.currentIndexChanged.connect(self.refresh) - - def publication_mode_changed(self, radio): - self.ui.edit_address.setEnabled(radio == "address") - self.ui.spinbox_port.setEnabled(radio == "address") - self.ui.combo_community.setEnabled(radio == "community") - self.refresh() - - def load_from_file(self): - selected_files = QFileDialog.getOpenFileName(self.widget, - self.tr("Load a revocation file"), - "", - self.tr("All text files (*.txt)")) - selected_file = selected_files[0] - try: - with open(selected_file, 'r') as file: - file_content = file.read() - self.revocation_document = Revocation.from_signed_raw(file_content) - self.revoked_selfcert = Revocation.extract_self_cert(file_content) - self.refresh() - self.ui.button_next.setEnabled(True) - except FileNotFoundError: - pass - except MalformedDocumentError: - QMessageBox.critical(self.widget, self.tr("Error loading document"), - self.tr("Loaded document is not a revocation document"), - QMessageBox.Ok) - self.ui.button_next.setEnabled(False) - - def revocation_selected(self): - pass - - def init_publication_page(self): - self.ui.combo_community.clear() - if self.account: - for community in self.account.communities: - self.ui.combo_community.addItem(community.currency) - self.ui.radio_community.setChecked(True) - else: - self.ui.radio_address.setChecked(True) - self.ui.radio_community.setEnabled(False) - - def publish(self): - self.ui.button_next.setEnabled(False) - answer = QMessageBox.warning(self.widget, self.tr("Revocation"), - self.tr("""<h4>The publication of this document will remove your identity from the network.</h4> -<li> - <li> <b>This identity won't be able to join the targeted community anymore.</b> </li> - <li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li> - <li> <b>This identity won't be able to certify individuals anymore.</b> </li> -</li> -Please think twice before publishing this document. -"""), QMessageBox.Ok | QMessageBox.Cancel) - if answer == QMessageBox.Ok: - self.accept() - else: - self.ui.button_next.setEnabled(True) - - @asyncify - async def accept(self): - try: - session = aiohttp.ClientSession() - if self.ui.radio_community.isChecked(): - community = self.account.communities[self.ui.combo_community.currentIndex()] - await community.bma_access.broadcast(bma.wot.Revoke, {}, - { - 'revocation': self.revocation_document.signed_raw(self.revoked_selfcert) - }) - elif self.ui.radio_address.isChecked(): - server = self.ui.edit_address.text() - port = self.ui.spinbox_port.value() - node = await Node.from_address(None, server, port, session=session) - conn_handler = node.endpoint.conn_handler() - await bma.wot.Revoke(conn_handler).post(session, - revocation=self.revocation_document.signed_raw(self.revoked_selfcert)) - except (MalformedDocumentError, ValueError, errors.DuniterError, - aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError, - aiohttp.errors.TimeoutError) as e: - await QAsyncMessageBox.critical(self.widget, self.tr("Error broadcasting document"), - str(e)) - else: - await QAsyncMessageBox.information(self.widget, self.tr("Revocation broadcast"), - self.tr("The document was successfully broadcasted.")) - self.widget.accept() - finally: - session.close() - - @classmethod - def open_dialog(cls, app, account): - """ - Certify and identity - :param sakia.core.Application app: the application - :param sakia.core.Account account: the account certifying the identity - :return: - """ - dialog = cls(app, account, QDialog(), Ui_RevocationDialog()) - dialog.refresh() - return dialog.exec() - - def refresh(self): - if self.revoked_selfcert: - text = self.tr(""" -<div>Identity revoked : {uid} (public key : {pubkey}...)</div> -<div>Identity signed on block : {timestamp}</div> - """.format(uid=self.revoked_selfcert.uid, - pubkey=self.revoked_selfcert.pubkey[:12], - timestamp=self.revoked_selfcert.timestamp)) - - self.ui.label_revocation_content.setText(text) - - if self.ui.radio_community.isChecked(): - target = self.tr("All nodes of community {name}".format(name=self.ui.combo_community.currentText())) - elif self.ui.radio_address.isChecked(): - target = self.tr("Address {address}:{port}".format(address=self.ui.edit_address.text(), - port=self.ui.spinbox_port.value())) - else: - target = "" - self.ui.label_revocation_info.setText(""" -<h4>Revocation document</h4> -<div>{text}</div> -<h4>Publication address</h4> -<div>{target}</div> -""".format(text=text, - target=target)) - else: - self.ui.label_revocation_content.setText("") - - def async_exec(self): - future = asyncio.Future() - self.widget.finished.connect(lambda r: future.set_result(r)) - self.widget.open() - self.refresh() - return future - - def exec(self): - self.widget.exec() diff --git a/src/sakia/tests/unit/gui/views/__init__.py b/src/sakia/gui/sub/__init__.py similarity index 100% rename from src/sakia/tests/unit/gui/views/__init__.py rename to src/sakia/gui/sub/__init__.py diff --git a/src/sakia/gui/sub/search_user/controller.py b/src/sakia/gui/sub/search_user/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..787c2aed85d7ad806e1f08e5e8376b2842e8f9ab --- /dev/null +++ b/src/sakia/gui/sub/search_user/controller.py @@ -0,0 +1,64 @@ +from PyQt5.QtCore import pyqtSignal, QObject + +from sakia.data.entities import Identity +from sakia.decorators import asyncify +from .model import SearchUserModel +from .view import SearchUserView + + +class SearchUserController(QObject): + """ + The navigation panel + """ + search_started = pyqtSignal() + search_ended = pyqtSignal() + identity_selected = pyqtSignal(Identity) + + def __init__(self, parent, view, model): + """ + :param sakia.gui.agent.controller.AgentController parent: the parent + :param sakia.gui.search_user.view.SearchUserView view: + :param sakia.gui.search_user.model.SearchUserModel model: + """ + super().__init__(parent) + self.view = view + self.model = model + self.view.search_requested.connect(self.search) + self.view.node_selected.connect(self.select_node) + + @classmethod + def create(cls, parent, app, currency): + view = SearchUserView(parent.view) + model = SearchUserModel(parent, app, currency) + search_user = cls(parent, view, model) + model.setParent(search_user) + return search_user + + @asyncify + async def search(self, text): + """ + Search for a user + :param text: + :return: + """ + if len(text) > 2: + await self.model.find_user(text) + user_nodes = self.model.user_nodes() + self.view.set_search_result(text, user_nodes) + + def current_identity(self): + """ + + :rtype: sakia.core.registry.Identity + """ + return self.model.identity() + + def select_node(self, index): + """ + Select node in graph when item is selected in combobox + """ + self.model.select_identity(index) + self.identity_selected.emit(self.model.identity()) + + def set_currency(self, currency): + self.model.currency = currency diff --git a/src/sakia/gui/sub/search_user/model.py b/src/sakia/gui/sub/search_user/model.py new file mode 100644 index 0000000000000000000000000000000000000000..222aeaa1a38dda2e5dacb773278ba84f00bcb530 --- /dev/null +++ b/src/sakia/gui/sub/search_user/model.py @@ -0,0 +1,69 @@ +from PyQt5.QtCore import QObject +from duniterpy.api import errors +from sakia.errors import NoPeerAvailable +from sakia.data.processors import IdentitiesProcessor + +import logging + + +class SearchUserModel(QObject): + """ + The model of Navigation component + """ + + def __init__(self, parent, app, currency): + """ + + :param sakia.gui.search_user.controller.NetworkController parent: the controller + :param sakia.app.Application app: the app + :param str currency: the currency network to look for users + """ + super().__init__(parent) + self.app = app + self.identities_processor = IdentitiesProcessor.instanciate(app) + self.currency = currency + self._nodes = list() + self._current_identity = None + + def identity(self): + """ + Get current identity selected + :rtype: sakia.core.registry.Identity + """ + return self._current_identity + + def user_nodes(self): + """ + Gets user nodes + :return: + """ + return [n.uid for n in self._nodes] + + async def find_user(self, text): + """ + Search for a user + :param text: + :return: + """ + try: + self._nodes = await self.identities_processor.lookup(self.currency, text) + except errors.DuniterError as e: + if e.ucode == errors.NO_MATCHING_IDENTITY: + self._nodes = list() + else: + logging.debug(str(e)) + except NoPeerAvailable as e: + logging.debug(str(e)) + except BaseException as e: + logging.debug(str(e)) + + def select_identity(self, index): + """ + Select an identity from a node index + :param index: + :return: + """ + if index < 0 or index >= len(self._nodes): + self._current_identity = None + return False + self._current_identity = self._nodes[index] \ No newline at end of file diff --git a/res/ui/search_user_view.ui b/src/sakia/gui/sub/search_user/search_user.ui similarity index 100% rename from res/ui/search_user_view.ui rename to src/sakia/gui/sub/search_user/search_user.ui diff --git a/src/sakia/gui/sub/search_user/view.py b/src/sakia/gui/sub/search_user/view.py new file mode 100644 index 0000000000000000000000000000000000000000..3c13fc892b1c5a8da5e83d93a9587306a2914cb4 --- /dev/null +++ b/src/sakia/gui/sub/search_user/view.py @@ -0,0 +1,64 @@ +from PyQt5.QtWidgets import QWidget, QComboBox +from PyQt5.QtCore import QT_TRANSLATE_NOOP, pyqtSignal, Qt +from .search_user_uic import Ui_SearchUserWidget + + +class SearchUserView(QWidget, Ui_SearchUserWidget): + """ + The model of Navigation component + """ + _search_placeholder = QT_TRANSLATE_NOOP("SearchUserWidget", "Research a pubkey, an uid...") + search_requested = pyqtSignal(str) + reset_requested = pyqtSignal() + node_selected = pyqtSignal(int) + + def __init__(self, parent): + # construct from qtDesigner + super().__init__(parent) + self.setupUi(self) + # Default text when combo lineEdit is empty + self.combobox_search.lineEdit().setPlaceholderText(self.tr(SearchUserView._search_placeholder)) + #  add combobox events + self.combobox_search.lineEdit().returnPressed.connect(self.search) + self.button_reset.clicked.connect(self.reset_requested) + # To fix a recall of the same item with different case, + # the edited text is not added in the item list + self.combobox_search.setInsertPolicy(QComboBox.NoInsert) + self.combobox_search.activated.connect(self.node_selected) + + def search(self): + """ + Search nodes when return is pressed in combobox lineEdit + """ + text = self.combobox_search.lineEdit().text() + self.combobox_search.lineEdit().clear() + self.combobox_search.lineEdit().setPlaceholderText(self.tr("Looking for {0}...".format(text))) + self.search_requested.emit(text) + + def set_search_result(self, text, nodes): + """ + Set the list of users displayed in the combo box + :param str text: the text of the search + :param list[str] nodes: the list of users found + """ + self.blockSignals(True) + self.combobox_search.clear() + if len(nodes) > 0: + self.combobox_search.lineEdit().setText(text) + for uid in nodes: + self.combobox_search.addItem(uid) + self.blockSignals(False) + self.combobox_search.showPopup() + + def retranslateUi(self, widget): + """ + Retranslate missing widgets from generated code + """ + self.combobox_search.lineEdit().setPlaceholderText(self.tr(SearchUserView._search_placeholder)) + super().retranslateUi(self) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Return: + return + + super().keyPressEvent(event) diff --git a/src/sakia/tests/unit/tools/__init__.py b/src/sakia/gui/sub/user_information/__init__.py similarity index 100% rename from src/sakia/tests/unit/tools/__init__.py rename to src/sakia/gui/sub/user_information/__init__.py diff --git a/src/sakia/gui/sub/user_information/controller.py b/src/sakia/gui/sub/user_information/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..5ffb47cce0d62e4c23fcd5e4d68d79b52065b1e7 --- /dev/null +++ b/src/sakia/gui/sub/user_information/controller.py @@ -0,0 +1,86 @@ +from PyQt5.QtWidgets import QDialog, QTabWidget, QVBoxLayout +from PyQt5.QtCore import QObject, pyqtSignal +from sakia.decorators import asyncify +from sakia.gui.widgets.dialogs import dialog_async_exec +from .model import UserInformationModel +from .view import UserInformationView + + +class UserInformationController(QObject): + """ + The homescreen view + """ + identity_loaded = pyqtSignal() + + def __init__(self, parent, view, model): + """ + Constructor of the homescreen component + + :param sakia.gui.homescreen.view.HomeScreenView: the view + :param sakia.gui.homescreen.model.HomeScreenModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + + @classmethod + def create(cls, parent, app, currency, identity): + view = UserInformationView(parent.view if parent else None) + model = UserInformationModel(None, app, currency, identity) + homescreen = cls(parent, view, model) + model.setParent(homescreen) + return homescreen + + @classmethod + def show_identity(cls, parent, app, currency, identity): + dialog = QDialog() + dialog.setWindowTitle("Informations") + user_info = cls.create(parent, app, currency, identity) + user_info.view.setParent(dialog) + user_info.refresh() + dialog.exec() + + @classmethod + @asyncify + async def search_and_show_pubkey(cls, parent, app, currency, pubkey): + dialog = QDialog(parent) + dialog.setWindowTitle("Informations") + layout = QVBoxLayout(dialog) + tabwidget = QTabWidget(dialog) + layout.addWidget(tabwidget) + + identities = await app.identities_services[currency].lookup(pubkey) + for i in identities: + user_info = cls.create(parent, app, currency, i) + user_info.refresh() + tabwidget.addTab(user_info.view, i.uid) + return await dialog_async_exec(dialog) + + @asyncify + async def refresh(self): + if self.model.identity: + self.view.show_busy() + await self.model.load_identity(self.model.identity) + self.view.display_uid(self.model.identity.uid, self.model.identity.member) + self.view.display_identity_timestamps(self.model.identity.pubkey, self.model.identity.timestamp, + self.model.identity.membership_timestamp, + self.model.mstime_remaining(), await self.model.nb_certs()) + self.view.hide_busy() + self.identity_loaded.emit() + + @asyncify + async def search_identity(self, identity): + await self.model.load_identity(identity) + self.refresh() + + def change_identity(self, identity): + """ + Set identity + :param sakia.core.registry.Identity identity: + """ + self.model.identity = identity + self.refresh() + + def set_currency(self, currency): + self.model.set_currency(currency) + self.refresh() \ No newline at end of file diff --git a/src/sakia/gui/sub/user_information/model.py b/src/sakia/gui/sub/user_information/model.py new file mode 100644 index 0000000000000000000000000000000000000000..7751122233e71f4540ecf8c447bfdfe2f75959d1 --- /dev/null +++ b/src/sakia/gui/sub/user_information/model.py @@ -0,0 +1,50 @@ +from PyQt5.QtCore import QObject +from sakia.data.processors import CertificationsProcessor, BlockchainProcessor + + +class UserInformationModel(QObject): + """ + The model of HomeScreen component + """ + + def __init__(self, parent, app, currency, identity): + """ + + :param sakia.gui.user_information.controller.UserInformationController parent: + :param sakia.core.Application app: the app + :param str currency: the currency currently requested + :param sakia.data.entities.Identity identity: the identity + :param sakia.services.IdentitiesService identities_service: the identities service of current currency + """ + super().__init__(parent) + self._certifications_processor = CertificationsProcessor.instanciate(app) + self._blockchain_processor = BlockchainProcessor.instanciate(app) + self.app = app + self.currency = currency + self.identity = identity + if identity: + self.certs_sent = self._certifications_processor.certifications_sent(currency, identity.pubkey) + self.certs_received = self._certifications_processor.certifications_received(currency, identity.pubkey) + if currency: + self.identities_service = self.app.identities_services[self.currency] + else: + self.identities_service = None + + async def load_identity(self, identity): + """ + Ask network service to request identity informations + """ + self.identity = identity + self.identity = await self.identities_service.load_memberships(self.identity) + self.identity = await self.identities_service.load_requirements(self.identity) + + def set_currency(self, currency): + self.currency = currency + self.identities_service = self.app.identities_services[self.currency] + + async def nb_certs(self): + certs = await self.identities_service.load_certifiers_of(self.identity) + return len(certs) + + def mstime_remaining(self): + return self.identities_service.ms_time_remaining(self.identity) diff --git a/res/ui/member.ui b/src/sakia/gui/sub/user_information/user_information.ui similarity index 86% rename from res/ui/member.ui rename to src/sakia/gui/sub/user_information/user_information.ui index 23bb9d1769f46c84eca8ca91a172ae5612de9dce..b7aa85477487eaa90d1ad37f283857503996d91a 100644 --- a/res/ui/member.ui +++ b/src/sakia/gui/sub/user_information/user_information.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>MemberView</class> - <widget class="QWidget" name="MemberView"> + <class>UserInformationWidget</class> + <widget class="QWidget" name="UserInformationWidget"> <property name="geometry"> <rect> <x>0</x> @@ -15,7 +15,7 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QGroupBox" name="groupbox_member"> + <widget class="QGroupBox" name="groupbox_user"> <property name="styleSheet"> <string notr="true">QGroupBox { border: 1px solid gray; @@ -31,7 +31,7 @@ QGroupBox::title { }</string> </property> <property name="title"> - <string>Member</string> + <string>User</string> </property> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="1"> @@ -52,7 +52,7 @@ QGroupBox::title { <string/> </property> <property name="pixmap"> - <pixmap resource="../icons/icons.qrc">:/icons/member_icon</pixmap> + <pixmap resource="../../../../res/icons/icons.qrc">:/icons/member_icon</pixmap> </property> <property name="scaledContents"> <bool>true</bool> @@ -95,7 +95,8 @@ QGroupBox::title { </layout> </widget> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../res/icons/icons.qrc"/> + <include location="../../../../res/icons/icons.qrc"/> </resources> <connections/> </ui> diff --git a/src/sakia/gui/sub/user_information/view.py b/src/sakia/gui/sub/user_information/view.py new file mode 100644 index 0000000000000000000000000000000000000000..8a0ccbc2756a36fc17fd065946be25062c05b826 --- /dev/null +++ b/src/sakia/gui/sub/user_information/view.py @@ -0,0 +1,107 @@ +from PyQt5.QtCore import QLocale, QDateTime +from PyQt5.QtWidgets import QWidget +from .user_information_uic import Ui_UserInformationWidget +from sakia.gui.widgets.busy import Busy + + +class UserInformationView(QWidget, Ui_UserInformationWidget): + """ + User information view + """ + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + self.setupUi(self) + self.busy = Busy(self) + self.busy.hide() + + def display_identity_timestamps(self, pubkey, publish_time, join_date, + mstime_remaining, nb_certs): + """ + Display identity timestamps in localized format + :param str pubkey: + :param int publish_time: + :param int join_date: + :return: + """ + if join_date: + localized_join_date = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(join_date), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + else: + localized_join_date = "###" + + if publish_time: + localized_publish_date = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(publish_time), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + else: + localized_publish_date = "###" + + if mstime_remaining: + days, remainder = divmod(mstime_remaining, 3600 * 24) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + if days > 0: + localized_mstime_remaining = "{days} days".format(days=days) + else: + localized_mstime_remaining = "{hours} hours and {min} min.".format(hours=hours, + min=minutes) + else: + localized_mstime_remaining = "###" + + + text = self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + """).format( + self.tr('Public key'), + pubkey, + self.tr('UID Published on'), + localized_publish_date, + self.tr('Join date'), + localized_join_date, + self.tr("Expires in"), + localized_mstime_remaining, + self.tr("Certs. received"), + nb_certs + ) + + # close html text + text += "</table>" + + # set text in label + self.label_properties.setText(text) + + def display_uid(self, uid, member): + """ + Display the uid in the label + :param str uid: + """ + status_label = self.tr("Member") if member else self.tr("Non-Member") + status_color = '#00AA00' if member else self.tr('#FF0000') + text = "<b>{uid}</b> <p style='color: {status_color};'>({status_label})</p>".format( + uid=uid, status_color=status_color, status_label=status_label + ) + self.label_uid.setText(text) + + def show_busy(self): + self.busy.show() + + def hide_busy(self): + self.busy.hide() + + def resizeEvent(self, event): + self.busy.resize(event.size()) + super().resizeEvent(event) \ No newline at end of file diff --git a/src/sakia/gui/transactions_tab.py b/src/sakia/gui/transactions_tab.py deleted file mode 100644 index a498c54944e80d7fa78313ad712d9e67dd26783c..0000000000000000000000000000000000000000 --- a/src/sakia/gui/transactions_tab.py +++ /dev/null @@ -1,222 +0,0 @@ -import logging - -from duniterpy.api import errors -from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView -from PyQt5.QtCore import Qt, QObject, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent -from PyQt5.QtGui import QCursor - -from ..gen_resources.transactions_tab_uic import Ui_transactionsTabWidget -from ..models.txhistory import HistoryTableModel, TxFilterProxyModel -from ..tools.exceptions import NoPeerAvailable -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from .widgets.context_menu import ContextMenu -from .widgets import toast - - -class TransactionsTabWidget(QObject): - """ - classdocs - """ - view_in_wot = pyqtSignal(object) - - def __init__(self, app, account=None, community=None, password_asker=None, - widget=QWidget, view=Ui_transactionsTabWidget): - """ - Init - - :param sakia.core.app.Application app: Application instance - :param sakia.core.Account account: The account displayed in the widget - :param sakia.core.Community community: The community displayed in the widget - :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords - :param class widget: The class of the PyQt5 widget used for this tab - :param class view: The class of the UI View for this tab - """ - - super().__init__() - self.widget = widget() - self.ui = view() - self.ui.setupUi(self.widget) - self.app = app - self.account = account - self.community = community - self.password_asker = password_asker - self.ui.busy_balance.hide() - - ts_from = self.ui.date_from.dateTime().toTime_t() - ts_to = self.ui.date_to.dateTime().toTime_t() - model = HistoryTableModel(self.app, self.account, self.community) - proxy = TxFilterProxyModel(ts_from, ts_to) - proxy.setSourceModel(model) - proxy.setDynamicSortFilter(True) - proxy.setSortRole(Qt.DisplayRole) - - self.ui.table_history.setModel(proxy) - self.ui.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) - self.ui.table_history.setSortingEnabled(True) - self.ui.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.ui.table_history.resizeColumnsToContents() - - self.ui.table_history.customContextMenuRequested['QPoint'].connect(self.history_context_menu) - self.ui.date_from.dateChanged['QDate'].connect(self.dates_changed) - self.ui.date_to.dateChanged['QDate'].connect(self.dates_changed) - - model.modelAboutToBeReset.connect(lambda: self.ui.table_history.setEnabled(False)) - model.modelReset.connect(lambda: self.ui.table_history.setEnabled(True)) - self.app.refresh_transfers.connect(self.refresh) - - self.ui.progressbar.hide() - self.refresh() - - def cancel_once_tasks(self): - cancel_once_task(self, self.refresh_minimum_maximum) - cancel_once_task(self, self.refresh_balance) - cancel_once_task(self, self.history_context_menu) - - def change_account(self, account, password_asker): - self.cancel_once_tasks() - self.account = account - self.password_asker = password_asker - self.ui.table_history.model().sourceModel().change_account(account) - if account: - self.connect_progress() - - def change_community(self, community): - self.cancel_once_tasks() - self.community = community - self.ui.progressbar.hide() - self.ui.table_history.model().sourceModel().change_community(self.community) - self.refresh() - - @once_at_a_time - @asyncify - async def refresh_minimum_maximum(self): - try: - block = await self.community.get_block(1) - minimum_datetime = QDateTime() - minimum_datetime.setTime_t(block['medianTime']) - minimum_datetime.setTime(QTime(0, 0)) - - self.ui.date_from.setMinimumDateTime(minimum_datetime) - self.ui.date_from.setDateTime(minimum_datetime) - self.ui.date_from.setMaximumDateTime(QDateTime().currentDateTime()) - - self.ui.date_to.setMinimumDateTime(minimum_datetime) - tomorrow_datetime = QDateTime().currentDateTime().addDays(1) - self.ui.date_to.setDateTime(tomorrow_datetime) - self.ui.date_to.setMaximumDateTime(tomorrow_datetime) - except NoPeerAvailable as e: - logging.debug(str(e)) - except errors.DuniterError as e: - logging.debug(str(e)) - - def refresh(self): - if self.community: - refresh_task = self.ui.table_history.model().sourceModel().refresh_transfers() - refresh_task.add_done_callback(lambda fut: self.ui.table_history.resizeColumnsToContents()) - self.refresh_minimum_maximum() - self.refresh_balance() - - def connect_progress(self): - def progressing(community, value, maximum): - if community == self.community: - self.ui.progressbar.show() - self.ui.progressbar.setValue(value) - self.ui.progressbar.setMaximum(maximum) - self.account.loading_progressed.connect(progressing) - self.account.loading_finished.connect(self.stop_progress) - - def stop_progress(self, community, received_list): - if community == self.community: - self.ui.progressbar.hide() - self.ui.table_history.model().sourceModel().refresh_transfers() - self.ui.table_history.resizeColumnsToContents() - self.notification_reception(received_list) - - @asyncify - async def notification_reception(self, received_list): - if len(received_list) > 0: - amount = 0 - for r in received_list: - amount += r.metadata['amount'] - localized_amount = await self.app.current_account.current_ref.instance(amount, self.community, self.app)\ - .localized(units=True, - international_system=self.app.preferences['international_system_of_units']) - text = self.tr("Received {amount} from {number} transfers").format(amount=localized_amount , - number=len(received_list)) - if self.app.preferences['notifications']: - toast.display(self.tr("New transactions received"), text) - - @once_at_a_time - @asyncify - async def refresh_balance(self): - self.ui.busy_balance.show() - try: - amount = await self.app.current_account.amount(self.community) - localized_amount = await self.app.current_account.current_ref.instance(amount, self.community, - self.app).localized(units=True, - international_system=self.app.preferences['international_system_of_units']) - - # set infos in label - self.ui.label_balance.setText( - self.tr("{:}") - .format( - localized_amount - ) - ) - except NoPeerAvailable as e: - logging.debug(str(e)) - except errors.DuniterError as e: - logging.debug(str(e)) - self.ui.busy_balance.hide() - - @once_at_a_time - @asyncify - async def history_context_menu(self, point): - index = self.ui.table_history.indexAt(point) - model = self.ui.table_history.model() - if index.isValid() and index.row() < model.rowCount(QModelIndex()): - source_index = model.mapToSource(index) - - pubkey_col = model.sourceModel().columns_types.index('pubkey') - pubkey_index = model.sourceModel().index(source_index.row(), - pubkey_col) - pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) - - identity = await self.app.identities_registry.future_find(pubkey, self.community) - - transfer = model.sourceModel().transfers()[source_index.row()] - menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, - (identity, transfer)) - menu.view_identity_in_wot.connect(self.view_in_wot) - - # Show the context menu. - menu.qmenu.popup(QCursor.pos()) - - def dates_changed(self): - logging.debug("Changed dates") - if self.ui.table_history.model(): - qdate_from = self.ui.date_from - qdate_from.setTime(QTime(0, 0, 0)) - qdate_to = self.ui.date_to - qdate_to.setTime(QTime(0, 0, 0)) - ts_from = qdate_from.dateTime().toTime_t() - ts_to = qdate_to.dateTime().toTime_t() - - self.ui.table_history.model().set_period(ts_from, ts_to) - - self.refresh_balance() - - def resizeEvent(self, event): - self.ui.busy_balance.resize(event.size()) - super().resizeEvent(event) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh() - return super(TransactionsTabWidget, self).changeEvent(event) diff --git a/src/sakia/gui/transfer.py b/src/sakia/gui/transfer.py deleted file mode 100644 index 3fb340db67de93cf6b19c331b4f965c6c2e834c9..0000000000000000000000000000000000000000 --- a/src/sakia/gui/transfer.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" -import asyncio -import logging - -from PyQt5.QtWidgets import QDialog, QApplication, QDialogButtonBox -from PyQt5.QtCore import QRegExp, Qt, QObject - -from PyQt5.QtGui import QRegExpValidator - -from ..gen_resources.transfer_uic import Ui_TransferMoneyDialog -from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox, QMessageBox -from ..tools.decorators import asyncify - - -class TransferMoneyDialog(QObject): - - """ - classdocs - """ - - def __init__(self, app, account, password_asker, community, transfer, widget=QDialog, view=Ui_TransferMoneyDialog): - """ - Constructor - :param sakia.core.Application app: The application - :param sakia.core.Account account: The account - :param sakia.gui.password_asker.Password_Asker password_asker: The password asker - :param sakia.core.Community community: - :param sakia.core.Transfer transfer: - :param class widget: - :param class view: - :return: - """ - super().__init__() - self.widget = widget() - self.ui = view() - self.ui.setupUi(self.widget) - - self.app = app - self.account = account - self.password_asker = password_asker - self.recipient_trusts = [] - self.transfer = transfer - self.wallet = None - self.community = community if community else self.account.communities[0] - self.wallet = self.account.wallets[0] - - self.ui.radio_contact.toggled.connect(lambda c, radio="contact": self.recipient_mode_changed(radio)) - self.ui.radio_pubkey.toggled.connect(lambda c, radio="pubkey": self.recipient_mode_changed(radio)) - self.ui.radio_search.toggled.connect(lambda c, radio="search": self.recipient_mode_changed(radio)) - self.ui.button_box.accepted.connect(self.accept) - self.ui.button_box.rejected.connect(self.widget.reject) - self.ui.combo_wallets.currentIndexChanged.connect(self.change_displayed_wallet) - self.ui.combo_community.currentIndexChanged.connect(self.change_current_community) - self.ui.spinbox_relative.valueChanged.connect(self.relative_amount_changed) - self.ui.spinbox_amount.valueChanged.connect(self.amount_changed) - self.ui.search_user.button_reset.hide() - self.ui.search_user.init(self.app) - self.ui.search_user.change_account(self.account) - self.ui.search_user.change_community(self.community) - self.ui.search_user.search_started.connect(lambda: self.ui.button_box.setEnabled(False)) - self.ui.search_user.search_completed.connect(lambda: self.ui.button_box.setEnabled(True)) - - regexp = QRegExp('^([ a-zA-Z0-9-_:/;*?\[\]\(\)\\\?!^+=@&~#{}|<>%.]{0,255})$') - validator = QRegExpValidator(regexp) - self.ui.edit_message.setValidator(validator) - - for community in self.account.communities: - self.ui.combo_community.addItem(community.currency) - - for wallet in self.account.wallets: - self.ui.combo_wallets.addItem(wallet.name) - - for contact_name in sorted([c['name'] for c in account.contacts], key=str.lower): - self.ui.combo_contact.addItem(contact_name) - - if len(self.account.contacts) == 0: - self.ui.combo_contact.setEnabled(False) - self.ui.radio_contact.setEnabled(False) - self.ui.radio_pubkey.setChecked(True) - - self.ui.combo_community.setCurrentText(self.community.name) - - if self.transfer: - account = self.transfer.metadata['issuer'] - wallet_index = [w.pubkey for w in app.current_account.wallets].index(account) - self.ui.combo_wallets.setCurrentIndex(wallet_index) - self.ui.edit_pubkey.setText(transfer.metadata['receiver']) - self.ui.radio_pubkey.setChecked(True) - self.ui.edit_message.setText(transfer.metadata['comment']) - - @classmethod - async def send_money_to_identity(cls, app, account, password_asker, community, identity): - dialog = cls(app, account, password_asker, community, None) - dialog.ui.edit_pubkey.setText(identity.pubkey) - dialog.ui.radio_pubkey.setChecked(True) - return await dialog.async_exec() - - @classmethod - async def send_transfer_again(cls, app, account, password_asker, community, transfer): - dialog = cls(app, account, password_asker, community, transfer) - dividend = await community.dividend() - relative = transfer.metadata['amount'] / dividend - dialog.ui.spinbox_amount.setMaximum(transfer.metadata['amount']) - dialog.ui.spinbox_relative.setMaximum(relative) - dialog.ui.spinbox_amount.setValue(transfer.metadata['amount']) - - return await dialog.async_exec() - - @asyncify - async def accept(self): - logging.debug("Accept transfer action...") - self.ui.button_box.setEnabled(False) - comment = self.ui.edit_message.text() - - logging.debug("checking recipient mode...") - if self.ui.radio_contact.isChecked(): - for contact in self.account.contacts: - if contact['name'] == self.ui.combo_contact.currentText(): - recipient = contact['pubkey'] - break - elif self.ui.radio_search.isChecked(): - if self.ui.search_user.current_identity(): - recipient = self.ui.search_user.current_identity().pubkey - else: - return - else: - recipient = self.ui.edit_pubkey.text() - amount = self.ui.spinbox_amount.value() - - logging.debug("checking amount...") - if not amount: - await QAsyncMessageBox.critical(self.widget, self.tr("Money transfer"), - self.tr("No amount. Please give the transfert amount"), - QMessageBox.Ok) - self.ui.button_box.setEnabled(True) - return - logging.debug("Showing password dialog...") - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - self.ui.button_box.setEnabled(True) - return - - logging.debug("Setting cursor...") - QApplication.setOverrideCursor(Qt.WaitCursor) - - logging.debug("Send money...") - result = await self.wallet.send_money(self.account.salt, password, self.account.scrypt_params, - self.community, recipient, amount, comment) - if result[0]: - logging.debug("Checking result to display...") - if self.app.preferences['notifications']: - toast.display(self.tr("Transfer"), - self.tr("Success sending money to {0}").format(recipient)) - else: - await QAsyncMessageBox.information(self.widget, self.tr("Transfer"), - self.tr("Success sending money to {0}").format(recipient)) - logging.debug("Restore cursor...") - QApplication.restoreOverrideCursor() - - # If we sent back a transaction we cancel the first one - if self.transfer: - self.transfer.cancel() - self.app.refresh_transfers.emit() - self.widget.accept() - else: - logging.debug("Error occured...") - if self.app.preferences['notifications']: - toast.display(self.tr("Transfer"), "Error : {0}".format(result[1])) - else: - await QAsyncMessageBox.critical(self.widget, self.tr("Transfer"), result[1]) - - QApplication.restoreOverrideCursor() - self.ui.button_box.setEnabled(True) - - @asyncify - async def amount_changed(self, value): - ud_block = await self.community.get_ud_block() - if ud_block: - dividend = ud_block['dividend'] - base = ud_block['unitbase'] - else: - dividend = 1 - base = 0 - relative = value / (dividend * pow(10, base)) - self.ui.spinbox_relative.blockSignals(True) - self.ui.spinbox_relative.setValue(relative) - self.ui.spinbox_relative.blockSignals(False) - correct_amount = int(pow(10, base) * round(float(value) / pow(10, base))) - self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(correct_amount == value) - - @asyncify - async def relative_amount_changed(self, value): - ud_block = await self.community.get_ud_block() - if ud_block: - dividend = ud_block['dividend'] - base = ud_block['unitbase'] - else: - dividend = 1 - base = 0 - amount = value * dividend * pow(10, base) - amount = int(pow(10, base) * round(float(amount) / pow(10, base))) - self.ui.spinbox_amount.blockSignals(True) - self.ui.spinbox_amount.setValue(amount) - self.ui.spinbox_amount.blockSignals(False) - self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(True) - - @asyncify - async def change_current_community(self, index): - self.community = self.account.communities[index] - amount = await self.wallet.value(self.community) - - ref_text = await self.account.current_ref.instance(amount, self.community, self.app)\ - .diff_localized(units=True, - international_system=self.app.preferences['international_system_of_units']) - self.ui.label_total.setText("{0}".format(ref_text)) - self.ui.spinbox_amount.setSuffix(" " + self.community.currency) - await self.refresh_spinboxes() - - @asyncify - async def change_displayed_wallet(self, index): - self.wallet = self.account.wallets[index] - amount = await self.wallet.value(self.community) - ref_text = await self.account.current_ref.instance(amount, self.community, self.app)\ - .diff_localized(units=True, - international_system=self.app.preferences['international_system_of_units']) - self.ui.label_total.setText("{0}".format(ref_text)) - await self.refresh_spinboxes() - - async def refresh_spinboxes(self): - max_amount = await self.wallet.value(self.community) - ud_block = await self.community.get_ud_block() - if ud_block: - dividend = ud_block['dividend'] - base = ud_block['unitbase'] - else: - dividend = 1 - base = 0 - max_amount = int(pow(10, base) * round(float(max_amount) / pow(10, base))) - max_relative = max_amount / dividend - self.ui.spinbox_amount.setMaximum(max_amount) - self.ui.spinbox_relative.setMaximum(max_relative) - self.ui.spinbox_amount.setSingleStep(pow(10, base)) - - def recipient_mode_changed(self, radio): - self.ui.edit_pubkey.setEnabled(radio == "pubkey") - self.ui.combo_contact.setEnabled(radio == "contact") - self.ui.search_user.setEnabled(radio == "search") - - def async_exec(self): - future = asyncio.Future() - self.widget.finished.connect(lambda r: future.set_result(r) and self.widget.finished.disconnect()) - self.widget.open() - return future - - def exec(self): - self.widget.exec() \ No newline at end of file diff --git a/src/sakia/gui/views/__init__.py b/src/sakia/gui/views/__init__.py deleted file mode 100644 index 3ca1e9f7ff0ac7bd3d3e7c9e4d8344dd36a87a54..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .explorer import ExplorerView -from .wot import WotView \ No newline at end of file diff --git a/src/sakia/gui/views/edges/__init__.py b/src/sakia/gui/views/edges/__init__.py deleted file mode 100644 index 188e3acfd2ccf101092227b52be070f696cd68c4..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/edges/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .wot_edge import WotEdge -from .explorer_edge import ExplorerEdge \ No newline at end of file diff --git a/src/sakia/gui/views/edges/explorer_edge.py b/src/sakia/gui/views/edges/explorer_edge.py deleted file mode 100644 index 6f39bea9cc3fc8879eaa3575eadf0b5d2065b2c8..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/edges/explorer_edge.py +++ /dev/null @@ -1,154 +0,0 @@ -from PyQt5.QtCore import Qt, QRectF, QLineF, QPointF, QSizeF, \ - qFuzzyCompare, QTimeLine -from PyQt5.QtGui import QColor, QPen, QPolygonF -import math -from .base_edge import BaseEdge -from ....core.graph.constants import EdgeStatus - - -class ExplorerEdge(BaseEdge): - def __init__(self, source_node, destination_node, metadata, nx_pos, steps, steps_max): - """ - Create an arc between two nodes - - :param Node source_node: Source node of the arc - :param Node destination_node: Destination node of the arc - :param dict metadata: Arc metadata - :param dict nx_pos: The position generated by nx_graph - :param int steps: The steps from the center identity - :param int steps_max: The steps max of the graph - """ - super().__init__(source_node, destination_node, metadata, nx_pos) - - self.source_point = self.destination_point - self.steps = steps - self.steps_max = steps_max - self.highlighted = False - - self.arrow_size = 5 - # cursor change on hover - self.setAcceptHoverEvents(True) - self.setZValue(0) - self._line_styles = { - EdgeStatus.STRONG: Qt.SolidLine, - EdgeStatus.WEAK: Qt.DashLine - } - self.timeline = None - - @property - def line_style(self): - return self._line_styles[self.status] - - # virtual function require subclassing - def boundingRect(self): - """ - Return the bounding rectangle size - - :return: QRectF - """ - if not self.source or not self.destination: - return QRectF() - pen_width = 1.0 - extra = (pen_width + self.arrow_size) / 2.0 - - return QRectF( - self.source_point, QSizeF( - self.destination_point.x() - self.source_point.x(), - self.destination_point.y() - self.source_point.y() - ) - ).normalized().adjusted( - -extra, - -extra, - extra, - extra - ) - - def paint(self, painter, option, widget): - """ - Customize line adding an arrow head - - :param QPainter painter: Painter instance of the item - :param option: Painter option of the item - :param widget: Widget instance - """ - if not self.source or not self.destination: - return - line = QLineF(self.source_point, self.destination_point) - if qFuzzyCompare(line.length(), 0): - return - - # Draw the line itself - color = QColor() - color.setHsv(120 - 60 / self.steps_max * self.steps, - 180 + 50 / self.steps_max * self.steps, - 150 + 80 / self.steps_max * self.steps) - if self.highlighted: - color.setHsv(0, 0, 0) - - style = self.line_style - - painter.setPen(QPen(color, 1, style, Qt.RoundCap, Qt.RoundJoin)) - painter.drawLine(line) - painter.setPen(QPen(color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) - - # Draw the arrows - angle = math.acos(line.dx() / line.length()) - if line.dy() >= 0: - angle = (2.0 * math.pi) - angle - - # arrow in the middle of the arc - hpx = line.p1().x() + (line.dx() / 2.0) - hpy = line.p1().y() + (line.dy() / 2.0) - head_point = QPointF(hpx, hpy) - - painter.setPen(QPen(color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) - destination_arrow_p1 = head_point + QPointF( - math.sin(angle - math.pi / 3) * self.arrow_size, - math.cos(angle - math.pi / 3) * self.arrow_size) - destination_arrow_p2 = head_point + QPointF( - math.sin(angle - math.pi + math.pi / 3) * self.arrow_size, - math.cos(angle - math.pi + math.pi / 3) * self.arrow_size) - - painter.setBrush(color) - painter.drawPolygon(QPolygonF([head_point, destination_arrow_p1, destination_arrow_p2])) - - if self.metadata["confirmation_text"]: - painter.drawText(head_point, self.metadata["confirmation_text"]) - - def move_source_point(self, node_id, x, y): - """ - Move to corresponding position - :param str node_id: the node id - :param float x: x coordinates - :param float y: y coordinates - :return: - """ - if node_id == self.source: - self.source_point = QPointF(x, y) - self.update(self.boundingRect()) - - def move_destination_point(self, node_id, x, y): - """ - Move to corresponding position - :param str node_id: the node id - :param float x: x coordinates - :param float y: y coordinates - :return: - """ - if node_id == self.destination: - self.destination_point = QPointF(x, y) - self.update(self.boundingRect()) - - def highlight(self): - """ - Highlight the edge in the scene - """ - self.highlighted = True - self.update(self.boundingRect()) - - def neutralize(self): - """ - Neutralize the edge in the scene - """ - self.highlighted = False - self.update(self.boundingRect()) diff --git a/src/sakia/gui/views/explorer.py b/src/sakia/gui/views/explorer.py deleted file mode 100644 index 927d9f085cb63c2c633a878a0c70cb4f380781f1..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/explorer.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging - -import networkx -from PyQt5.QtCore import Qt, QPoint, pyqtSignal -from PyQt5.QtGui import QPainter, QWheelEvent -from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene - -from .scenes import ExplorerScene - - -class ExplorerView(QGraphicsView): - def __init__(self, parent=None): - """ - Create View to display scene - - :param parent: [Optional, default=None] Parent widget - """ - super().__init__(parent) - - self.setScene(ExplorerScene(self)) - - self.setCacheMode(QGraphicsView.CacheBackground) - self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate) - self.setRenderHint(QPainter.Antialiasing) - self.setRenderHint(QPainter.SmoothPixmapTransform) - - def wheelEvent(self, event: QWheelEvent): - """ - Zoom in/out on the mouse cursor - """ - # zoom only when CTRL key pressed - if (event.modifiers() & Qt.ControlModifier) == Qt.ControlModifier: - steps = event.angleDelta().y() / 15 / 8 - - if steps == 0: - event.ignore() - return - - # scale factor 1.25 - sc = pow(1.25, steps) - self.scale(sc, sc) - self.centerOn(self.mapToScene(event.pos())) - event.accept() - # act normally on scrollbar - else: - # transmit event to parent class wheelevent - super(QGraphicsView, self).wheelEvent(event) diff --git a/src/sakia/gui/views/nodes/__init__.py b/src/sakia/gui/views/nodes/__init__.py deleted file mode 100644 index 28332381ab8beea33a4eb493b6d21af09a582adb..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/nodes/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .wot_node import WotNode -from .explorer_node import ExplorerNode \ No newline at end of file diff --git a/src/sakia/gui/views/nodes/explorer_node.py b/src/sakia/gui/views/nodes/explorer_node.py deleted file mode 100644 index 854d1e8fef5ac9b9eaa50aaf9981a1a696840037..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/nodes/explorer_node.py +++ /dev/null @@ -1,189 +0,0 @@ -from PyQt5.QtWidgets import QGraphicsSimpleTextItem -from PyQt5.QtCore import Qt, QPointF, QTimeLine, QTimer -from PyQt5.QtGui import QTransform, QColor, QPen, QBrush, QRadialGradient -from ....core.graph.constants import NodeStatus -from .base_node import BaseNode -import math - - -class ExplorerNode(BaseNode): - def __init__(self, nx_node, center_pos, nx_pos, steps, steps_max, small): - """ - Create node in the graph scene - - :param tuple nx_node: Node info - :param center_pos: The position of the center node - :param nx_pos: Position of the nodes in the graph - :param int steps: The steps from the center identity - :param int steps_max: The steps max of the graph - :param bool small: Small dots for big networks - """ - super().__init__(nx_node, nx_pos) - - self.steps = steps - self.steps_max = steps_max - self.highlighted = False - self.status_sentry = False - - if small: - self.setRect( - 0, - 0, - 10, - 10 - ) - self.text_item = None - else: - # text inside ellipse - self.text_item = QGraphicsSimpleTextItem(self) - self.text_item.setText(self.text) - # center ellipse around text - self.setRect( - 0, - 0, - self.text_item.boundingRect().width() * 2, - self.text_item.boundingRect().height() * 2 - ) - # center text in ellipse - self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) - - - # set anchor to the center - self.setTransform( - QTransform().translate(-self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) - - # cursor change on hover - self.setAcceptHoverEvents(True) - self.setZValue(1) - - # animation and moves - self.timeline = None - self.loading_timer = QTimer() - self.loading_timer.timeout.connect(self.next_tick) - self.loading_counter = 0 - self._refresh_colors() - self.setPos(center_pos) - self.move_to(nx_pos) - - def update_metadata(self, metadata): - super().update_metadata(metadata) - self.status_sentry = self.metadata['is_sentry'] if 'is_sentry' in self.metadata else False - self._refresh_colors() - - def _refresh_colors(self): - """ - Refresh elements in the node - """ - # color around ellipse - outline_color = QColor('grey') - outline_style = Qt.SolidLine - outline_width = 1 - if self.status_wallet: - outline_width = 2 - if not self.status_member: - outline_color = QColor('red') - - if self.status_sentry: - outline_color = QColor('black') - outline_width = 3 - - self.setPen(QPen(outline_color, outline_width, outline_style)) - - if self.highlighted: - text_color = QColor('grey') - else: - text_color = QColor('black') - - if self.status_wallet == NodeStatus.HIGHLIGHTED: - text_color = QColor('grey') - - if self.text_item: - self.text_item.setBrush(QBrush(text_color)) - - # create gradient inside the ellipse - gradient = QRadialGradient(QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) - color = QColor() - color.setHsv(120 - 60 / self.steps_max * self.steps, - 180 + 50 / self.steps_max * self.steps, - 60 + 170 / self.steps_max * self.steps) - if self.highlighted: - color = color.darker(200) - color = color.lighter(math.fabs(math.sin(self.loading_counter / 100 * math.pi) * 100) + 100) - gradient.setColorAt(0, color) - gradient.setColorAt(1, color.darker(150)) - self.setBrush(QBrush(gradient)) - - def move_to(self, nx_pos): - """ - Move to corresponding position - :param nx_pos: - :return: - """ - origin_x = self.x() - origin_y = self.y() - final_x = nx_pos[self.id][0] - final_y = nx_pos[self.id][1] - - def frame_move(frame): - value = self.timeline.valueForTime(self.timeline.currentTime()) - x = origin_x + (final_x - origin_x) * value - y = origin_y + (final_y - origin_y) * value - self.setPos(x, y) - if self.scene(): - self.scene().node_moved.emit(self.id, x, y) - - def timeline_ends(): - self.setPos(final_x, final_y) - self.timeline = None - - # Remember to hold the references to QTimeLine and QGraphicsItemAnimation instances. - # They are not kept anywhere, even if you invoke QTimeLine.start(). - self.timeline = QTimeLine(1000) - self.timeline.setFrameRange(0, 100) - self.timeline.frameChanged.connect(frame_move) - self.timeline.finished.connect(timeline_ends) - - self.timeline.start() - - def highlight(self): - """ - Highlight the edge in the scene - """ - self.highlighted = True - self._refresh_colors() - self.update(self.boundingRect()) - - def neutralize(self): - """ - Neutralize the edge in the scene - """ - self.highlighted = False - self._refresh_colors() - self.update(self.boundingRect()) - - def start_loading_animation(self): - """ - Neutralize the edge in the scene - """ - if not self.loading_timer.isActive(): - self.loading_timer.start(10) - - def stop_loading_animation(self): - """ - Neutralize the edge in the scene - """ - self.loading_timer.stop() - self.loading_counter = 100 - self._refresh_colors() - self.update(self.boundingRect()) - - def next_tick(self): - """ - Next tick - :return: - """ - self.loading_counter += 1 - self.loading_counter %= 100 - self._refresh_colors() - self.update(self.boundingRect()) - diff --git a/src/sakia/gui/views/scenes/__init__.py b/src/sakia/gui/views/scenes/__init__.py deleted file mode 100644 index f2eef13ee6a02e87b65491aa2d076785a7fc84d1..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/scenes/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .wot_scene import WotScene -from .explorer_scene import ExplorerScene \ No newline at end of file diff --git a/src/sakia/gui/views/scenes/explorer_scene.py b/src/sakia/gui/views/scenes/explorer_scene.py deleted file mode 100644 index 11b8511c8ecce401d6079dfc7275826ed0cb1b23..0000000000000000000000000000000000000000 --- a/src/sakia/gui/views/scenes/explorer_scene.py +++ /dev/null @@ -1,309 +0,0 @@ -import networkx -import logging -import math -from PyQt5.QtCore import QPoint, pyqtSignal -from PyQt5.QtWidgets import QGraphicsScene - -from ..edges import ExplorerEdge -from ..nodes import ExplorerNode - -from .base_scene import BaseScene - - -class ExplorerScene(BaseScene): - - node_moved = pyqtSignal(str, float, float) - - def __init__(self, parent=None): - """ - Create scene of the graph - - :param parent: [Optional, default=None] Parent view - """ - super().__init__(parent) - - self.lastDragPos = QPoint() - self.setItemIndexMethod(QGraphicsScene.NoIndex) - - # list of nodes in scene - self.nodes = dict() - # axis of the scene for debug purpose - # self.addLine(-100, 0, 100, 0) - # self.addLine(0, -100, 0, 100) - self.node_hovered.connect(self.display_path_to) - - # list of nodes in scene - self.nodes = dict() - self.edges = dict() - self.busy = None - self.nx_graph = None - self.identity = None - # axis of the scene for debug purpose - # self.addLine(-100, 0, 100, 0) - # self.addLine(0, -100, 0, 100) - - @staticmethod - def _init_layout(nx_graph): - """ - Init the data of the layout - :param MultiGraph nx_graph: - """ - data = {} - INF = len(nx_graph.nodes()) * len(nx_graph.nodes()) - - for node in nx_graph.nodes(): - data[node] = { - 'theta': None, - 'scenter': INF, - 'nchild': 0, - 'sparent': None, - 'stsize': 0.0, - 'span': 0.0 - } - return data - - @staticmethod - def _set_nstep_to_center(nx_graph, data, current): - """ - Set the number of steps to the center - :param networkx.MultiGraph nx_graph: the graph - :param dict data: the data of the layout - """ - queue = [current] - while queue: - n = queue.pop() - nsteps = data[n]['scenter'] + 1 - for edge in networkx.edges(nx_graph.to_undirected(), n): - next_node = edge[0] if edge[0] is not n else edge[1] - if data[next_node]['sparent']: - continue - if nsteps < data[next_node]['scenter']: - data[next_node]['scenter'] = nsteps - data[next_node]['sparent'] = n - data[n]['nchild'] += 1 - queue.append(next_node) - - @staticmethod - def _set_parent_nodes(nx_graph, data, center): - """ - Set the parent of each node - :param networkx.MultiGraph nx_graph: the graph - :param dict data: the data of the layout - :param str center: the id of the node at the center - """ - unset = data[center]['scenter'] - data[center]['scenter'] = 0 - data[center]['sparent'] = None - - logging.debug("Parent node of {0}".format(center)) - ExplorerScene._set_nstep_to_center(nx_graph, data, center) - for node in nx_graph.nodes(): - if data[node]['scenter'] == unset: - return -1 - return max([n['scenter'] for n in data.values()]) - - @staticmethod - def _set_subtree_size(nx_graph, data): - """ - Compute the subtree size of each node, which is the - number of leaves in subtree rooted to the node - :param networkx.MultiGraph nx_graph: the graph - :param dict data: - """ - for node in nx_graph.nodes(): - if data[node]['nchild'] > 0: - continue - data[node]['stsize'] += 1 - parent = data[node]['sparent'] - while parent: - data[parent]['stsize'] += 1 - parent = data[parent]['sparent'] - - @staticmethod - def _set_subtree_spans(nx_graph, data, current): - """ - Compute the subtree spans of each node - :param networkx.MultiGraph nx_graph: the graph - :param dict data: the data of the layout - :param str current: the current node which we compute the subtree - """ - ratio = data[current]['span'] / data[current]['stsize'] - for edge in nx_graph.to_undirected().edges(current): - next_node = edge[0] if edge[0] != current else edge[1] - if data[next_node]['sparent'] != current: - continue - if data[next_node]['span'] != 0.0: - continue - - data[next_node]['span'] = ratio * data[next_node]['stsize'] - if data[next_node]['nchild'] > 0: - ExplorerScene._set_subtree_spans(nx_graph, data, next_node) - - @staticmethod - def _set_positions(nx_graph, data, current): - """ - Compute the polar positions of each node - :param networkx.MultiDiGraph nx_graph: the graph - :param dict data: the data of the layout - :param str current: the current node which we compute the subtree - """ - if not data[current]['sparent']: - theta = 0 - else: - theta = data[current]['theta'] - data[current]['span'] / 2 - - for edge in nx_graph.to_undirected().edges(current): - next_node = edge[0] if edge[0] != current else edge[1] - if data[next_node]['sparent'] != current: - continue - if data[next_node]['theta']: - continue - - data[next_node]['theta'] = theta + data[next_node]['span'] / 2.0 - theta += data[next_node]['span'] - if data[next_node]['nchild'] > 0: - ExplorerScene._set_positions(nx_graph, data, next_node) - - @staticmethod - def twopi_layout(nx_graph, center=None): - """ - Render the twopi layout. Ported from C code available at - https://github.com/ellson/graphviz/blob/master/lib/twopigen/circle.c - - :param networkx.MultiDiGraph nx_graph: the networkx graph - :param str center: the centered node - :return: - """ - if len(nx_graph.nodes()) == 0: - return {} - - if len(nx_graph.nodes()) == 1: - return {nx_graph.nodes()[0]: (0, 0)} - #nx_graph = nx_graph.to_undirected() - - data = ExplorerScene._init_layout(nx_graph) - if not center: - center = networkx.center(nx_graph)[0] - ExplorerScene._set_parent_nodes(nx_graph, data, center) - ExplorerScene._set_subtree_size(nx_graph, data) - data[center]['span'] = 2 * math.pi - ExplorerScene._set_subtree_spans(nx_graph, data, center) - data[center]['theta'] = 0.0 - ExplorerScene._set_positions(nx_graph, data, center) - - distances = networkx.shortest_path_length(nx_graph.to_undirected(), center) - nx_pos = {} - for node in nx_graph.nodes(): - hyp = distances[node] + 1 - theta = data[node]['theta'] - nx_pos[node] = (hyp * math.cos(theta) * 100, hyp * math.sin(theta) * 100) - return nx_pos - - def clear(self): - """ - clear the scene - """ - for node in self.nodes.values(): - if node.timeline: - node.timeline.stop() - - self.nodes.clear() - self.edges.clear() - super().clear() - - def update_current_identity(self, identity_pubkey): - """ - Update the current identity loaded - - :param str identity_pubkey: - """ - for node in self.nodes.values(): - node.stop_loading_animation() - - if identity_pubkey in self.nodes: - self.nodes[identity_pubkey].start_loading_animation() - - def update_wot(self, nx_graph, identity, dist_max): - """ - draw community graph - - :param networkx.Graph nx_graph: graph to draw - :param sakia.core.registry.Identity identity: the wot of the identity - :param dist_max: the dist_max to display - """ - # clear scene - self.identity = identity - self.nx_graph = nx_graph.copy() - - graph_pos = ExplorerScene.twopi_layout(nx_graph, center=identity.pubkey) - if len(nx_graph.nodes()) > 0: - distances = networkx.shortest_path_length(nx_graph.to_undirected(), identity.pubkey) - else: - distances = {} - - # create networkx graph - for nx_node in nx_graph.nodes(data=True): - if nx_node[0] in self.nodes: - v = self.nodes[nx_node[0]] - v.move_to(graph_pos) - v.update_metadata(nx_node[1]) - else: - center_pos = None - if len(nx_graph.edges(nx_node[0])) > 0: - for edge in nx_graph.edges(nx_node[0]): - neighbour = edge[0] if edge[0] != nx_node[0] else edge[1] - if neighbour in self.nodes: - center_pos = self.nodes[neighbour].pos() - break - if not center_pos: - if identity.pubkey in self.nodes: - center_pos = self.nodes[identity.pubkey].pos() - else: - center_pos = QPoint(0, 0) - - small = distances[nx_node[0]] > 1 - - v = ExplorerNode(nx_node, center_pos, graph_pos, distances[nx_node[0]], dist_max, small) - self.addItem(v) - self.nodes[nx_node[0]] = v - - for edge in nx_graph.edges(data=True): - edge[2]["confirmation_text"] = "" - if (edge[0], edge[1]) not in self.edges: - distance = max(self.nodes[edge[0]].steps, self.nodes[edge[1]].steps) - explorer_edge = ExplorerEdge(edge[0], edge[1], edge[2], graph_pos, distance, dist_max) - self.node_moved.connect(explorer_edge.move_source_point) - self.node_moved.connect(explorer_edge.move_destination_point) - self.addItem(explorer_edge) - self.edges[(edge[0], edge[1])] = explorer_edge - - self.update() - - def display_path_to(self, node_id): - if node_id != self.identity.pubkey: - for edge in self.edges.values(): - edge.neutralize() - - for node in self.nodes.values(): - node.neutralize() - - path = [] - try: - path = networkx.shortest_path(self.nx_graph, node_id, self.identity.pubkey) - except (networkx.exception.NetworkXError, networkx.exception.NetworkXNoPath) as e: - logging.debug(str(e)) - try: - path = networkx.shortest_path(self.nx_graph, self.identity.pubkey, node_id) - except (networkx.exception.NetworkXError, networkx.exception.NetworkXNoPath) as e: - logging.debug(str(e)) - - for node, next_node in zip(path[:-1], path[1:]): - if (node, next_node) in self.edges: - edge = self.edges[(node, next_node)] - elif (next_node, node) in self.edges: - edge = self.edges[(next_node, node)] - if edge: - edge.highlight() - self.nodes[node].highlight() - self.nodes[next_node].highlight() - logging.debug("Update edge between {0} and {1}".format(node, next_node)) diff --git a/src/sakia/gui/widgets/context_menu.py b/src/sakia/gui/widgets/context_menu.py index 60e7b4a14ca899b66231c30894fe693978ee6b76..9a869bcf824345a04a08607a98c4e270822e0304 100644 --- a/src/sakia/gui/widgets/context_menu.py +++ b/src/sakia/gui/widgets/context_menu.py @@ -1,35 +1,31 @@ -from PyQt5.QtWidgets import QMenu, QAction, QApplication, QMessageBox -from PyQt5.QtCore import QObject, pyqtSignal -from duniterpy.documents import Block, Membership import logging -from ..member import MemberDialog -from ..contact import ConfigureContactDialog -from ..transfer import TransferMoneyDialog -from ..certification import CertificationDialog -from ...tools.decorators import asyncify -from ...core.transfer import Transfer, TransferState -from ...core.registry import Identity -from ...tools.exceptions import MembershipNotFoundError +from PyQt5.QtCore import QObject, pyqtSignal +from PyQt5.QtWidgets import QMenu, QAction, QApplication, QMessageBox + +from duniterpy.documents import Block +from sakia.data.entities import Identity, Transaction +from sakia.data.processors import BlockchainProcessor, TransactionsProcessor +from sakia.decorators import asyncify +from sakia.gui.dialogs.certification.controller import CertificationController +from sakia.gui.dialogs.transfer.controller import TransferController +from sakia.gui.sub.user_information.controller import UserInformationController class ContextMenu(QObject): view_identity_in_wot = pyqtSignal(object) + identity_information_loaded = pyqtSignal(Identity) - def __init__(self, qmenu, app, account, community, password_asker): + def __init__(self, qmenu, app, connection): """ :param PyQt5.QtWidgets.QMenu: the qmenu widget - :param sakia.core.Application app: Application instance - :param sakia.core.Account account: The current account instance - :param sakia.core.Community community: The community instance - :param sakia.gui.PasswordAsker password_asker: The password dialog + :param sakia.app.Application app: Application instance + :param sakia.data.entities.Connection connection: The current connection instance """ super().__init__() self.qmenu = qmenu self._app = app - self._community = community - self._account = account - self._password_asker = password_asker + self._connection = connection @staticmethod def _add_identity_actions(menu, identity): @@ -37,40 +33,31 @@ class ContextMenu(QObject): :param ContextMenu menu: the qmenu to add actions to :param Identity identity: the identity """ - menu.qmenu.addSeparator().setText(identity.uid) + menu.qmenu.addSeparator().setText(identity.uid if identity.uid else "Pubkey") informations = QAction(menu.qmenu.tr("Informations"), menu.qmenu.parent()) informations.triggered.connect(lambda checked, i=identity: menu.informations(i)) menu.qmenu.addAction(informations) - if menu._account.pubkey != identity.pubkey: - add_as_contact = QAction(menu.qmenu.tr("Add as contact"), menu.qmenu.parent()) - add_as_contact.triggered.connect(lambda checked, i=identity: menu.add_as_contact(i)) - menu.qmenu.addAction(add_as_contact) - - if menu._account.pubkey != identity.pubkey: + if menu._connection.pubkey != identity.pubkey: send_money = QAction(menu.qmenu.tr("Send money"), menu.qmenu.parent()) send_money.triggered.connect(lambda checked, i=identity: menu.send_money(i)) menu.qmenu.addAction(send_money) - if menu._account.pubkey != identity.pubkey: + if identity.uid and menu._connection.pubkey != identity.pubkey: certify = QAction(menu.tr("Certify identity"), menu.qmenu.parent()) certify.triggered.connect(lambda checked, i=identity: menu.certify_identity(i)) menu.qmenu.addAction(certify) - view_wot = QAction(menu.qmenu.tr("View in Web of Trust"), menu.qmenu.parent()) - view_wot.triggered.connect(lambda checked, i=identity: menu.view_wot(i)) - menu.qmenu.addAction(view_wot) + view_wot = QAction(menu.qmenu.tr("View in Web of Trust"), menu.qmenu.parent()) + view_wot.triggered.connect(lambda checked, i=identity: menu.view_wot(i)) + menu.qmenu.addAction(view_wot) copy_pubkey = QAction(menu.qmenu.tr("Copy pubkey to clipboard"), menu.qmenu.parent()) copy_pubkey.triggered.connect(lambda checked, i=identity: ContextMenu.copy_pubkey_to_clipboard(i)) menu.qmenu.addAction(copy_pubkey) - if menu._app.preferences['expert_mode']: - copy_membership = QAction(menu.qmenu.tr("Copy membership document to clipboard"), menu.qmenu.parent()) - copy_membership.triggered.connect(lambda checked, i=identity: menu.copy_membership_to_clipboard(i)) - menu.qmenu.addAction(copy_membership) - + if identity.uid and menu._app.parameters.expert_mode: copy_selfcert = QAction(menu.qmenu.tr("Copy self-certification document to clipboard"), menu.qmenu.parent()) copy_selfcert.triggered.connect(lambda checked, i=identity: menu.copy_selfcert_to_clipboard(i)) menu.qmenu.addAction(copy_selfcert) @@ -82,7 +69,7 @@ class ContextMenu(QObject): :param Transfer transfer: the transfer """ menu.qmenu.addSeparator().setText(menu.qmenu.tr("Transfer")) - if transfer.state in (TransferState.REFUSED, TransferState.TO_SEND): + if transfer.state in (Transaction.REFUSED, Transaction.TO_SEND): send_back = QAction(menu.qmenu.tr("Send again"), menu.qmenu.parent()) send_back.triggered.connect(lambda checked, tr=transfer: menu.send_again(tr)) menu.qmenu.addAction(send_back) @@ -91,38 +78,35 @@ class ContextMenu(QObject): cancel.triggered.connect(lambda checked, tr=transfer: menu.cancel_transfer(tr)) menu.qmenu.addAction(cancel) - if menu._app.preferences['expert_mode']: + if menu._app.parameters.expert_mode: copy_doc = QAction(menu.qmenu.tr("Copy raw transaction to clipboard"), menu.qmenu.parent()) copy_doc.triggered.connect(lambda checked, tx=transfer: menu.copy_transaction_to_clipboard(tx)) menu.qmenu.addAction(copy_doc) - if transfer.blockUID: + if transfer.blockstamp: copy_doc = QAction(menu.qmenu.tr("Copy transaction block to clipboard"), menu.qmenu.parent()) - copy_doc.triggered.connect(lambda checked, number=transfer.blockUID.number: - menu.copy_block_to_clipboard(number)) + copy_doc.triggered.connect(lambda checked, number=transfer.blockstamp.number: + menu.copy_block_to_clipboard(transfer.blockstamp.number)) menu.qmenu.addAction(copy_doc) - @classmethod - def from_data(cls, parent, app, account, community, password_asker, data): + def from_data(cls, parent, app, connection, data): """ Builds a QMenu from data passed as parameters Data can be Identity or Transfer :param PyQt5.QtWidgets.QWidget parent: the parent widget - :param sakia.core.Application app: the application - :param sakia.core.Application app: Application instance - :param sakia.core.Account account: The current account instance - :param sakia.core.Community community: The community instance - :param sakia.gui.PasswordAsker password_asker: The password dialog + :param sakia.app.Application app: Application instance + :param sakia.data.entities.Connection connection: the current connection :param tuple data: a tuple of data to add to the menu :rtype: ContextMenu """ - menu = cls(QMenu(parent), app, account, community, password_asker) + menu = cls(QMenu(parent), app, connection) build_actions = { Identity: ContextMenu._add_identity_actions, - Transfer: ContextMenu._add_transfers_actions, - dict: lambda m, d: None + Transaction: ContextMenu._add_transfers_actions, + dict: lambda m, d: None, + type(None): lambda m, d: None } for d in data: build_actions[type(d)](menu, d) @@ -135,31 +119,27 @@ class ContextMenu(QObject): clipboard.setText(identity.pubkey) def informations(self, identity): - MemberDialog.open_dialog(self._app, self._account, self._community, identity) + if identity.uid: + UserInformationController.show_identity(self.parent(), self._app, self._connection.currency, identity) + self.identity_information_loaded.emit(identity) + else: + UserInformationController.search_and_show_pubkey(self.parent(), self._app, self._connection.currency, + identity.pubkey) - def add_as_contact(self, identity): - dialog = ConfigureContactDialog.from_identity(self._app, self.parent(), self._account, identity) - dialog.exec_() @asyncify async def send_money(self, identity): - await TransferMoneyDialog.send_money_to_identity(self._app, self._account, self._password_asker, - self._community, identity) - self._app.refresh_transfers.emit() + await TransferController.send_money_to_identity(None, self._app, self._connection, identity) def view_wot(self, identity): self.view_identity_in_wot.emit(identity) @asyncify async def certify_identity(self, identity): - await CertificationDialog.certify_identity(self._app, self._account, self._password_asker, - self._community, identity) + await CertificationController.certify_identity(None, self._app, self._connection, identity) - @asyncify - async def send_again(self, transfer): - await TransferMoneyDialog.send_transfer_again(self._app, self._app.current_account, - self._password_asker, self._community, transfer) - self._app.refresh_transfers.emit() + def send_again(self, transfer): + TransferController.send_transfer_again(None, self._app, self._connection, transfer) def cancel_transfer(self, transfer): reply = QMessageBox.warning(self.qmenu, self.tr("Warning"), @@ -167,43 +147,21 @@ class ContextMenu(QObject): This money transfer will be removed and not sent."""), QMessageBox.Ok | QMessageBox.Cancel) if reply == QMessageBox.Ok: - transfer.cancel() - self._app.refresh_transfers.emit() + transactions_processor = TransactionsProcessor.instanciate(self._app) + transactions_processor.cancel(transfer) + self._app.db.commit() + self._app.transaction_state_changed.emit(transfer) - @asyncify - async def copy_transaction_to_clipboard(self, tx): + def copy_transaction_to_clipboard(self, tx): clipboard = QApplication.clipboard() - raw_doc = await tx.get_raw_document(self._community) - if raw_doc: - clipboard.setText(raw_doc.signed_raw()) + clipboard.setText(tx.raw) @asyncify async def copy_block_to_clipboard(self, number): clipboard = QApplication.clipboard() - block = await self._community.get_block(number) - if block: - block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) - clipboard.setText(block_doc.signed_raw()) - - @asyncify - async def copy_membership_to_clipboard(self, identity): - """ - - :param sakia.core.registry.Identity identity: - :return: - """ - clipboard = QApplication.clipboard() - try: - membership = await identity.membership(self._community) - if membership: - block_number = membership['written'] - block = await self._community.get_block(block_number) - block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) - for ms_doc in block_doc.joiners: - if ms_doc.issuer == identity.pubkey: - clipboard.setText(ms_doc.signed_raw()) - except MembershipNotFoundError: - logging.debug("Could not find membership") + blockchain_processor = BlockchainProcessor.instanciate(self._app) + block_doc = await blockchain_processor.get_block(self._connection.currency, number) + clipboard.setText(block_doc.signed_raw()) @asyncify async def copy_selfcert_to_clipboard(self, identity): diff --git a/src/sakia/gui/widgets/search_user.py b/src/sakia/gui/widgets/search_user.py deleted file mode 100644 index 6f4a0195e13294bdb5ca850ec9ee2667a8df15d3..0000000000000000000000000000000000000000 --- a/src/sakia/gui/widgets/search_user.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging - -from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP, Qt -from PyQt5.QtWidgets import QComboBox, QWidget - -from duniterpy.api import bma, errors - -from ...tools.decorators import asyncify -from ...tools.exceptions import NoPeerAvailable -from ...core.registry import BlockchainState, Identity -from ...gen_resources.search_user_view_uic import Ui_SearchUserWidget - - -class SearchUserWidget(QWidget, Ui_SearchUserWidget): - _search_placeholder = QT_TRANSLATE_NOOP("SearchUserWidget", "Research a pubkey, an uid...") - - identity_selected = pyqtSignal(Identity) - search_started = pyqtSignal() - search_completed = pyqtSignal() - reset = pyqtSignal() - - def __init__(self, parent): - """ - :param sakia.core.app.Application app: Application instance - """ - # construct from qtDesigner - super().__init__(parent) - self.setupUi(self) - # Default text when combo lineEdit is empty - self.combobox_search.lineEdit().setPlaceholderText(self.tr(SearchUserWidget._search_placeholder)) - #  add combobox events - self.combobox_search.lineEdit().returnPressed.connect(self.search) - # To fix a recall of the same item with different case, - # the edited text is not added in the item list - self.combobox_search.setInsertPolicy(QComboBox.NoInsert) - self.combobox_search.activated.connect(self.select_node) - self.button_reset.clicked.connect(self.reset) - self.nodes = list() - self.community = None - self.account = None - self.app = None - self._current_identity = None - - def current_identity(self): - return self._current_identity - - def init(self, app): - """ - Initialize the widget - :param sakia.core.Application app: the application - """ - self.app = app - - def change_account(self, account): - self.account = account - - def change_community(self, community): - self.community = community - - @asyncify - async def search(self): - """ - Search nodes when return is pressed in combobox lineEdit - """ - self.search_started.emit() - text = self.combobox_search.lineEdit().text() - self.combobox_search.lineEdit().clear() - self.combobox_search.lineEdit().setPlaceholderText(self.tr("Looking for {0}...".format(text))) - - if len(text) > 2: - try: - response = await self.community.bma_access.future_request(bma.wot.Lookup, {'search': text}) - - nodes = {} - for identity in response['results']: - nodes[identity['pubkey']] = identity['uids'][0]['uid'] - - if nodes: - self.nodes = list() - self.blockSignals(True) - self.combobox_search.clear() - self.combobox_search.lineEdit().setText(text) - for pubkey, uid in nodes.items(): - self.nodes.append({'pubkey': pubkey, 'uid': uid}) - self.combobox_search.addItem(uid) - self.blockSignals(False) - self.combobox_search.showPopup() - except errors.DuniterError as e: - if e.ucode == errors.NO_MATCHING_IDENTITY: - self.nodes = list() - self.blockSignals(True) - self.combobox_search.clear() - self.blockSignals(False) - self.combobox_search.showPopup() - else: - pass - except NoPeerAvailable: - pass - self.search_completed.emit() - - def select_node(self, index): - """ - Select node in graph when item is selected in combobox - """ - if index < 0 or index >= len(self.nodes): - self._current_identity = None - return False - node = self.nodes[index] - metadata = {'id': node['pubkey'], 'text': node['uid']} - self._current_identity = self.app.identities_registry.from_handled_data( - metadata['text'], - metadata['id'], - None, - BlockchainState.VALIDATED, - self.community - ) - self.identity_selected.emit( - self._current_identity - ) - - def retranslateUi(self, widget): - """ - Retranslate missing widgets from generated code - """ - self.combobox_search.lineEdit().setPlaceholderText(self.tr(SearchUserWidget._search_placeholder)) - super().retranslateUi(self) - - def keyPressEvent(self, event): - if event.key() == Qt.Key_Return: - return - - super().keyPressEvent(event) diff --git a/src/sakia/gui/widgets/toast.py b/src/sakia/gui/widgets/toast.py index fd50cf8f8edcb900778f0b2f87c776ba1d4cad78..afd7d0aef22b31160b990cc21c4a229b49460b26 100644 --- a/src/sakia/gui/widgets/toast.py +++ b/src/sakia/gui/widgets/toast.py @@ -8,7 +8,7 @@ import logging from PyQt5.QtCore import Qt, QThread from PyQt5.QtWidgets import QMainWindow, QApplication from PyQt5.QtGui import QImage, QPixmap -from ...gen_resources.toast_uic import Ui_Toast +from .toast_uic import Ui_Toast window = None # global diff --git a/res/ui/toast.ui b/src/sakia/gui/widgets/toast.ui similarity index 100% rename from res/ui/toast.ui rename to src/sakia/gui/widgets/toast.ui diff --git a/src/sakia/main.py b/src/sakia/main.py index 6e55dbf2bd773098c02a18ba0839864ca7a079b8..859d0edeb727180ef814d853a8eef656070f6251 100755 --- a/src/sakia/main.py +++ b/src/sakia/main.py @@ -3,25 +3,19 @@ Created on 1 févr. 2014 @author: inso """ -import signal -import sys import asyncio import logging -import os -import traceback - -# To debug missing spec -import jsonschema +import signal +import sys import traceback -# To force cx_freeze import -import PyQt5.QtSvg +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication, QMessageBox from quamash import QSelectorEventLoop -from PyQt5.QtWidgets import QApplication, QMessageBox -from PyQt5.QtCore import Qt -from sakia.gui.mainwindow import MainWindow -from sakia.core.app import Application +from sakia.app import Application +from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController +from sakia.gui.main_window.controller import MainWindowController def async_exception_handler(loop, context): @@ -97,15 +91,19 @@ if __name__ == '__main__': with loop: app = Application.startup(sys.argv, sakia, loop) - window = MainWindow.startup(app) + if not app.connection_exists(): + conn_controller = ConnectionConfigController.create_connection(None, app) + loop.run_until_complete(conn_controller.async_exec()) + app.instanciate_services() + app.start_coroutines() + window = MainWindowController.startup(app) loop.run_forever() try: loop.set_exception_handler(None) - loop.run_until_complete(app.stop()) + loop.run_until_complete(app.stop_current_profile()) logging.debug("Application stopped") except asyncio.CancelledError: logging.info('CancelledError') logging.debug("Exiting") sys.exit() - logging.debug("Application stopped") diff --git a/src/sakia/models/certifications.py b/src/sakia/models/certifications.py deleted file mode 100644 index 72bbf69717f6ff0dbdb11eedaebd34660beab5c9..0000000000000000000000000000000000000000 --- a/src/sakia/models/certifications.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Created on 5 févr. 2014 - -@author: inso -""" - -import datetime -import logging -import asyncio -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ - QDateTime, QLocale, QModelIndex - -from PyQt5.QtGui import QFont, QColor - - -class CertsFilterProxyModel(QSortFilterProxyModel): - def __init__(self, ts_from, ts_to, parent=None): - super().__init__(parent) - self.app = None - self.ts_from = ts_from - self.ts_to = ts_to - - @property - def account(self): - return self.app.current_account - - def set_period(self, ts_from, ts_to): - """ - Filter table by given timestamps - """ - logging.debug("Filtering from {0} to {1}".format( - datetime.datetime.fromtimestamp(ts_from).isoformat(' '), - datetime.datetime.fromtimestamp(ts_to).isoformat(' ')) - ) - self.ts_from = ts_from - self.ts_to = ts_to - self.modelReset.emit() - - def filterAcceptsRow(self, sourceRow, sourceParent): - def in_period(date_ts): - return date_ts in range(self.ts_from, self.ts_to) - - source_model = self.sourceModel() - date_col = source_model.columns_types.index('date') - source_index = source_model.index(sourceRow, date_col) - date = source_model.data(source_index, Qt.DisplayRole) - return in_period(date) - - @property - def community(self): - return self.sourceModel().community - - def columnCount(self, parent): - return self.sourceModel().columnCount(None) - 5 - - def setSourceModel(self, sourceModel): - self.app = sourceModel.app - super().setSourceModel(sourceModel) - - def lessThan(self, left, right): - """ - Sort table by given column number. - """ - source_model = self.sourceModel() - left_data = source_model.data(left, Qt.DisplayRole) - right_data = source_model.data(right, Qt.DisplayRole) - if left_data == "": - return self.sortOrder() == Qt.DescendingOrder - elif right_data == "": - return self.sortOrder() == Qt.AscendingOrder - return (left_data < right_data) - - def data(self, index, role): - source_index = self.mapToSource(index) - model = self.sourceModel() - source_data = model.data(source_index, role) - state_col = model.columns_types.index('state') - state_index = model.index(source_index.row(), state_col) - state_data = model.data(state_index, Qt.DisplayRole) - if role == Qt.DisplayRole: - if source_index.column() == model.columns_types.index('uid'): - return source_data - if source_index.column() == model.columns_types.index('date'): - return QLocale.toString( - QLocale(), - QDateTime.fromTime_t(source_data).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ) - if source_index.column() == model.columns_types.index('payment') or \ - source_index.column() == model.columns_types.index('deposit'): - return source_data - - if role == Qt.FontRole: - font = QFont() - return font - - if role == Qt.ForegroundRole: - if state_data == TransferState.REFUSED: - return QColor(Qt.red) - elif state_data == TransferState.TO_SEND: - return QColor(Qt.blue) - - if role == Qt.TextAlignmentRole: - if source_index.column() == self.sourceModel().columns_types.index('date'): - return Qt.AlignCenter - - if role == Qt.ToolTipRole: - if source_index.column() == self.sourceModel().columns_types.index('date'): - return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate) - return None - - return source_data - - -class HistoryTableModel(QAbstractTableModel): - """ - A Qt abstract item model to display communities in a tree - """ - - def __init__(self, app, account, community, parent=None): - """ - Constructor - """ - super().__init__(parent) - self.app = app - self.account = account - self.community = community - self.transfers_data = [] - self.refresh_certs() - self._max_confirmations = 0 - - self.columns_types = ( - 'date', - 'uid', - 'state', - 'pubkey', - 'block_number' - ) - - self.column_headers = ( - self.tr('Date'), - self.tr('UID/Public key'), - 'State', - 'Pubkey', - 'Block Number' - ) - - def change_account(self, account): - cancel_once_task(self, self.refresh_certs) - self.account = account - - def change_community(self, community): - cancel_once_task(self, self.refresh_certs) - self.community = community - - def certifications(self): - if self.account: - return self.account.certifications(self.community) - else: - return [] - - @once_at_a_time - @asyncify - async def refresh_certs(self): - self.beginResetModel() - self.transfers_data = [] - self.endResetModel() - - def max_confirmations(self): - return self._max_confirmations - - def rowCount(self, parent): - return len(self.transfers_data) - - def columnCount(self, parent): - return len(self.columns_types) - - def headerData(self, section, orientation, role): - if self.account and self.community: - if role == Qt.DisplayRole: - return self.column_headers[section] - - def data(self, index, role): - row = index.row() - col = index.column() - - if not index.isValid(): - return QVariant() - - if role == Qt.DisplayRole: - return self.transfers_data[row][col] - - if role == Qt.ToolTipRole: - return self.transfers_data[row][col] - - def flags(self, index): - return Qt.ItemIsSelectable | Qt.ItemIsEnabled - diff --git a/src/sakia/models/communities.py b/src/sakia/models/communities.py deleted file mode 100644 index d782ad461a4bcf380af6bbb6dbff24a3f624089d..0000000000000000000000000000000000000000 --- a/src/sakia/models/communities.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Created on 5 févr. 2014 - -@author: inso -""" - -from PyQt5.QtCore import QAbstractListModel, Qt - - -class CommunitiesListModel(QAbstractListModel): - - """ - A Qt abstract item model to display communities in a tree - """ - - def __init__(self, account, parent=None): - """ - Constructor - """ - super(CommunitiesListModel, self).__init__(parent) - self.communities = account.communities - - def rowCount(self, parent): - return len(self.communities) - - def data(self, index, role): - - if role == Qt.DisplayRole: - row = index.row() - value = self.communities[row].name - return value - - def flags(self, index): - return Qt.ItemIsSelectable | Qt.ItemIsEnabled diff --git a/src/sakia/models/community.py b/src/sakia/models/community.py deleted file mode 100644 index 16babb17f50b415a760af1d68a148f18bcd8ab4d..0000000000000000000000000000000000000000 --- a/src/sakia/models/community.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Created on 5 fevr. 2014 - -@author: inso -""" - - -class CommunityItemModel(object): - - def __init__(self, community, communities_item=None): - self.communities_item = communities_item - self.community_text = community.name - self.main_node_items = [] - - def appendChild(self, item): - self.main_node_items.append(item) - - def child(self, row): - return self.main_node_items[row] - - def childCount(self): - return len(self.main_node_items) - - def columnCount(self): - return 1 - - def data(self, column): - try: - return self.community_text - except IndexError: - return None - - def parent(self): - return self.communities_item - - def row(self): - if self.communities_item: - return self.communities_item.index(self) - - return 0 diff --git a/src/sakia/models/generic_tree.py b/src/sakia/models/generic_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..0b7eb55b785dfe8df03783a75ed3e97278cae3c5 --- /dev/null +++ b/src/sakia/models/generic_tree.py @@ -0,0 +1,172 @@ +""" +Created on 5 févr. 2014 + +@author: inso +""" + +from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt +from PyQt5.QtGui import QIcon +import logging + + +def parse_node(node_data, parent_item): + node = NodeItem(node_data, parent_item) + if parent_item: + parent_item.appendChild(node) + if 'children' in node_data: + for child in node_data['children']: + parse_node(child, node) + return node + + +class NodeItem(QAbstractItemModel): + def __init__(self, node, parent_item): + super().__init__(parent_item) + self.children = [] + self.node = node + self.parent_item = parent_item + + def appendChild(self, node_item): + self.children.append(node_item) + + def child(self, row): + return self.children[row] + + def childCount(self): + return len(self.children) + + def columnCount(self): + return 1 + + def data(self, index, role): + if role == Qt.DisplayRole and 'title' in self.node: + return self.node['title'] + + if role == Qt.ToolTipRole and 'tooltip' in self.node: + return self.node['tooltip'] + + if role == Qt.DecorationRole and 'icon' in self.node: + return QIcon(self.node['icon']) + + if role == GenericTreeModel.ROLE_RAW_DATA: + return self.node + + def row(self): + if self.parent_item: + return self.parent_item.row() + self.parent_item.children.index(self) + return 0 + + def column(self): + return 0 + + +class GenericTreeModel(QAbstractItemModel): + + """ + A Qt abstract item model to display nodes from a dict + + + dict_format = { + 'root_node': { + 'node': ["title", "icon", "tooltip", "action"], + 'children': {} + } + } + + """ + + ROLE_RAW_DATA = 101 + + def __init__(self, title, root_item): + """ + Constructor + """ + super().__init__(None) + self.title = title + self.root_item = root_item + + @classmethod + def create(cls, title, data): + root_item = NodeItem({}, None) + for node in data: + parse_node(node, root_item) + + return cls(title, root_item) + + def columnCount(self, parent): + return 1 + + def data(self, index, role): + if not index.isValid(): + return None + + item = index.internalPointer() + + if role in (Qt.DisplayRole, + Qt.DecorationRole, + Qt.ToolTipRole, + GenericTreeModel.ROLE_RAW_DATA) \ + and index.column() == 0: + return item.data(0, role) + + return None + + def flags(self, index): + if not index.isValid(): + return Qt.NoItemFlags + + if index.column() == 0: + return Qt.ItemIsEnabled | Qt.ItemIsSelectable + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal \ + and role == Qt.DisplayRole and section == 0: + return self.title + return None + + def index(self, row, column, parent): + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if not parent.isValid(): + parent_item = self.root_item + else: + parent_item = parent.internalPointer() + + child_item = parent_item.child(row) + if child_item: + return self.createIndex(row, column, child_item) + else: + return QModelIndex() + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + + child_item = index.internalPointer() + parent_item = child_item.parent() + + if parent_item == self.root_item: + return QModelIndex() + + return self.createIndex(parent_item.row(), 0, parent_item) + + def rowCount(self, parent_index): + if not parent_index.isValid(): + parent_item = self.root_item + else: + parent_item = parent_index.internalPointer() + + if parent_index.column() > 0: + return 0 + + return parent_item.childCount() + + def setData(self, index, value, role=Qt.EditRole): + if index.column() == 0: + return True + + def insert_node(self, raw_data): + self.beginInsertRows(QModelIndex(), self.rowCount(QModelIndex()), 0) + parse_node(raw_data, self.root_item) + self.endInsertRows() diff --git a/src/sakia/models/peering.py b/src/sakia/models/peering.py deleted file mode 100644 index 4b2e48c2b4a5a4c2f2041d5eb9836a81751d6b56..0000000000000000000000000000000000000000 --- a/src/sakia/models/peering.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -Created on 5 févr. 2014 - -@author: inso -""" - -from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt -import logging - - -class RootItem(object): - - def __init__(self, name): - self.name = name - self.node_items = [] - - def appendChild(self, item): - self.node_items.append(item) - - def child(self, row): - return self.node_items[row] - - def childCount(self): - return len(self.node_items) - - def columnCount(self): - return 1 - - def data(self, column): - try: - return self.name - except IndexError: - return None - - def parent(self): - return None - - def row(self): - return 0 - - -class NodeItem(object): - - def __init__(self, node, root_item): - e = node.endpoint - if e.server: - self.address = "{0}:{1}".format(e.server, e.port) - elif e.ipv4: - self.address = "{0}:{1}".format(e.ipv4, e.port) - elif e.ipv6: - self.address = "{0}:{1}".format(e.ipv6, e.port) - else: - self.address = "{0}".format(node.pubkey) - - self.root_item = root_item - self.node_items = [] - - def appendChild(self, node_item): - self.node_items.append(node_item) - - def child(self, row): - return self.node_items[row] - - def childCount(self): - return len(self.node_items) - - def columnCount(self): - return 1 - - def data(self, column): - try: - return self.address - except IndexError: - return None - - def parent(self): - return self.root_item - - def row(self): - if self.root_item: - return self.root_item.node_items.index(self) - return 0 - - -class PeeringTreeModel(QAbstractItemModel): - - """ - A Qt abstract item model to display nodes of a community - """ - - def __init__(self, community): - """ - Constructor - """ - super().__init__(None) - self.nodes = community._network.root_nodes - self.root_item = RootItem(community.currency) - self.refresh_tree() - - def columnCount(self, parent): - return 1 - - def data(self, index, role): - if not index.isValid(): - return None - - item = index.internalPointer() - - if role == Qt.DisplayRole and index.column() == 0: - return item.data(0) - - return None - - def flags(self, index): - if not index.isValid(): - return Qt.NoItemFlags - - if index.column() == 0: - return Qt.ItemIsEnabled | Qt.ItemIsSelectable - - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal \ - and role == Qt.DisplayRole and section == 0: - return self.root_item.data(0) + " nodes" - return None - - def index(self, row, column, parent): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - if not parent.isValid(): - parent_item = self.root_item - else: - parent_item = parent.internalPointer() - - child_item = parent_item.child(row) - if child_item: - return self.createIndex(row, column, child_item) - else: - return QModelIndex() - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - - child_item = index.internalPointer() - parent_item = child_item.parent() - - if parent_item == self.root_item: - return QModelIndex() - - return self.createIndex(parent_item.row(), 0, parent_item) - - def rowCount(self, parent): - if parent.column() > 0: - return 0 - - if not parent.isValid(): - parent_item = self.root_item - else: - parent_item = parent.internalPointer() - - return parent_item.childCount() - - def setData(self, index, value, role=Qt.EditRole): - if index.column() == 0: - return True - - def refresh_tree(self): - logging.debug("root : " + self.root_item.data(0)) - for node in self.nodes: - node_item = NodeItem(node, self.root_item) - self.root_item.appendChild(node_item) diff --git a/src/sakia/models/txhistory.py b/src/sakia/models/txhistory.py deleted file mode 100644 index 5ea783e80ea738d7c5069a6faefd41b0b63ff15b..0000000000000000000000000000000000000000 --- a/src/sakia/models/txhistory.py +++ /dev/null @@ -1,386 +0,0 @@ -""" -Created on 5 févr. 2014 - -@author: inso -""" - -import datetime -import logging -import asyncio -import math -from ..core.transfer import Transfer, TransferState -from ..core.net.network import MAX_CONFIRMATIONS -from ..tools.exceptions import NoPeerAvailable -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ - QDateTime, QLocale, QModelIndex - -from PyQt5.QtGui import QFont, QColor, QIcon - - -class TxFilterProxyModel(QSortFilterProxyModel): - def __init__(self, ts_from, ts_to, parent=None): - super().__init__(parent) - self.app = None - self.ts_from = ts_from - self.ts_to = ts_to - self.payments = 0 - self.deposits = 0 - - @property - def account(self): - return self.app.current_account - - def set_period(self, ts_from, ts_to): - """ - Filter table by given timestamps - """ - logging.debug("Filtering from {0} to {1}".format( - datetime.datetime.fromtimestamp(ts_from).isoformat(' '), - datetime.datetime.fromtimestamp(ts_to).isoformat(' ')) - ) - self.ts_from = ts_from - self.ts_to = ts_to - self.modelReset.emit() - - def filterAcceptsRow(self, sourceRow, sourceParent): - def in_period(date_ts): - return date_ts in range(self.ts_from, self.ts_to) - - source_model = self.sourceModel() - date_col = source_model.columns_types.index('date') - source_index = source_model.index(sourceRow, date_col) - date = source_model.data(source_index, Qt.DisplayRole) - if in_period(date): - # calculate sum total payments - payment = source_model.data( - source_model.index(sourceRow, source_model.columns_types.index('amount')), - Qt.DisplayRole - ) - if payment: - self.payments += int(payment) - # calculate sum total deposits - deposit = source_model.data( - source_model.index(sourceRow, source_model.columns_types.index('amount')), - Qt.DisplayRole - ) - if deposit: - self.deposits += int(deposit) - - return in_period(date) - - @property - def community(self): - return self.sourceModel().community - - def columnCount(self, parent): - return self.sourceModel().columnCount(None) - 5 - - def setSourceModel(self, sourceModel): - self.app = sourceModel.app - super().setSourceModel(sourceModel) - - def lessThan(self, left, right): - """ - Sort table by given column number. - """ - source_model = self.sourceModel() - left_data = source_model.data(left, Qt.DisplayRole) - right_data = source_model.data(right, Qt.DisplayRole) - if left_data == "": - return self.sortOrder() == Qt.DescendingOrder - elif right_data == "": - return self.sortOrder() == Qt.AscendingOrder - if left_data == right_data: - txid_col = source_model.columns_types.index('txid') - txid_left = source_model.index(left.row(), txid_col) - txid_right = source_model.index(right.row(), txid_col) - return (txid_left < txid_right) - - return (left_data < right_data) - - def data(self, index, role): - source_index = self.mapToSource(index) - model = self.sourceModel() - source_data = model.data(source_index, role) - state_col = model.columns_types.index('state') - state_index = model.index(source_index.row(), state_col) - state_data = model.data(state_index, Qt.DisplayRole) - if role == Qt.DisplayRole: - if source_index.column() == model.columns_types.index('uid'): - return source_data - if source_index.column() == model.columns_types.index('date'): - return QLocale.toString( - QLocale(), - QDateTime.fromTime_t(source_data).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ) - if source_index.column() == model.columns_types.index('payment') or \ - source_index.column() == model.columns_types.index('deposit'): - return source_data - - if role == Qt.FontRole: - font = QFont() - if state_data == TransferState.AWAITING or state_data == TransferState.VALIDATING: - font.setItalic(True) - elif state_data == TransferState.REFUSED: - font.setItalic(True) - elif state_data == TransferState.TO_SEND: - font.setBold(True) - else: - font.setItalic(False) - return font - - if role == Qt.ForegroundRole: - if state_data == TransferState.REFUSED: - return QColor(Qt.red) - elif state_data == TransferState.TO_SEND: - return QColor(Qt.blue) - - if role == Qt.TextAlignmentRole: - if source_index.column() == self.sourceModel().columns_types.index( - 'deposit') or source_index.column() == self.sourceModel().columns_types.index('payment'): - return Qt.AlignRight | Qt.AlignVCenter - if source_index.column() == self.sourceModel().columns_types.index('date'): - return Qt.AlignCenter - - if role == Qt.ToolTipRole: - if source_index.column() == self.sourceModel().columns_types.index('date'): - return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate) - - if state_data == TransferState.VALIDATING or state_data == TransferState.AWAITING: - block_col = model.columns_types.index('block_number') - block_index = model.index(source_index.row(), block_col) - block_data = model.data(block_index, Qt.DisplayRole) - - current_confirmations = 0 - if state_data == TransferState.VALIDATING: - current_blockUID_number = self.community.network.current_blockUID.number - if current_blockUID_number: - current_confirmations = current_blockUID_number - block_data - elif state_data == TransferState.AWAITING: - current_confirmations = 0 - - max_confirmations = self.sourceModel().max_confirmations() - - if self.app.preferences['expert_mode']: - return self.tr("{0} / {1} confirmations").format(current_confirmations, max_confirmations) - else: - confirmation = current_confirmations / max_confirmations * 100 - confirmation = 100 if confirmation > 100 else confirmation - return self.tr("Confirming... {0} %").format(QLocale().toString(float(confirmation), 'f', 0)) - - return None - return source_data - - -class HistoryTableModel(QAbstractTableModel): - """ - A Qt abstract item model to display communities in a tree - """ - - def __init__(self, app, account, community, parent=None): - """ - Constructor - """ - super().__init__(parent) - self.app = app - self.account = account - self.community = community - self.transfers_data = [] - self.refresh_transfers() - - self.columns_types = ( - 'date', - 'uid', - 'payment', - 'deposit', - 'comment', - 'state', - 'txid', - 'pubkey', - 'block_number', - 'amount' - ) - - self.column_headers = ( - lambda: self.tr('Date'), - lambda: self.tr('UID/Public key'), - lambda: self.tr('Payment'), - lambda: self.tr('Deposit'), - lambda: self.tr('Comment'), - lambda: 'State', - lambda: 'TXID', - lambda: 'Pubkey', - lambda: 'Block Number' - ) - - def change_account(self, account): - cancel_once_task(self, self.refresh_transfers) - self.account = account - - def change_community(self, community): - cancel_once_task(self, self.refresh_transfers) - self.community = community - - def transfers(self): - if self.account: - return self.account.transfers(self.community) + self.account.dividends(self.community) - else: - return [] - - async def data_received(self, transfer): - amount = transfer.metadata['amount'] - if transfer.blockUID: - block_number = transfer.blockUID.number - else: - block_number = None - try: - deposit = await self.account.current_ref.instance(transfer.metadata['amount'], self.community, - self.app, block_number)\ - .diff_localized(international_system=self.app.preferences['international_system_of_units']) - except NoPeerAvailable: - deposit = "Could not compute" - comment = "" - if transfer.metadata['comment'] != "": - comment = transfer.metadata['comment'] - if transfer.metadata['issuer_uid'] != "": - sender = transfer.metadata['issuer_uid'] - else: - sender = "pub:{0}".format(transfer.metadata['issuer'][:5]) - - date_ts = transfer.metadata['time'] - txid = transfer.metadata['txid'] - - return (date_ts, sender, "", deposit, - comment, transfer.state, txid, - transfer.metadata['issuer'], block_number, amount) - - async def data_sent(self, transfer): - if transfer.blockUID: - block_number = transfer.blockUID.number - else: - block_number = None - - amount = transfer.metadata['amount'] - try: - paiment = await self.account.current_ref.instance(transfer.metadata['amount'], self.community, - self.app, block_number)\ - .diff_localized(international_system=self.app.preferences['international_system_of_units']) - except NoPeerAvailable: - paiment = "Could not compute" - comment = "" - if transfer.metadata['comment'] != "": - comment = transfer.metadata['comment'] - if transfer.metadata['receiver_uid'] != "": - receiver = transfer.metadata['receiver_uid'] - else: - receiver = "pub:{0}".format(transfer.metadata['receiver'][:5]) - - date_ts = transfer.metadata['time'] - txid = transfer.metadata['txid'] - return (date_ts, receiver, paiment, - "", comment, transfer.state, txid, - transfer.metadata['receiver'], block_number, amount) - - async def data_dividend(self, dividend): - amount = dividend['amount'] * math.pow(10, dividend['base']) - try: - deposit = await self.account.current_ref.instance(amount, self.community, self.app, dividend['block_number'])\ - .diff_localized(international_system=self.app.preferences['international_system_of_units']) - except NoPeerAvailable: - deposit = "Could not compute" - comment = "" - receiver = self.account.name - date_ts = dividend['time'] - id = dividend['id'] - block_number = dividend['block_number'] - state = dividend['state'] - - return (date_ts, receiver, "", - deposit, "", state, id, - self.account.pubkey, block_number, amount) - - @once_at_a_time - @asyncify - async def refresh_transfers(self): - self.beginResetModel() - self.transfers_data = [] - self.endResetModel() - self.beginResetModel() - transfers_data = [] - if self.community: - requests_coro = [] - data_list = [] - count = 0 - transfers = self.transfers() - for transfer in transfers: - coro = None - count += 1 - if type(transfer) is Transfer: - if transfer.metadata['issuer'] == self.account.pubkey: - coro = asyncio.ensure_future(self.data_sent(transfer)) - else: - coro = asyncio.ensure_future(self.data_received(transfer)) - elif type(transfer) is dict: - coro = asyncio.ensure_future(self.data_dividend(transfer)) - if coro: - requests_coro.append(coro) - if count % 25 == 0: - gathered_list = await asyncio.gather(*requests_coro) - requests_coro = [] - data_list.extend(gathered_list) - # One last gathering - gathered_list = await asyncio.gather(*requests_coro) - data_list.extend(gathered_list) - - for data in data_list: - transfers_data.append(data) - self.transfers_data = transfers_data - self.endResetModel() - - def max_confirmations(self): - return MAX_CONFIRMATIONS - - def rowCount(self, parent): - return len(self.transfers_data) - - def columnCount(self, parent): - return len(self.columns_types) - - def headerData(self, section, orientation, role): - if self.account and self.community: - if role == Qt.DisplayRole: - if self.columns_types[section] == 'payment' or self.columns_types[section] == 'deposit': - return '{:}\n({:})'.format( - self.column_headers[section](), - self.account.current_ref.instance(0, self.community, self.app, None).diff_units - ) - - return self.column_headers[section]() - - def data(self, index, role): - row = index.row() - col = index.column() - - if not index.isValid(): - return QVariant() - - if role == Qt.DisplayRole: - return self.transfers_data[row][col] - - if role == Qt.ToolTipRole: - return self.transfers_data[row][col] - - if role == Qt.DecorationRole and index.column() == 0: - transfer = self.transfers_data[row] - if transfer[self.columns_types.index('payment')] != "": - return QIcon(":/icons/sent") - elif transfer[self.columns_types.index('uid')] == self.account.name: - return QIcon(":/icons/dividend") - else: - return QIcon(":/icons/received") - - def flags(self, index): - return Qt.ItemIsSelectable | Qt.ItemIsEnabled - diff --git a/src/sakia/models/wallets.py b/src/sakia/models/wallets.py deleted file mode 100644 index 5eac7066bd402bed1ff1bbe478710249584b9220..0000000000000000000000000000000000000000 --- a/src/sakia/models/wallets.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -Created on 8 févr. 2014 - -@author: inso -""" -import asyncio -from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, QLocale, pyqtSlot - - -class WalletsFilterProxyModel(QSortFilterProxyModel): - def __init__(self, parent=None): - super().__init__(parent) - self.community = None - self.app = None - - @property - def account(self): - return self.app.current_account - - def columnCount(self, parent): - return self.sourceModel().columnCount(None) - - def setSourceModel(self, source_model): - self.community = source_model.community - self.app = source_model.app - super().setSourceModel(source_model) - - def lessThan(self, left, right): - """ - Sort table by given column number. - """ - left_data = self.sourceModel().data(left, Qt.DisplayRole) - right_data = self.sourceModel().data(right, Qt.DisplayRole) - return left_data < right_data - - def data(self, index, role): - source_index = self.mapToSource(index) - source_data = self.sourceModel().data(source_index, role) - if role == Qt.DisplayRole: - if source_index.column() == self.sourceModel().columns_types.index('pubkey'): - pubkey = source_data - source_data = pubkey - return source_data - if source_index.column() == self.sourceModel().columns_types.index('amount'): - return self.account.current_ref.instance(source_data, self.community, self.app).localized() - if role == Qt.TextAlignmentRole: - if source_index.column() == self.sourceModel().columns_types.index('amount'): - return Qt.AlignRight | Qt.AlignVCenter - - return source_data - - -class WalletsTableModel(QAbstractTableModel): - """ - A Qt list model to display wallets and edit their names - """ - - def __init__(self, app, community, parent=None): - """ - - :param list of sakia.core.wallet.Wallet wallets: The list of wallets to display - :param sakia.core.community.Community community: The community to show - :param PyQt5.QtCore.QObject parent: The parent widget - :return: The model - :rtype: WalletsTableModel - """ - super().__init__(parent) - self.app = app - self.account.wallets_changed.connect(self.refresh_account_wallets) - - self.community = community - self.columns_headers = (self.tr('Name'), - self.tr('Amount'), - self.tr('Pubkey')) - self.columns_types = ('name', 'amount', 'pubkey') - - @property - def account(self): - return self.app.current_account - - @property - def wallets(self): - return self.account.wallets - - @pyqtSlot() - def refresh_account_wallets(self): - """ - Change the current wallets, reconnect the slots - """ - self.beginResetModel() - for w in self.account.wallets: - w.inner_data_changed.connect(lambda: self.refresh_wallet(w)) - self.endResetModel() - - def refresh_wallet(self, wallet): - """ - Refresh the specified wallet value - :param sakia.core.wallet.Wallet wallet: The wallet to refresh - """ - index = self.account.wallets.index(wallet) - if index > 0: - self.dataChanged.emit(index, index) - - def rowCount(self, parent): - return len(self.wallets) - - def columnCount(self, parent): - return len(self.columns_types) - - def headerData(self, section, orientation, role): - if role == Qt.DisplayRole: - if self.columns_types[section] == 'amount': - return '{:}\n({:})'.format( - self.columns_headers[section], - self.account.current_ref.instance(0, self.community, self.app, None).units - ) - return self.columns_headers[section] - - def wallet_data(self, row): - name = self.wallets[row].name - amount = self.wallets[row].value(self.community) - pubkey = self.wallets[row].pubkey - return name, amount, pubkey - - def data(self, index, role): - row = index.row() - col = index.column() - if role == Qt.DisplayRole: - return self.wallet_data(row)[col] - - def setData(self, index, value, role): - if role == Qt.EditRole: - row = index.row() - col = index.column() - # Change model only if value not empty... - if col == self.columns_types.index('name') and value: - self.wallets[row].name = value - self.dataChanged.emit(index, index) - return True - return False - - def flags(self, index): - default_flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled - #  Only name column is editable - if index.column() == 0: - return default_flags | Qt.ItemIsEditable - return default_flags diff --git a/src/sakia/core/money/__init__.py b/src/sakia/money/__init__.py similarity index 76% rename from src/sakia/core/money/__init__.py rename to src/sakia/money/__init__.py index dda0375b6aecb776c1534da5f4fff6a09546ac1b..05a4b506a6ff4f03c503992f6cae11774a6a4ab1 100644 --- a/src/sakia/core/money/__init__.py +++ b/src/sakia/money/__init__.py @@ -2,6 +2,5 @@ from .quantitative import Quantitative from .relative import Relative from .quant_zerosum import QuantitativeZSum from .relative_zerosum import RelativeZSum -from .dividend_per_day import DividendPerDay -Referentials = (Quantitative, Relative, QuantitativeZSum, RelativeZSum, DividendPerDay) +Referentials = (Quantitative, Relative, QuantitativeZSum, RelativeZSum) diff --git a/src/sakia/money/base_referential.py b/src/sakia/money/base_referential.py new file mode 100644 index 0000000000000000000000000000000000000000..3dabb7cc66c2ab304399a0cf0f57791172d02ad8 --- /dev/null +++ b/src/sakia/money/base_referential.py @@ -0,0 +1,54 @@ + + +class BaseReferential: + """ + Interface to all referentials + """ + def __init__(self, amount, currency, app, block_number=None): + """ + + :param int amount: + :param str currency: + :param sakia.app.Application app: + :param int block_number: + """ + self.amount = amount + self.app = app + self.currency = currency + self._block_number = block_number + + @classmethod + def instance(cls, amount, currency, app, block_number=None): + return cls(amount, currency, app, block_number) + + @classmethod + def translated_name(self): + raise NotImplementedError() + + @property + def units(self): + raise NotImplementedError() + + @property + def diff_units(self): + raise NotImplementedError() + + def value(self): + raise NotImplementedError() + + def differential(self): + raise NotImplementedError() + + @staticmethod + def to_si(value, base): + raise NotImplementedError() + + @staticmethod + def base_str(base): + raise NotImplementedError() + + def localized(self, units=False, show_base=False): + raise NotImplementedError() + + def diff_localized(self, units=False, show_base=False): + raise NotImplementedError() diff --git a/src/sakia/money/currency.py b/src/sakia/money/currency.py new file mode 100644 index 0000000000000000000000000000000000000000..8d1e335666583941cfb26539ed4bfd982f1cc599 --- /dev/null +++ b/src/sakia/money/currency.py @@ -0,0 +1,28 @@ +import re + + +def shortened(currency): + """ + Format the currency name to a short one + + :return: The currency name in a shot format. + """ + words = re.split('[_\W]+', currency) + if len(words) > 1: + short = ''.join([w[0] for w in words]) + else: + vowels = ('a', 'e', 'i', 'o', 'u', 'y') + short = currency + short = ''.join([c for c in short if c not in vowels]) + return short.upper() + + +def symbol(currency): + """ + Format the currency name to a symbol one. + + :return: The currency name as a utf-8 circled symbol. + """ + letter = currency[0] + u = ord('\u24B6') + ord(letter) - ord('A') + return chr(u) \ No newline at end of file diff --git a/src/sakia/core/money/quant_zerosum.py b/src/sakia/money/quant_zerosum.py similarity index 65% rename from src/sakia/core/money/quant_zerosum.py rename to src/sakia/money/quant_zerosum.py index 767257c85d1142fb3d510e881d06b8de897150ba..b5ec19f6a914988f5339a9ba934e4d1bff720029 100644 --- a/src/sakia/core/money/quant_zerosum.py +++ b/src/sakia/money/quant_zerosum.py @@ -1,11 +1,13 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from . import Quantitative from .base_referential import BaseReferential +from .currency import shortened +from ..data.processors import BlockchainProcessor class QuantitativeZSum(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', 'Quant Z-sum') - _REF_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', "{0} {1}Q0 {2}") + _REF_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', "{0} {1}Q0{2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', "Q0 {0}") _FORMULA_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', """Z0 = Q - ( M(t-1) / N(t) ) @@ -26,8 +28,9 @@ class QuantitativeZSum(BaseReferential): the value is under the average value. """.replace('\n', '<br >')) - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) + def __init__(self, amount, currency, app, block_number=None): + super().__init__(amount, currency, app, block_number) + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) @classmethod def translated_name(cls): @@ -35,7 +38,7 @@ class QuantitativeZSum(BaseReferential): @property def units(self): - return QCoreApplication.translate("QuantitativeZSum", QuantitativeZSum._UNITS_STR_).format(self.community.short_currency) + return QCoreApplication.translate("QuantitativeZSum", QuantitativeZSum._UNITS_STR_).format(shortened(self.currency)) @property def formula(self): @@ -47,9 +50,9 @@ class QuantitativeZSum(BaseReferential): @property def diff_units(self): - return QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_).format(self.community.short_currency) + return QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_).format(shortened(self.currency)) - async def value(self): + def value(self): """ Return quantitative value of amount minus the average value @@ -66,35 +69,45 @@ class QuantitativeZSum(BaseReferential): :param sakia.core.community.Community community: Community instance :return: int """ - ud_block = await self.community.get_ud_block() - if ud_block and ud_block['membersCount'] > 0: - monetary_mass = await self.community.monetary_mass() - average = int(monetary_mass / ud_block['membersCount']) + last_members_count = self._blockchain_processor.last_members_count(self.currency) + monetary_mass = self._blockchain_processor.current_mass(self.currency) + if last_members_count != 0: + average = int(monetary_mass / last_members_count) else: average = 0 - return self.amount - average + return (self.amount - average)/100 - async def differential(self): - return await Quantitative(self.amount, self.community, self.app).value() + @staticmethod + def base_str(base): + return Quantitative.base_str(base) - async def localized(self, units=False, international_system=False): - value = await self.value() + @staticmethod + def to_si(value, base): + return Quantitative.to_si(value, base) + + def differential(self): + return Quantitative(self.amount, self.currency, self.app).value() + + def localized(self, units=False, show_base=False): + value = self.value() + dividend, base = self._blockchain_processor.last_ud(self.currency) prefix = "" - if international_system: - localized_value, prefix = Quantitative.to_si(value, self.app.preferences['digits_after_comma']) + if show_base: + localized_value = QuantitativeZSum.to_si(value, base) + prefix = QuantitativeZSum.base_str(base) else: - localized_value = QLocale().toString(float(value), 'f', 0) + localized_value = QLocale().toString(float(value), 'f', 2) - if units or international_system: + if units or show_base: return QCoreApplication.translate("QuantitativeZSum", QuantitativeZSum._REF_STR_) \ - .format(localized_value, - prefix, - self.community.short_currency if units else "") + .format(localized_value, + prefix + (" " if prefix else ""), + (" " if units else "") + (shortened(self.currency) if units else "")) else: return localized_value - async def diff_localized(self, units=False, international_system=False): - localized = await Quantitative(self.amount, self.community, self.app).localized(units, international_system) + def diff_localized(self, units=False, show_base=False): + localized = Quantitative(self.amount, self.currency, self.app).localized(units, show_base) return localized diff --git a/src/sakia/core/money/quantitative.py b/src/sakia/money/quantitative.py similarity index 57% rename from src/sakia/core/money/quantitative.py rename to src/sakia/money/quantitative.py index aa4808e39ce6108a770681ba55d1984246800645..1f4c06dafa456d362dff65af2883c19a8e3e8bd5 100644 --- a/src/sakia/core/money/quantitative.py +++ b/src/sakia/money/quantitative.py @@ -1,5 +1,7 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from .base_referential import BaseReferential +from .currency import shortened +from ..data.processors import BlockchainProcessor class Quantitative(BaseReferential): @@ -16,8 +18,9 @@ class Quantitative(BaseReferential): ) _DESCRIPTION_STR_ = QT_TRANSLATE_NOOP('Quantitative', "Base referential of the money. Units values are used here.") - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) + def __init__(self, amount, currency, app, block_number=None): + super().__init__(amount, currency, app, block_number) + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) @classmethod def translated_name(cls): @@ -25,7 +28,7 @@ class Quantitative(BaseReferential): @property def units(self): - return QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_).format(self.community.short_currency) + return QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_).format(shortened(self.currency)) @property def formula(self): @@ -39,7 +42,7 @@ class Quantitative(BaseReferential): def diff_units(self): return self.units - async def value(self): + def value(self): """ Return quantitative value of amount @@ -47,14 +50,29 @@ class Quantitative(BaseReferential): :param sakia.core.community.Community community: Community instance :return: int """ - return int(self.amount) + return int(self.amount) / 100 - async def differential(self): - return await self.value() + def differential(self): + return self.value() @staticmethod - def to_si(value, digits): - prefixes = ['', 'k', 'M', 'G', 'Tera', 'Peta', 'Exa', 'Zeta', 'Yotta'] + def base_str(base): + unicodes = { + '0': ord('\u2070'), + '1': ord('\u00B9'), + '2': ord('\u00B2'), + '3': ord('\u00B3'), + } + for n in range(4, 10): + unicodes[str(n)] = ord('\u2070') + n + + if base > 1: + return "x10" + "".join([chr(unicodes[e]) for e in str(base)]) + else: + return "" + + @staticmethod + def to_si(value, base): if value < 0: value = -value multiplier = -1 @@ -62,51 +80,49 @@ class Quantitative(BaseReferential): multiplier = 1 scientific_value = value - prefix_index = 0 - prefix = "" - - while scientific_value > 1000: - prefix_index += 1 - scientific_value /= 1000 + scientific_value /= 10**base - if prefix_index < len(prefixes): - prefix = prefixes[prefix_index] - localized_value = QLocale().toString(float(scientific_value * multiplier), 'f', digits) + if base > 1: + localized_value = QLocale().toString(float(scientific_value * multiplier), 'f', 2) else: - localized_value = QLocale().toString(float(value * multiplier), 'f', 0) + localized_value = QLocale().toString(float(value * multiplier), 'f', 2) - return localized_value, prefix + return localized_value - async def localized(self, units=False, international_system=False): - value = await self.value() - prefix = "" - if international_system: - localized_value, prefix = Quantitative.to_si(value, self.app.preferences['digits_after_comma']) + def localized(self, units=False, show_base=False): + value = self.value() + dividend, base = self._blockchain_processor.last_ud(self.currency) + if show_base: + localized_value = Quantitative.to_si(value, base) + prefix = Quantitative.base_str(base) else: - localized_value = QLocale().toString(float(value), 'f', 0) + localized_value = QLocale().toString(float(value), 'f', 2) + prefix = "" - if units or international_system: + if units or show_base: return QCoreApplication.translate("Quantitative", Quantitative._REF_STR_) \ .format(localized_value, prefix, - self.community.short_currency if units else "") + (" " if prefix and units else "") + (shortened(self.currency) if units else "")) else: return localized_value - async def diff_localized(self, units=False, international_system=False): - value = await self.differential() - prefix = "" - if international_system: - localized_value, prefix = Quantitative.to_si(value, self.app.preferences['digits_after_comma']) + def diff_localized(self, units=False, show_base=False): + value = self.differential() + dividend, base = self._blockchain_processor.last_ud(self.currency) + if show_base: + localized_value = Quantitative.to_si(value, base) + prefix = Quantitative.base_str(base) else: - localized_value = QLocale().toString(float(value), 'f', 0) + localized_value = QLocale().toString(float(value), 'f', 2) + prefix = "" - if units or international_system: + if units or show_base: return QCoreApplication.translate("Quantitative", Quantitative._REF_STR_) \ .format(localized_value, prefix, - self.community.short_currency if units else "") + (" " if prefix and units else "") + (shortened(self.currency) if units else "")) else: return localized_value diff --git a/src/sakia/core/money/relative.py b/src/sakia/money/relative.py similarity index 51% rename from src/sakia/core/money/relative.py rename to src/sakia/money/relative.py index 004deb6c3aed2d7b2a597740d41e523a518c349d..f8afe4f35adf55bbffa76fec3d39a724395e1727 100644 --- a/src/sakia/core/money/relative.py +++ b/src/sakia/money/relative.py @@ -1,13 +1,13 @@ -from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale from .base_referential import BaseReferential -from .relative_to_past import RelativeToPast +from .currency import shortened +from ..data.processors import BlockchainProcessor from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale class Relative(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('Relative', 'UD') - _REF_STR_ = QT_TRANSLATE_NOOP('Relative', "{0} {1}UD {2}") + _REF_STR_ = QT_TRANSLATE_NOOP('Relative', "{0} {1}UD{2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('Relative', "UD {0}") _FORMULA_STR_ = QT_TRANSLATE_NOOP('Relative', """R = Q / UD(t) @@ -28,23 +28,29 @@ class Relative(BaseReferential): the average. """.replace('\n', '<br >')) - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) + def __init__(self, amount, currency, app, block_number=None): + super().__init__(amount, currency, app, block_number) + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) @classmethod - def instance(cls, amount, community, app, block_number=None): - if app.preferences['forgetfulness']: - return cls(amount, community, app, block_number) - else: - return RelativeToPast(amount, community, app, block_number) + def instance(cls, amount, currency, app, block_number=None): + """ + :param int amount: + :param str currency: + :param sakia.app.Application app: + :param int block_number: + :return: + """ + return cls(amount, currency, app, block_number) + @classmethod def translated_name(cls): return QCoreApplication.translate('Relative', Relative._NAME_STR_) @property def units(self): - return QCoreApplication.translate("Relative", Relative._UNITS_STR_).format(self.community.short_currency) + return QCoreApplication.translate("Relative", Relative._UNITS_STR_).format(shortened(self.currency)) @property def formula(self): @@ -58,7 +64,11 @@ class Relative(BaseReferential): def diff_units(self): return self.units - async def value(self): + @staticmethod + def base_str(base): + return "" + + def value(self): """ Return relative value of amount @@ -68,67 +78,37 @@ class Relative(BaseReferential): :param sakia.core.community.Community community: Community instance :return: float """ - dividend = await self.community.dividend() + dividend, base = self._blockchain_processor.last_ud(self.currency) if dividend > 0: - return self.amount / float(dividend) + return self.amount / (float(dividend * (10**base))) else: return self.amount - async def differential(self): - return await self.value() + def differential(self): + return self.value() - @staticmethod - def to_si(value, digits): - prefixes = ['', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'] - if value < 0: - value = -value - multiplier = -1 - else: - multiplier = 1 - scientific_value = value - prefix_index = 0 + def localized(self, units=False, show_base=False): + value = self.value() prefix = "" + localized_value = QLocale().toString(float(value), 'f', self.app.parameters.digits_after_comma) - while int(scientific_value) == 0 and scientific_value > 0.0: - scientific_value *= 1000 - prefix_index += 1 - - if prefix_index < len(prefixes): - prefix = prefixes[prefix_index] - localized_value = QLocale().toString(float(scientific_value * multiplier), 'f', digits) - else: - localized_value = QLocale().toString(float(value * multiplier), 'f', digits) - - return localized_value, prefix - - async def localized(self, units=False, international_system=False): - value = await self.value() - prefix = "" - if international_system: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - - if units or international_system: + if units: return QCoreApplication.translate("Relative", Relative._REF_STR_) \ .format(localized_value, - prefix, - self.community.short_currency if units else "") + prefix + " " if prefix else "", + (" " + shortened(self.currency)) if units else "") else: return localized_value - async def diff_localized(self, units=False, international_system=False): - value = await self.differential() + def diff_localized(self, units=False, show_base=False): + value = self.differential() prefix = "" - if international_system and value != 0: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) + localized_value = QLocale().toString(float(value), 'f', self.app.parameters.digits_after_comma) - if units or international_system: + if units: return QCoreApplication.translate("Relative", Relative._REF_STR_) \ .format(localized_value, - prefix, - self.community.short_currency if units else "") + prefix + " " if prefix else "", + (" " + shortened(self.currency)) if units else "") else: return localized_value diff --git a/src/sakia/core/money/relative_zerosum.py b/src/sakia/money/relative_zerosum.py similarity index 56% rename from src/sakia/core/money/relative_zerosum.py rename to src/sakia/money/relative_zerosum.py index 86d2eb1c4aae2acefd811c3c6c3fe57fe5c4ecc4..c000a30b3f02524becad42c5fdd35869cafeaab9 100644 --- a/src/sakia/core/money/relative_zerosum.py +++ b/src/sakia/money/relative_zerosum.py @@ -1,19 +1,20 @@ -import math from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from .relative import Relative from .base_referential import BaseReferential +from .currency import shortened +from ..data.processors import BlockchainProcessor class RelativeZSum(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', 'Relat Z-sum') - _REF_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', "{0} {1}R0 {2}") + _REF_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', "{0} {1}R0{2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', "R0 {0}") _FORMULA_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', - """R0 = (Q / UD(t)) - (( M(t-1) / N(t) ) / UD(t)) + """R0 = (R / UD(t)) - (( M(t-1) / N(t) ) / UD(t)) <br > <table> <tr><td>R0</td><td>Relative value at zero sum</td></tr> - <tr><td>Q</td><td>Quantitative value</td></tr> + <tr><td>R</td><td>Relative value</td></tr> <tr><td>M</td><td>Monetary mass</td></tr> <tr><td>N</td><td>Members count</td></tr> <tr><td>t</td><td>Last UD time</td></tr> @@ -26,8 +27,9 @@ class RelativeZSum(BaseReferential): the value is under the average value. """.replace('\n', '<br >')) - def __init__(self, amount, community, app, block_number=None): - super().__init__(amount, community, app, block_number) + def __init__(self, amount, currency, app, block_number=None): + super().__init__(amount, currency, app, block_number) + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) @classmethod def translated_name(cls): @@ -35,7 +37,7 @@ class RelativeZSum(BaseReferential): @property def units(self): - return QCoreApplication.translate("RelativeZSum", RelativeZSum._UNITS_STR_).format(self.community.short_currency) + return QCoreApplication.translate("RelativeZSum", RelativeZSum._UNITS_STR_).format(shortened(self.currency)) @property def formula(self): @@ -47,9 +49,13 @@ class RelativeZSum(BaseReferential): @property def diff_units(self): - return QCoreApplication.translate("Relative", Relative._UNITS_STR_).format(self.community.short_currency) + return QCoreApplication.translate("Relative", Relative._UNITS_STR_).format(shortened(self.currency)) - async def value(self): + @staticmethod + def base_str(base): + return Relative.base_str(base) + + def value(self): """ Return relative value of amount minus the average value @@ -64,48 +70,41 @@ class RelativeZSum(BaseReferential): :param sakia.core.community.Community community: Community instance :return: float """ - ud_block = await self.community.get_ud_block() - ud_block_minus_1 = await self.community.get_ud_block(x=1) - if ud_block_minus_1 and ud_block['membersCount'] > 0: - median = ud_block_minus_1['monetaryMass'] / ud_block['membersCount'] - relative_value = self.amount / float(ud_block['dividend'] * math.pow(10, ud_block['unitbase'])) - relative_median = median / (ud_block['dividend'] * math.pow(10, ud_block['unitbase'])) + dividend, base = self._blockchain_processor.previous_ud(self.currency) + previous_monetary_mass = self._blockchain_processor.previous_monetary_mass(self.currency) + members_count = self._blockchain_processor.current_members_count(self.currency) + if previous_monetary_mass and members_count > 0: + median = previous_monetary_mass / members_count + relative_value = self.amount / float(dividend * 10**base) + relative_median = median / float(dividend * 10**base) else: relative_value = self.amount relative_median = 0 return relative_value - relative_median - async def differential(self): - return await Relative(self.amount, self.community, self.app).value() + def differential(self): + return Relative(self.amount, self.currency, self.app).value() - async def localized(self, units=False, international_system=False): - value = await self.value() + def localized(self, units=False, show_base=False): + value = self.value() - prefix = "" - if international_system: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) + localized_value = QLocale().toString(float(value), 'f', self.app.parameters.digits_after_comma) - if units or international_system: + if units: return QCoreApplication.translate("RelativeZSum", RelativeZSum._REF_STR_)\ - .format(localized_value, - prefix, - self.community.short_currency if units else "") + .format(localized_value, "", + (" " + shortened(self.currency)) if units else "") else: return localized_value - async def diff_localized(self, units=False, international_system=False): - value = await self.differential() + def diff_localized(self, units=False, show_base=False): + value = self.differential() - prefix = "" - if international_system and value != 0: - localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) - else: - localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) + localized_value = QLocale().toString(float(value), 'f', self.app.parameters.digits_after_comma) - if units or international_system: + if units: return QCoreApplication.translate("Relative", Relative._REF_STR_)\ - .format(localized_value, prefix, self.community.short_currency if units else "") + .format(localized_value, "", + (" " + shortened(self.currency)) if units else "") else: return localized_value diff --git a/src/sakia/options.py b/src/sakia/options.py new file mode 100644 index 0000000000000000000000000000000000000000..506598acc3e4f3c7e20e5e33ec94511db3c3ab17 --- /dev/null +++ b/src/sakia/options.py @@ -0,0 +1,76 @@ +""" +Created on 7 févr. 2014 + +@author: inso +""" + +import attr +import logging +from logging import FileHandler, StreamHandler +from logging.handlers import RotatingFileHandler +from optparse import OptionParser +from os import environ, path, makedirs + + +def config_path_factory(): + if "XDG_CONFIG_HOME" in environ: + env_path = environ["XDG_CONFIG_HOME"] + elif "HOME" in environ: + env_path = path.join(environ["HOME"], ".config") + elif "APPDATA" in environ: + env_path = environ["APPDATA"] + else: + env_path = path.dirname(__file__) + return path.join(env_path, 'sakia') + + +@attr.s() +class SakiaOptions: + config_path = attr.ib(default=attr.Factory(config_path_factory)) + database = attr.ib(default="sakia") + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def from_arguments(cls, argv): + options = cls() + + if not path.exists(options.config_path): + makedirs(options.config_path) + + options._parse_arguments(argv) + + return options + + def _parse_arguments(self, argv): + parser = OptionParser() + parser.add_option("-v", "--verbose", + action="store_true", dest="verbose", default=False, + help="Print INFO messages to stdout") + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Print DEBUG messages to stdout") + + parser.add_option("--database", dest="database", default="sakia", + help="Select another database filename") + + (options, args) = parser.parse_args(argv) + + if options.database: + self.database = options.database + + if options.debug: + self._logger.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(levelname)s:%(module)s:%(funcName)s:%(message)s') + elif options.verbose: + self._logger.setLevel(logging.INFO) + formatter = logging.Formatter('%(levelname)s:%(message)s') + + if options.debug or options.verbose: + logging.getLogger('quamash').setLevel(logging.INFO) + file_handler = RotatingFileHandler(path.join(self.config_path, 'sakia.log'), 'a', 1000000, 10) + file_handler.setFormatter(formatter) + stream_handler = StreamHandler() + stream_handler.setFormatter(formatter) + self._logger.handlers = [file_handler, stream_handler] + self._logger.propagate = False diff --git a/src/sakia/services/__init__.py b/src/sakia/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..62b82310cb35d4f4e0d442bd01b676516b86a23d --- /dev/null +++ b/src/sakia/services/__init__.py @@ -0,0 +1,6 @@ +from .network import NetworkService +from .identities import IdentitiesService +from .blockchain import BlockchainService +from .documents import DocumentsService +from .sources import SourcesServices +from .transactions import TransactionsService diff --git a/src/sakia/services/blockchain.py b/src/sakia/services/blockchain.py new file mode 100644 index 0000000000000000000000000000000000000000..62a3e33f2d14f322a4ab80fb0255a6ebfe040c1d --- /dev/null +++ b/src/sakia/services/blockchain.py @@ -0,0 +1,108 @@ +from PyQt5.QtCore import QObject +import math +import logging +from duniterpy.api.errors import DuniterError +from sakia.errors import NoPeerAvailable + + +class BlockchainService(QObject): + """ + Blockchain service is managing new blocks received + to update data locally + """ + def __init__(self, app, currency, blockchain_processor, bma_connector, + identities_service, transactions_service, sources_service): + """ + Constructor the identities service + + :param sakia.app.Application app: Sakia application + :param str currency: The currency name of the community + :param sakia.data.processors.BlockchainProcessor blockchain_processor: the blockchain processor for given currency + :param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API + :param sakia.services.IdentitiesService identities_service: The identities service + :param sakia.services.TransactionsService transactions_service: The transactions service + :param sakia.services.SourcesService sources_service: The sources service + """ + super().__init__() + self.app = app + self._blockchain_processor = blockchain_processor + self._bma_connector = bma_connector + self.currency = currency + self._identities_service = identities_service + self._transactions_service = transactions_service + self._sources_service = sources_service + self._logger = logging.getLogger('sakia') + + async def handle_blockchain_progress(self, network_blockstamp): + """ + Handle a new current block uid + + :param duniterpy.documents.BlockUID network_blockstamp: + """ + try: + with_identities = await self._blockchain_processor.new_blocks_with_identities(self.currency) + with_money = await self._blockchain_processor.new_blocks_with_money(self.currency) + blocks = await self._blockchain_processor.blocks(with_identities + with_money + [network_blockstamp.number], + self.currency) + await self._sources_service.refresh_sources() + if len(blocks) > 0: + identities = await self._identities_service.handle_new_blocks(blocks) + changed_tx, new_tx, new_dividends = await self._transactions_service.handle_new_blocks(blocks) + self._blockchain_processor.handle_new_blocks(self.currency, blocks) + self.app.db.commit() + for tx in changed_tx: + self.app.transaction_state_changed.emit(tx) + for tx in new_tx: + self.app.new_transfer.emit(tx) + for ud in new_dividends: + self.app.new_dividend.emit(ud) + for idty in identities: + self.app.identity_changed.emit(idty) + self.app.sources_refreshed.emit() + except (NoPeerAvailable, DuniterError) as e: + self._logger.debug(str(e)) + + def current_buid(self): + return self._blockchain_processor.current_buid(self.currency) + + def parameters(self): + return self._blockchain_processor.parameters(self.currency) + + def time(self): + return self._blockchain_processor.time(self.currency) + + def current_members_count(self): + return self._blockchain_processor.current_members_count(self.currency) + + def current_mass(self): + return self._blockchain_processor.current_mass(self.currency) + + def last_ud(self): + return self._blockchain_processor.last_ud(self.currency) + + def last_members_count(self): + return self._blockchain_processor.last_members_count(self.currency) + + def last_ud_time(self): + return self._blockchain_processor.last_ud_time(self.currency) + + def previous_members_count(self): + return self._blockchain_processor.previous_members_count(self.currency) + + def previous_monetary_mass(self): + return self._blockchain_processor.previous_monetary_mass(self.currency) + + def previous_ud_time(self): + return self._blockchain_processor.previous_ud_time(self.currency) + + def previous_ud(self): + return self._blockchain_processor.previous_ud(self.currency) + + def computed_dividend(self): + """ + Computes next dividend value + :rtype: int + """ + parameters = self.parameters() + next_ud = parameters.c * self.current_mass() / self.last_members_count() + return math.ceil(next_ud) diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py new file mode 100644 index 0000000000000000000000000000000000000000..bd845224d63bc3a9b983ec7a502e09c0dca12d51 --- /dev/null +++ b/src/sakia/services/documents.py @@ -0,0 +1,424 @@ +import jsonschema +import attr +import logging + +from duniterpy.key import SigningKey +from duniterpy.documents import Certification, Membership, Revocation, InputSource, \ + OutputSource, SIGParameter, Unlock, block_uid +from duniterpy.documents import Identity as IdentityDoc +from duniterpy.documents import Transaction as TransactionDoc +from duniterpy.documents.transaction import reduce_base +from duniterpy.grammars import output +from duniterpy.api import bma, errors +from sakia.data.entities import Identity, Transaction +from sakia.data.processors import BlockchainProcessor, IdentitiesProcessor, NodesProcessor, \ + TransactionsProcessor, SourcesProcessor +from sakia.data.connectors import BmaConnector +from sakia.errors import NotEnoughChangeError + + +@attr.s() +class DocumentsService: + """ + A service to forge and broadcast documents + to the network + + :param sakia.data.connectors.BmaConnector _bma_connector: the connector + :param sakia.data.processors.BlockchainProcessor _blockchain_processor: the blockchain processor + :param sakia.data.processors.IdentitiesProcessor _identities_processor: the identities processor + :param sakia.data.processors.TransactionsProcessor _transactions_processor: the transactions processor + :param sakia.data.processors.SourcesProcessor _sources_processor: the sources processor + """ + _bma_connector = attr.ib() + _blockchain_processor = attr.ib() + _identities_processor = attr.ib() + _transactions_processor = attr.ib() + _sources_processor = attr.ib() + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters), + BlockchainProcessor.instanciate(app), + IdentitiesProcessor.instanciate(app), + TransactionsProcessor.instanciate(app), + SourcesProcessor.instanciate(app)) + + async def broadcast_identity(self, connection, password): + """ + Send our self certification to a target community + + :param sakia.data.entities.Connection connection: the connection published + :param str password: the private key password + """ + block_uid = self._blockchain_processor.current_buid(connection.currency) + timestamp = self._blockchain_processor.time(connection.currency) + selfcert = IdentityDoc(10, + connection.currency, + connection.pubkey, + connection.uid, + block_uid, + None) + key = SigningKey(connection.salt, password, connection.scrypt_params) + selfcert.sign([key]) + self._logger.debug("Key publish : {0}".format(selfcert.signed_raw())) + + responses = await self._bma_connector.broadcast(connection.currency, bma.wot.add, + req_args={'identity': selfcert.signed_raw()}) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + + if result[0]: + identity = self._identities_processor.get_identity(connection.currency, connection.pubkey, connection.uid) + if not identity: + identity = Identity(connection.currency, connection.pubkey, connection.uid) + identity.blockstamp = block_uid + identity.signature = selfcert.signatures[0] + identity.timestamp = timestamp + else: + identity = None + + return result, identity + + async def broadcast_revocation(self, currency, identity_document, revocation_document): + signed_raw = revocation_document.signed_raw(identity_document) + self._logger.debug("Broadcasting : \n" + signed_raw) + responses = await self._bma_connector.broadcast(currency, bma.wot.revoke, req_args={ + 'revocation': signed_raw + }) + + result = False, "" + for r in responses: + if r.status == 200: + result = True, (await r.json()) + elif not result[0]: + try: + result = False, bma.api.parse_error(await r.text())["message"] + except jsonschema.ValidationError as e: + result = False, str(e) + else: + await r.release() + + return result + + async def send_membership(self, connection, password, mstype): + """ + Send a membership document to a target community. + Signal "document_broadcasted" is emitted at the end. + + :param str currency: the currency target + :param sakia.data.entities.IdentityDoc identity: the identitiy data + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + :param str mstype: The type of membership demand. "IN" to join, "OUT" to leave + """ + self._logger.debug("Send membership") + + blockUID = self._blockchain_processor.current_buid(connection.currency) + membership = Membership(10, connection.currency, + connection.pubkey, blockUID, mstype, connection.uid, + connection.blockstamp, None) + key = SigningKey(connection.salt, password, connection.scrypt_params) + membership.sign([key]) + self._logger.debug("Membership : {0}".format(membership.signed_raw())) + responses = await self._bma_connector.broadcast(connection.currency, bma.blockchain.membership, + req_args={'membership': membership.signed_raw()}) + result = (False, "") + for r in responses: + if not result[0]: + if isinstance(r, BaseException): + result = (False, str(r)) + else: + try: + result = (False, (await r.json())["message"]) + except KeyError: + result = (False, await str(r.text())) + + elif r.status == 200: + result = (True, (await r.json())) + else: + await r.release() + return result + + async def certify(self, connection, password, identity): + """ + Certify an other identity + + :param sakia.data.entities.Connection connection: the connection published + :param str password: the private key password + :param sakia.data.entities.Identity identity: the identity certified + """ + self._logger.debug("Certdata") + blockUID = self._blockchain_processor.current_buid(connection.currency) + if not identity.signature: + lookup_data = await self._bma_connector.get(connection.currency, bma.wot.lookup, + req_args={'search': identity.pubkey}) + for uid_data in next(data["uids"] for data in lookup_data["results"] if data["pubkey"] == identity.pubkey): + if uid_data["uid"] == identity.uid and block_uid(uid_data["meta"]["timestamp"]) == identity.blockstamp: + identity.signature = uid_data["self"] + break + else: + return False, "Could not find certified identity signature" + + certification = Certification(10, connection.currency, + connection.pubkey, identity.pubkey, blockUID, None) + + key = SigningKey(connection.salt, password, connection.scrypt_params) + certification.sign(identity.document(), [key]) + signed_cert = certification.signed_raw(identity.document()) + self._logger.debug("Certification : {0}".format(signed_cert)) + + responses = await self._bma_connector.broadcast(connection.currency, bma.wot.certify, req_args={'cert': signed_cert}) + result = (False, "") + for r in responses: + if isinstance(r, BaseException) and not result[0]: + result = (False, (str(r))) + else: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + return result + + async def revoke(self, currency, identity, salt, password): + """ + Revoke self-identity on server, not in blockchain + + :param str currency: The currency of the identity + :param sakia.data.entities.IdentityDoc identity: The certified identity + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + """ + revocation = Revocation(10, currency, None) + self_cert = identity.document() + + key = SigningKey(salt, password) + revocation.sign(self_cert, [key]) + + self._logger.debug("Self-Revokation Document : \n{0}".format(revocation.raw(self_cert))) + self._logger.debug("Signature : \n{0}".format(revocation.signatures[0])) + + data = { + 'pubkey': identity.pubkey, + 'self_': self_cert.signed_raw(), + 'sig': revocation.signatures[0] + } + self._logger.debug("Posted data : {0}".format(data)) + responses = await self._bma_connector.broadcast(currency, bma.wot.Revoke, {}, data) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + return result + + def generate_revokation(self, connection, password): + """ + Generate account revokation document for given community + + :param sakia.data.entities.Connection connection: The connection of the identity + :param str password: The account SigningKey password + """ + document = Revocation(10, connection.currency, connection.pubkey, "") + identity = self._identities_processor.get_identity(connection.currency, connection.pubkey, connection.uid) + self_cert = identity.document() + + key = SigningKey(connection.salt, password, connection.scrypt_params) + + document.sign(self_cert, [key]) + return document.signed_raw(self_cert) + + def tx_sources(self, amount, amount_base, currency): + """ + Get inputs to generate a transaction with a given amount of money + :param int amount: The amount target value + :param int amount_base: The amount base target value + :param str currency: The community target of the transaction + :return: The list of inputs to use in the transaction document + """ + + # such a dirty algorithmm + # everything should be done again from scratch + # in future versions + + def current_value(inputs, overhs): + i = 0 + for s in inputs: + i += s.amount * (10**s.base) + for o in overhs: + i -= o[0] * (10**o[1]) + return i + + amount, amount_base = reduce_base(amount, amount_base) + available_sources = self._sources_processor.available(currency) + if available_sources: + current_base = max([src.base for src in available_sources]) + value = 0 + sources = [] + outputs = [] + overheads = [] + buf_sources = list(available_sources) + while current_base >= 0: + for s in [src for src in available_sources if src.base == current_base]: + test_sources = sources + [s] + val = current_value(test_sources, overheads) + # if we have to compute an overhead + if current_value(test_sources, overheads) > amount * (10**amount_base): + overhead = current_value(test_sources, overheads) - int(amount) * (10**amount_base) + # we round the overhead in the current base + # exemple : 12 in base 1 -> 1*10^1 + overhead = int(round(float(overhead) / (10**current_base))) + source_value = s.amount * (10**s.base) + out = int((source_value - (overhead * (10**current_base)))/(10**current_base)) + if out * (10**current_base) <= amount * (10**amount_base): + sources.append(s) + buf_sources.remove(s) + overheads.append((overhead, current_base)) + outputs.append((out, current_base)) + # else just add the output + else: + sources.append(s) + buf_sources.remove(s) + outputs.append((s.amount, s.base)) + if current_value(sources, overheads) == amount * (10 ** amount_base): + return sources, outputs, overheads + + current_base -= 1 + + raise NotEnoughChangeError(value, currency, len(sources), amount * pow(10, amount_base)) + + def tx_inputs(self, sources): + """ + Get inputs to generate a transaction with a given amount of money + :param list[sakia.data.entities.Source] sources: The sources used to send the given amount of money + :return: The list of inputs to use in the transaction document + """ + inputs = [] + for s in sources: + inputs.append(InputSource(s.amount, s.base, s.type, s.identifier, s.noffset)) + return inputs + + def tx_unlocks(self, sources): + """ + Get unlocks to generate a transaction with a given amount of money + :param list sources: The sources used to send the given amount of money + :return: The list of unlocks to use in the transaction document + """ + unlocks = [] + for i, s in enumerate(sources): + unlocks.append(Unlock(i, [SIGParameter(0)])) + return unlocks + + def tx_outputs(self, issuer, receiver, outputs, overheads): + """ + Get outputs to generate a transaction with a given amount of money + :param str issuer: The issuer of the transaction + :param str receiver: The target of the transaction + :param list outputs: The amount to send + :param list inputs: The inputs used to send the given amount of money + :param list overheads: The overheads used to send the given amount of money + :return: The list of outputs to use in the transaction document + """ + total = [] + outputs_bases = set(o[1] for o in outputs) + for base in outputs_bases: + output_sum = 0 + for o in outputs: + if o[1] == base: + output_sum += o[0] + total.append(OutputSource(output_sum, base, output.Condition.token(output.SIG.token(receiver)))) + + overheads_bases = set(o[1] for o in overheads) + for base in overheads_bases: + overheads_sum = 0 + for o in overheads: + if o[1] == base: + overheads_sum += o[0] + total.append(OutputSource(overheads_sum, base, output.Condition.token(output.SIG.token(issuer)))) + + return total + + def prepare_tx(self, issuer, receiver, blockstamp, amount, amount_base, message, currency): + """ + Prepare a simple Transaction document + :param str issuer: the issuer of the transaction + :param str receiver: the target of the transaction + :param duniterpy.documents.BlockUID blockstamp: the blockstamp + :param int amount: the amount sent to the receiver + :param int amount_base: the amount base of the currency + :param str message: the comment of the tx + :param str currency: the target community + :return: the transaction document + :rtype: duniterpy.documents.Transaction + """ + result = self.tx_sources(int(amount), amount_base, currency) + sources = result[0] + computed_outputs = result[1] + overheads = result[2] + self._sources_processor.consume(sources) + logging.debug("Inputs : {0}".format(sources)) + + inputs = self.tx_inputs(sources) + unlocks = self.tx_unlocks(sources) + outputs = self.tx_outputs(issuer, receiver, computed_outputs, overheads) + logging.debug("Outputs : {0}".format(outputs)) + tx = TransactionDoc(10, currency, blockstamp, 0, + [issuer], inputs, unlocks, + outputs, message, None) + return tx + + async def send_money(self, connection, password, recipient, amount, amount_base, message): + """ + Send money to a given recipient in a specified community + :param sakia.data.entities.Connection connection: The account salt + :param str password: The account password + :param str recipient: The pubkey of the recipient + :param int amount: The amount of money to transfer + :param int amount_base: The amount base of the transfer + :param str message: The message to send with the transfer + """ + blockstamp = self._blockchain_processor.current_buid(connection.currency) + time = self._blockchain_processor.time(connection.currency) + key = SigningKey(connection.salt, password, connection.scrypt_params) + logging.debug("Sender pubkey:{0}".format(key.pubkey)) + try: + txdoc = self.prepare_tx(connection.pubkey, recipient, blockstamp, amount, amount_base, + message, connection.currency) + logging.debug("TX : {0}".format(txdoc.raw())) + + txdoc.sign([key]) + logging.debug("Transaction : [{0}]".format(txdoc.signed_raw())) + txid = self._transactions_processor.next_txid(connection.currency, blockstamp.number) + tx = Transaction(currency=connection.currency, + sha_hash=txdoc.sha_hash, + written_block=0, + blockstamp=blockstamp, + timestamp=time, + signature=txdoc.signatures[0], + issuer=connection.pubkey, + receiver=recipient, + amount=amount, + amount_base=amount_base, + comment=message, + txid=txid, + state=Transaction.TO_SEND, + local=True, + raw=txdoc.signed_raw()) + return await self._transactions_processor.send(tx, txdoc, connection.currency) + except NotEnoughChangeError as e: + return (False, str(e)), None diff --git a/src/sakia/services/identities.py b/src/sakia/services/identities.py new file mode 100644 index 0000000000000000000000000000000000000000..ef1958fc0c4e39b8bc2157f34f6f7c1ba6fe4d84 --- /dev/null +++ b/src/sakia/services/identities.py @@ -0,0 +1,324 @@ +from PyQt5.QtCore import QObject +import asyncio +from duniterpy.api import bma, errors +from duniterpy.documents import BlockUID, block_uid +from sakia.errors import NoPeerAvailable +from sakia.data.entities import Certification +import logging + + +class IdentitiesService(QObject): + """ + Identities service is managing identities data received + to update data locally + """ + def __init__(self, currency, connections_processor, identities_processor, certs_processor, + blockchain_processor, bma_connector): + """ + Constructor the identities service + + :param str currency: The currency name of the community + :param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor for given currency + :param sakia.data.processors.CertificationsProcessor certs_processor: the certifications processor for given currency + :param sakia.data.processors.BlockchainProcessor blockchain_processor: the blockchain processor for given currency + :param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API + """ + super().__init__() + self._connections_processor = connections_processor + self._identities_processor = identities_processor + self._certs_processor = certs_processor + self._blockchain_processor = blockchain_processor + self._bma_connector = bma_connector + self.currency = currency + self._logger = logging.getLogger('sakia') + + def certification_expired(self, cert_time): + """ + Return True if the certificaton time is too old + + :param int cert_time: the timestamp of the certification + """ + parameters = self._blockchain_processor.parameters(self.currency) + blockchain_time = self._blockchain_processor.time(self.currency) + return blockchain_time - cert_time > parameters.sig_validity + + def certification_writable(self, cert_time): + """ + Return True if the certificaton time is too old + + :param int cert_time: the timestamp of the certification + """ + parameters = self._blockchain_processor.parameters(self.currency) + blockchain_time = self._blockchain_processor.time(self.currency) + return blockchain_time - cert_time < parameters.sig_window * parameters.avg_gen_time + + def expiration_date(self, identity): + """ + Get the expiration date of the identity + :param sakia.data.entities.Identity identity: + :return: the expiration timestamp + :rtype: int + """ + validity = self._blockchain_processor.parameters(self.currency).ms_validity + if identity.membership_timestamp: + return identity.membership_timestamp + validity + else: + return 0 + + def _get_connections_identities(self): + connections = self._connections_processor.connections_to(self.currency) + identities = [] + for c in connections: + identities.append(self._identities_processor.get_identity(self.currency, c.pubkey)) + return identities + + async def load_memberships(self, identity): + """ + Request the identity data and save it to written identities + It does nothing if the identity is already written and updated with blockchain lookups + :param sakia.data.entities.Identity identity: the identity + """ + try: + search = await self._bma_connector.get(self.currency, bma.blockchain.memberships, + req_args={'search': identity.pubkey}) + blockstamp = BlockUID.empty() + membership_data = None + + for ms in search['memberships']: + if ms['blockNumber'] > blockstamp.number: + blockstamp = BlockUID(ms["blockNumber"], ms['blockHash']) + membership_data = ms + if membership_data: + identity.membership_timestamp = await self._blockchain_processor.timestamp(self.currency, blockstamp) + identity.membership_buid = blockstamp + identity.membership_type = ms["membership"] + identity.membership_written_on = ms["written"] + identity = await self.load_requirements(identity) + # We save connections pubkeys + if identity.pubkey in self._connections_processor.pubkeys(): + self._identities_processor.insert_or_update_identity(identity) + except errors.DuniterError as e: + logging.debug(str(e)) + except NoPeerAvailable as e: + logging.debug(str(e)) + return identity + + async def load_certifiers_of(self, identity): + """ + Request the identity data and save it to written certifications + It does nothing if the identity is already written and updated with blockchain lookups + :param sakia.data.entities.Identity identity: the identity + """ + certifications = [] + try: + data = await self._bma_connector.get(self.currency, bma.wot.certifiers_of, + {'search': identity.pubkey}) + for certifier_data in data['certifications']: + cert = Certification(currency=self.currency, + certified=data["pubkey"], + certifier=certifier_data["pubkey"], + block=certifier_data["cert_time"]["block"], + timestamp=certifier_data["cert_time"]["medianTime"], + signature=certifier_data['signature']) + if certifier_data['written']: + cert.written_on = certifier_data['written']['number'] + certifications.append(cert) + # We save connections pubkeys + if identity.pubkey in self._connections_processor.pubkeys(): + self._certs_processor.insert_or_update_certification(cert) + except errors.DuniterError as e: + if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): + logging.debug("Certified by error : {0}".format(str(e))) + except NoPeerAvailable as e: + logging.debug(str(e)) + return certifications + + async def load_certified_by(self, identity): + """ + Request the identity data and save it to written certifications + It does nothing if the identity is already written and updated with blockchain lookups + :param sakia.data.entities.Identity identity: the identity + """ + certifications = [] + try: + data = await self._bma_connector.get(self.currency, bma.wot.certified_by, {'search': identity.pubkey}) + for certified_data in data['certifications']: + cert = Certification(currency=self.currency, + certifier=data["pubkey"], + certified=certified_data["pubkey"], + block=certified_data["cert_time"]["block"], + timestamp=certified_data["cert_time"]["medianTime"], + signature=certified_data['signature']) + if certified_data['written']: + cert.written_on = certified_data['written']['number'] + certifications.append(cert) + # We save connections pubkeys + if identity.pubkey in self._connections_processor.pubkeys(): + self._certs_processor.insert_or_update_certification(cert) + except errors.DuniterError as e: + if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): + logging.debug("Certified by error : {0}".format(str(e))) + except NoPeerAvailable as e: + logging.debug(str(e)) + return certifications + + def _parse_revocations(self, block): + """ + Parse revoked pubkeys found in a block and refresh local data + + :param duniterpy.documents.Block block: the block received + :return: list of pubkeys updated + """ + revoked = set([]) + for rev in block.revoked: + revoked.add(rev.pubkey) + + for pubkey in revoked: + written = self._identities_processor.get_identity(self.currency, pubkey) + # we update every written identities known locally + if written: + written.revoked_on = block.blockUID + return revoked + + def _parse_memberships(self, block): + """ + Parse memberships pubkeys found in a block and refresh local data + + :param duniterpy.documents.Block block: the block received + :return: list of pubkeys requiring a refresh of requirements + """ + need_refresh = [] + connections_identities = self._get_connections_identities() + for ms in block.joiners + block.actives: + # we update every written identities known locally + for identity in connections_identities: + if ms.issuer == identity: + identity.membership_written_on = block.number + identity.membership_type = "IN" + identity.membership_buid = ms.membership_ts + self._identities_processor.insert_or_update_identity(identity) + # If the identity was not member + # it can become one + if not identity.member: + need_refresh.append(identity) + + for ms in block.leavers: + # we update every written identities known locally + for identity in connections_identities: + identity.membership_written_on = block.number + identity.membership_type = "OUT" + identity.membership_buid = ms.membership_ts + self._identities_processor.insert_or_update_identity(identity) + # If the identity was a member + # it can stop to be one + if identity.member: + need_refresh.append(identity) + + return need_refresh + + def _parse_certifications(self, block): + """ + Parse certified pubkeys found in a block and refresh local data + This method only creates certifications if one of both identities is + locally known as written. + This method returns the identities needing to be refreshed. These can only be + the identities which we already known as written before parsing this certification. + :param duniterpy.documents.Block block: + :return: + """ + connections_identities = self._get_connections_identities() + need_refresh = [] + for cert in block.certifications: + # if we have are a target or a source of the certification + for identity in connections_identities: + if cert.pubkey_from == identity.pubkey or cert.pubkey_to in identity.pubkey: + self._certs_processor.create_certification(self.currency, cert, block.blockUID) + need_refresh.append(identity) + return need_refresh + + async def load_requirements(self, identity): + """ + Refresh a given identity information + :param sakia.data.entities.Identity identity: + :return: + """ + try: + requirements = await self._bma_connector.get(self.currency, bma.wot.requirements, + req_args={'search': identity.pubkey}) + identity_data = requirements['identities'][0] + identity.uid = identity_data["uid"] + identity.blockstamp = block_uid(identity_data["meta"]["timestamp"]) + identity.timestamp = await self._blockchain_processor.timestamp(self.currency, identity.blockstamp) + identity.outdistanced = identity_data["outdistanced"] + identity.member = identity_data["membershipExpiresIn"] > 0 and not identity_data["outdistanced"] + median_time = self._blockchain_processor.time(self.currency) + expiration_time = self._blockchain_processor.parameters(self.currency).ms_validity + identity.membership_timestamp = median_time - (expiration_time - identity_data["membershipExpiresIn"]) + # We save connections pubkeys + if identity.pubkey in self._connections_processor.pubkeys(): + self._identities_processor.insert_or_update_identity(identity) + except NoPeerAvailable as e: + self._logger.debug(str(e)) + return identity + + def parse_block(self, block): + """ + Parse a block to refresh local data + :param block: + :return: + """ + self._parse_revocations(block) + need_refresh = [] + need_refresh += self._parse_memberships(block) + need_refresh += self._parse_certifications(block) + return set(need_refresh) + + async def handle_new_blocks(self, blocks): + """ + Handle new block received and refresh local data + :param duniterpy.documents.Block block: the received block + """ + need_refresh = [] + for block in blocks: + need_refresh += self.parse_block(block) + refresh_futures = [] + # for every identity for which we need a refresh, we gather + # requirements requests + for identity in set(need_refresh): + refresh_futures.append(self.load_requirements(identity)) + await asyncio.gather(*refresh_futures) + return need_refresh + + async def lookup(self, text): + """ + Lookup for a given text in the network and in local db + :param str text: text contained in identity data + :rtype: list[sakia.data.entities.Identity] + :return: the list of identities found + """ + return await self._identities_processor.lookup(self.currency, text) + + def get_identity(self, pubkey, uid=""): + return self._identities_processor.get_identity(self.currency, pubkey, uid) + + async def find_from_pubkey(self, pubkey): + return await self._identities_processor.find_from_pubkey(self.currency, pubkey) + + def ms_time_remaining(self, identity): + return self.expiration_date(identity) - self._blockchain_processor.time(identity.currency) + + def certifications_received(self, pubkey): + """ + Get the list of certifications received by a given identity + :param str pubkey: the pubkey + :rtype: List[sakia.data.entities.Certifications] + """ + return self._certs_processor.certifications_received(self.currency, pubkey) + + def certifications_sent(self, pubkey): + """ + Get the list of certifications received by a given identity + :param str pubkey: the pubkey + :rtype: List[sakia.data.entities.Certifications] + """ + return self._certs_processor.certifications_sent(self.currency, pubkey) diff --git a/src/sakia/services/network.py b/src/sakia/services/network.py new file mode 100644 index 0000000000000000000000000000000000000000..472a72b6d03cde4468cdce204b715e94bf34ce6a --- /dev/null +++ b/src/sakia/services/network.py @@ -0,0 +1,278 @@ +import asyncio +import logging +import time +from collections import Counter + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject +from duniterpy.api import errors +from duniterpy.key import VerifyingKey +from sakia.data.connectors import NodeConnector +from sakia.data.entities import Node +from sakia.decorators import asyncify +from sakia.errors import InvalidNodeCurrency + + +class NetworkService(QObject): + """ + A network is managing nodes polling and crawling of a + given community. + """ + nodes_changed = pyqtSignal() + root_nodes_changed = pyqtSignal() + + def __init__(self, app, currency, node_processor, connectors, blockchain_service, identities_service): + """ + Constructor of a network + + :param sakia.app.Application app: The application + :param str currency: The currency name of the community + :param sakia.data.processors.NodesProcessor node_processor: the nodes processor for given currency + :param list connectors: The connectors to nodes of the network + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + """ + super().__init__() + self._app = app + self._logger = logging.getLogger('sakia') + self._processor = node_processor + self._connectors = [] + for c in connectors: + self.add_connector(c) + self.currency = currency + self._must_crawl = False + self._block_found = self._processor.current_buid(self.currency) + self._discovery_stack = [] + self._blockchain_service = blockchain_service + self._identities_service = identities_service + self._discovery_loop_task = None + + @classmethod + def create(cls, node_processor, node_connector): + """ + Create a new network with one knew node + Crawls the nodes from the first node to build the + community network + + :param sakia.data.processors.NodeProcessor node_processor: The nodes processor + :param sakia.data.connectors.NodeConnector node_connector: The first connector of the network service + :return: + """ + connectors = [node_connector] + node_processor.insert_node(node_connector.node) + network = cls(node_connector.node.currency, node_processor, connectors, node_connector.session) + return network + + @classmethod + def load(cls, app, currency, node_processor, blockchain_service, identities_service): + """ + Create a new network with all known nodes + + :param sakia.app.Application app: Sakia application + :param str currency: The currency of this service + :param sakia.data.processors.NodeProcessor node_processor: The nodes processor + :return: + """ + connectors = [] + for node in node_processor.nodes(currency): + connectors.append(NodeConnector(node, app.parameters)) + network = cls(app, currency, node_processor, connectors, blockchain_service, identities_service) + return network + + def start_coroutines(self): + """ + Start network nodes crawling + :return: + """ + if not self._discovery_loop_task: + self._discovery_loop_task = asyncio.ensure_future(self.discover_network()) + + def nodes(self): + """ + Get all nodes + :return: + """ + return self._processor.nodes(self.currency) + + def commit_node(self, node): + self._processor.commit_node(node) + + async def stop_coroutines(self, closing=False): + """ + Stop network nodes crawling. + """ + self._must_crawl = False + close_tasks = [] + self._logger.debug("Start closing") + for connector in self._connectors: + close_tasks.append(asyncio.ensure_future(connector.close_ws())) + self._logger.debug("Closing {0} websockets".format(len(close_tasks))) + if len(close_tasks) > 0: + await asyncio.wait(close_tasks, timeout=15) + self._logger.debug("Closed") + + def continue_crawling(self): + return self._must_crawl + + def _check_nodes_sync(self): + """ + Check nodes sync with the following rules : + 1 : The block of the majority + 2 : The more last different issuers + 3 : The more difficulty + 4 : The biggest number or timestamp + """ + online_nodes = self._processor.online_nodes(self.currency) + # rule number 1 : block of the majority + blocks = [n.current_buid.sha_hash for n in online_nodes if n.current_buid.sha_hash] + blocks_occurences = Counter(blocks) + blocks_by_occurences = {} + for key, value in blocks_occurences.items(): + the_block = [n.current_buid.sha_hash + for n in online_nodes if n.current_buid.sha_hash == key][0] + if value not in blocks_by_occurences: + blocks_by_occurences[value] = [the_block] + else: + blocks_by_occurences[value].append(the_block) + + if len(blocks_by_occurences) == 0: + for n in [n for n in online_nodes if n.state in (Node.ONLINE, Node.DESYNCED)]: + n.state = Node.ONLINE + self._processor.update_node(n) + return + + most_present = max(blocks_by_occurences.keys()) + + synced_block_hash = blocks_by_occurences[most_present][0] + + for n in online_nodes: + if n.current_buid.sha_hash == synced_block_hash: + n.state = Node.ONLINE + else: + n.state = Node.DESYNCED + self._processor.update_node(n) + + def add_connector(self, node_connector): + """ + Add a nod to the network. + """ + self._connectors.append(node_connector) + node_connector.changed.connect(self.handle_change) + node_connector.error.connect(self.handle_error) + node_connector.identity_changed.connect(self.handle_identity_change) + node_connector.neighbour_found.connect(self.handle_new_node) + self._logger.debug("{:} connected".format(node_connector.node.pubkey[:5])) + + @asyncify + async def refresh_once(self): + for connector in self._connectors: + await asyncio.sleep(1) + await connector.init_session() + connector.refresh(manual=True) + + async def discover_network(self): + """ + Start crawling which never stops. + To stop this crawling, call "stop_crawling" method. + """ + self._must_crawl = True + first_loop = True + asyncio.ensure_future(self.discovery_loop()) + while self.continue_crawling(): + for connector in self._connectors: + if self.continue_crawling(): + await connector.init_session() + connector.refresh() + if not first_loop: + await asyncio.sleep(15) + first_loop = False + await asyncio.sleep(15) + + self._logger.debug("End of network discovery") + + async def discovery_loop(self): + """ + Handle poping of nodes in discovery stack + :return: + """ + while self.continue_crawling(): + try: + await asyncio.sleep(1) + peer = self._discovery_stack.pop() + node = self._processor.update_peer(self.currency, peer) + if not node: + self._logger.debug("New node found : {0}".format(peer.pubkey[:5])) + try: + connector = NodeConnector.from_peer(self.currency, peer, self._app.parameters) + node = connector.node + self._processor.insert_node(connector.node) + await connector.init_session() + connector.refresh(manual=True) + self.add_connector(connector) + self.nodes_changed.emit() + except InvalidNodeCurrency as e: + self._logger.debug(str(e)) + if node: + try: + identity = await self._identities_service.find_from_pubkey(node.pubkey) + identity = await self._identities_service.load_requirements(identity) + node.member = identity.member + node.uid = identity.uid + self._processor.update_node(node) + self.nodes_changed.emit() + except errors.DuniterError as e: + self._logger.error(e.message) + + self._app.db.commit() + except IndexError: + await asyncio.sleep(2) + + def handle_new_node(self, peer): + key = VerifyingKey(peer.pubkey) + if key.verify_document(peer): + if len(self._discovery_stack) < 1000 \ + and peer.signatures[0] not in [p.signatures[0] for p in self._discovery_stack]: + self._logger.debug("Stacking new peer document : {0}".format(peer.pubkey)) + self._discovery_stack.append(peer) + else: + self._logger.debug("Wrong document received : {0}".format(peer.signed_raw())) + + @pyqtSlot() + def handle_identity_change(self): + connector = self.sender() + self._processor.update_node(connector.node) + self.nodes_changed.emit() + + @pyqtSlot() + def handle_error(self): + node = self.sender() + if node.state in (Node.OFFLINE, Node.CORRUPTED) and \ + node.last_change + 3600 < time.time(): + node.disconnect() + self._processor.delete_node(node) + self.nodes_changed.emit() + + def handle_change(self): + node_connector = self.sender() + + if node_connector.node.state in (Node.ONLINE, Node.DESYNCED): + self._check_nodes_sync() + self.nodes_changed.emit() + self._processor.update_node(node_connector.node) + + if node_connector.node.state == Node.ONLINE: + current_buid = self._processor.current_buid(self.currency) + self._logger.debug("{0} -> {1}".format(self._block_found.sha_hash[:10], current_buid.sha_hash[:10])) + if self._block_found.sha_hash != current_buid.sha_hash: + self._logger.debug("Latest block changed : {0}".format(current_buid.number)) + # If new latest block is lower than the previously found one + # or if the previously found block is different locally + # than in the main chain, we declare a rollback + if current_buid < self._block_found \ + or node_connector.node.previous_buid != self._block_found: + self._logger.debug("Start rollback") + self._block_found = current_buid + #TODO: self._blockchain_service.rollback() + else: + self._logger.debug("Start refresh") + self._block_found = current_buid + asyncio.ensure_future(self._blockchain_service.handle_blockchain_progress(self._block_found)) diff --git a/src/sakia/services/sources.py b/src/sakia/services/sources.py new file mode 100644 index 0000000000000000000000000000000000000000..fe082a28252da85dd09e59041c64f3e3f80c8939 --- /dev/null +++ b/src/sakia/services/sources.py @@ -0,0 +1,47 @@ +from PyQt5.QtCore import QObject +from duniterpy.api import bma, errors +import logging +from sakia.data.entities import Source + + +class SourcesServices(QObject): + """ + Source service is managing sources received + to update data locally + """ + def __init__(self, currency, sources_processor, connections_processor, bma_connector): + """ + Constructor the identities service + + :param str currency: The currency name of the community + :param sakia.data.processors.SourcesProcessor sources_processor: the sources processor for given currency + :param sakia.data.processors.ConnectionsProcessor connections_processor: the connections processor + :param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API + """ + super().__init__() + self._sources_processor = sources_processor + self._connections_processor = connections_processor + self._bma_connector = bma_connector + self.currency = currency + self._logger = logging.getLogger('sakia') + + def amount(self, pubkey): + return self._sources_processor.amount(self.currency, pubkey) + + async def refresh_sources(self): + connections_pubkeys = [c.pubkey for c in self._connections_processor.connections_to(self.currency)] + for pubkey in connections_pubkeys: + sources_data = await self._bma_connector.get(self.currency, bma.tx.sources, + req_args={'pubkey': pubkey}) + + self._logger.debug("Found {0} sources".format(len(sources_data['sources']))) + self._sources_processor.drop_all_of(currency=self.currency, pubkey=pubkey) + for i, s in enumerate(sources_data['sources']): + source = Source(currency=self.currency, pubkey=pubkey, + identifier=s['identifier'], + type=s['type'], + noffset=s['noffset'], + amount=s['amount'], + base=s['base']) + self._sources_processor.commit(source) + self._logger.debug("{0}/{1} sources".format(i, len(sources_data['sources']))) diff --git a/src/sakia/services/transactions.py b/src/sakia/services/transactions.py new file mode 100644 index 0000000000000000000000000000000000000000..c537d333bad69ffbd1450e88ebbbf0ff2fda7b85 --- /dev/null +++ b/src/sakia/services/transactions.py @@ -0,0 +1,141 @@ +from PyQt5.QtCore import QObject +from sakia.data.entities.transaction import parse_transaction_doc +from duniterpy.documents import Transaction as TransactionDoc +from duniterpy.documents import SimpleTransaction +from sakia.data.entities import Dividend +from duniterpy.api import bma +import logging +import sqlite3 + + +class TransactionsService(QObject): + """ + Transaction service is managing sources received + to update data locally + """ + def __init__(self, currency, transactions_processor, dividends_processor, + identities_processor, connections_processor, bma_connector): + """ + Constructor the identities service + + :param str currency: The currency name of the community + :param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor for given currency + :param sakia.data.processors.TransactionsProcessor transactions_processor: the transactions processor for given currency + :param sakia.data.processors.DividendsProcessor dividends_processor: the dividends processor for given currency + :param sakia.data.processors.ConnectionsProcessor connections_processor: the connections processor for given currency + :param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API + """ + super().__init__() + self._transactions_processor = transactions_processor + self._dividends_processor = dividends_processor + self._identities_processor = identities_processor + self._connections_processor = connections_processor + self._bma_connector = bma_connector + self.currency = currency + self._logger = logging.getLogger('sakia') + + def _parse_block(self, block_doc, txid): + """ + Parse a block + :param duniterpy.documents.Block block_doc: The block + :param int txid: Latest tx id + :return: The list of transfers sent + """ + transfers_changed = [] + new_transfers = [] + for tx in [t for t in self._transactions_processor.awaiting(self.currency)]: + if self._transactions_processor.run_state_transitions(tx, block_doc): + transfers_changed.append(tx) + + new_transactions = [t for t in block_doc.transactions + if not self._transactions_processor.find_by_hash(t.sha_hash) + and SimpleTransaction.is_simple(t)] + connections_pubkeys = [c.pubkey for c in self._connections_processor.connections_to(self.currency)] + for pubkey in connections_pubkeys: + for (i, tx_doc) in enumerate(new_transactions): + tx = parse_transaction_doc(tx_doc, pubkey, block_doc.blockUID.number, block_doc.mediantime, txid+i) + if tx: + new_transfers.append(tx) + self._transactions_processor.commit(tx) + else: + logging.debug("Error during transfer parsing") + + return transfers_changed, new_transfers + + async def handle_new_blocks(self, blocks): + """ + Refresh last transactions + + :param list[duniterpy.documents.Block] blocks: The blocks containing data to parse + """ + self._logger.debug("Refresh transactions") + transfers_changed = [] + new_transfers = [] + txid = 0 + for block in blocks: + changes, new_tx = self._parse_block(block, txid) + txid += len(new_tx) + transfers_changed += changes + new_transfers += new_tx + new_dividends = await self.parse_dividends_history(blocks, new_transfers) + return transfers_changed, new_transfers, new_dividends + + async def parse_dividends_history(self, blocks, transactions): + """ + Request transactions from the network to initialize data for a given pubkey + :param List[duniterpy.documents.Block] blocks: the list of transactions found by tx parsing + :param List[sakia.data.entities.Transaction] transactions: the list of transactions found by tx parsing + """ + connections_pubkeys = [c.pubkey for c in self._connections_processor.connections_to(self.currency)] + min_block_number = blocks[0].number + dividends = [] + for pubkey in connections_pubkeys: + history_data = await self._bma_connector.get(self.currency, bma.ud.history, + req_args={'pubkey': pubkey}) + block_numbers = [] + for ud_data in history_data["history"]["history"]: + dividend = Dividend(currency=self.currency, + pubkey=pubkey, + block_number=ud_data["block_number"], + timestamp=ud_data["time"], + amount=ud_data["amount"], + base=ud_data["base"]) + if dividend.block_number > min_block_number: + self._logger.debug("Dividend of block {0}".format(dividend.block_number)) + block_numbers.append(dividend.block_number) + if self._dividends_processor.commit(dividend): + dividends.append(dividend) + + for tx in transactions: + txdoc = TransactionDoc.from_signed_raw(tx.raw) + for input in txdoc.inputs: + if input.source == "D" and input.origin_id == pubkey and input.index not in block_numbers: + block = next((b for b in blocks if b.number == input.index)) + dividend = Dividend(currency=self.currency, + pubkey=pubkey, + block_number=input.index, + timestamp=block.mediantime, + amount=block.ud, + base=block.unit_base) + self._logger.debug("Dividend of block {0}".format(dividend.block_number)) + if self._dividends_processor.commit(dividend): + dividends.append(dividend) + return dividends + + def transfers(self, pubkey): + """ + Get all transfers from or to a given pubkey + :param str pubkey: + :return: the list of Transaction entities + :rtype: List[sakia.data.entities.Transaction] + """ + return self._transactions_processor.transfers(self.currency, pubkey) + + def dividends(self, pubkey): + """ + Get all dividends from or to a given pubkey + :param str pubkey: + :return: the list of Dividend entities + :rtype: List[sakia.data.entities.Dividend] + """ + return self._dividends_processor.dividends(self.currency, pubkey) \ No newline at end of file diff --git a/src/sakia/tests/__init__.py b/src/sakia/tests/__init__.py deleted file mode 100644 index a7d4d00287516418923b1e4dbef7ff519ded3ed1..0000000000000000000000000000000000000000 --- a/src/sakia/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .quamash_utils import QuamashTest \ No newline at end of file diff --git a/src/sakia/tests/functional/certification/test_certification.py b/src/sakia/tests/functional/certification/test_certification.py deleted file mode 100644 index d1615149433bb51c866e5cc7aeada594339db847..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/certification/test_certification.py +++ /dev/null @@ -1,90 +0,0 @@ -import sys -import unittest -import asyncio -import time -import logging -import aiohttp -from duniterpy.documents.peer import BMAEndpoint -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QApplication -from PyQt5.QtCore import QLocale, Qt -from PyQt5.QtTest import QTest -from sakia.tests.mocks.bma import init_new_community -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.certification import CertificationDialog, Ui_CertificationDialog -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.core.app import Application -from sakia.core import Account, Community, Wallet -from sakia.core.net import Network, Node -from sakia.core.net.api.bma.access import BmaAccess -from sakia.tests import QuamashTest -from duniterpy.key import ScryptParams - - -class TestCertificationDialog(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.mock_new_community = init_new_community.get_mock(self.lp) - - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50010) - self.node = Node(self.mock_new_community.peer(), - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - None, Node.ONLINE, - time.time(), {}, "duniter", "0.14.0", 0, session=aiohttp.ClientSession()) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - ScryptParams(4096, 16, 1), - "john", [self.community], [self.wallet], [], self.identities_registry) - self.account.notifications['warning_certifying_first_time'] = False - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_certification_init_community(self): - time.sleep(2) - certification_dialog = CertificationDialog(self.application, - self.account, - self.password_asker, - QDialog(), - Ui_CertificationDialog()) - - async def open_dialog(certification_dialog): - srv, port, url = await self.mock_new_community.create_server() - self.addCleanup(srv.close) - await certification_dialog.async_exec() - await self.mock_new_community.close() - - def close_dialog(): - if certification_dialog.widget.isVisible(): - certification_dialog.widget.close() - - async def exec_test(): - await asyncio.sleep(1) - QTest.mouseClick(certification_dialog.ui.radio_pubkey, Qt.LeftButton) - QTest.keyClicks(certification_dialog.ui.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") - QTest.mouseClick(certification_dialog.ui.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) - await asyncio.sleep(3) - topWidgets = QApplication.topLevelWidgets() - for w in topWidgets: - if type(w) is QMessageBox: - QTest.keyClick(w, Qt.Key_Enter) - - self.lp.call_later(10, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(open_dialog(certification_dialog)) diff --git a/src/sakia/tests/functional/identities_tab/test_identities_table.py b/src/sakia/tests/functional/identities_tab/test_identities_table.py deleted file mode 100644 index 55389c795911351c0c8732a8f1d95774951610c7..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/identities_tab/test_identities_table.py +++ /dev/null @@ -1,88 +0,0 @@ -import sys -import unittest -import asyncio -import aiohttp -import logging -import time -from PyQt5.QtCore import QLocale, Qt -from PyQt5.QtTest import QTest - -from sakia.tests.mocks.bma import nice_blockchain -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.identities_tab import IdentitiesTabWidget -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.core.app import Application -from sakia.core import Account, Community, Wallet -from sakia.core.net import Network, Node -from sakia.core.net.api.bma.access import BmaAccess -from sakia.tests import QuamashTest -from duniterpy.key import ScryptParams - - -class TestIdentitiesTable(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry() - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) - self.node = Node(self.mock_nice_blockchain.peer(), - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - None, Node.ONLINE, - time.time(), {}, "duniter", "0.14.0", 0, session=aiohttp.ClientSession()) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - ScryptParams(4096, 16, 1), - "john", [self.community], [self.wallet], [], self.identities_registry) - - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_search_identity_found(self): - time.sleep(2) - identities_tab = IdentitiesTabWidget(self.application) - future = asyncio.Future() - - def open_widget(): - identities_tab.widget.show() - return future - - def close_dialog(): - if identities_tab.widget.isVisible(): - identities_tab.widget.close() - future.set_result(True) - - async def exec_test(): - srv, port, url = await self.mock_nice_blockchain.create_server() - self.addCleanup(srv.close) - - identities_tab.change_account(self.account, self.password_asker) - identities_tab.change_community(self.community) - await asyncio.sleep(1) - - QTest.keyClicks(identities_tab.ui.edit_textsearch, "doe") - QTest.mouseClick(identities_tab.ui.button_search, Qt.LeftButton) - await asyncio.sleep(2) - - self.assertEqual(identities_tab.ui.table_identities.model().rowCount(), 1) - await self.mock_nice_blockchain.close() - self.lp.call_soon(close_dialog) - - asyncio.ensure_future(exec_test()) - self.lp.call_later(15, close_dialog) - self.lp.run_until_complete(open_widget()) diff --git a/src/sakia/tests/functional/main_window/test_main_window_dialogs.py b/src/sakia/tests/functional/main_window/test_main_window_dialogs.py deleted file mode 100644 index ed9157a9816b196643e7ed133ec8d2a053c53779..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/main_window/test_main_window_dialogs.py +++ /dev/null @@ -1,80 +0,0 @@ -import unittest -import asyncio -from PyQt5.QtWidgets import QDialog, QFileDialog -from PyQt5.QtCore import QLocale, QTimer -from sakia.gui.mainwindow import MainWindow -from sakia.core.app import Application -from sakia.tests import QuamashTest -from sakia.core.registry.identities import IdentitiesRegistry - - -class MainWindowDialogsTest(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - self.application = Application(self.qapplication, self.lp, IdentitiesRegistry()) - self.main_window = MainWindow.startup(self.application) - - def tearDown(self): - self.main_window.widget.close() - self.tearDownQuamash() - - def test_action_about(self): - #select menu - self.main_window.ui.actionAbout.trigger() - widgets = self.qapplication.topLevelWidgets() - for widget in widgets: - if isinstance(widget, QDialog): - if widget.isVisible(): - self.assertEqual('AboutPopup', widget.objectName()) - widget.close() - break - - def test_action_add_account(self): - async def exec_test(): - self.main_window.ui.action_add_account.trigger() - await asyncio.sleep(1) - widgets = self.qapplication.topLevelWidgets() - for widget in widgets: - if isinstance(widget, QDialog): - if widget.isVisible(): - try: - self.assertEqual('AccountConfigurationDialog', widget.objectName()) - break - finally: - widget.close() - self.lp.run_until_complete(exec_test()) - - # fixme: require a app.current_account fixture - # def test_action_configure_account(self): - # # asynchronous test, cause dialog is waiting user response - # QTimer.singleShot(0, self._async_test_action_configure_account) - # # select about menu - # self.main_window.action_configure_parameters.trigger() - # - # def _async_test_action_configure_account(self): - # widgets = qapplication.topLevelWidgets() - # for widget in widgets: - # if isinstance(widget, PyQt5.QtWidgets.QDialog): - # self.assertEqual(widget.objectName(), 'AccountConfigurationDialog') - # self.assertEqual(widget.isVisible(), True) - # widget.close() - # break - # - def test_action_export_account(self): - #select menu - self.main_window.ui.action_export.trigger() - - widgets = self.qapplication.topLevelWidgets() - for widget in widgets: - if isinstance(widget, QFileDialog): - if widget.isVisible(): - try: - self.assertEqual('ExportFileDialog', widget.objectName()) - break - finally: - widget.close() - -if __name__ == '__main__': - unittest.main() diff --git a/src/sakia/tests/functional/main_window/test_main_window_menus.py b/src/sakia/tests/functional/main_window/test_main_window_menus.py deleted file mode 100644 index 7022217db7230392c1725c0140f58afe3af88245..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/main_window/test_main_window_menus.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys -import unittest -import os -import asyncio -import quamash -from PyQt5.QtWidgets import QMenu -from PyQt5.QtCore import QLocale -from sakia.gui.mainwindow import MainWindow -from sakia.core.app import Application -from sakia.tests import QuamashTest - -class MainWindowMenusTest(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - self.application = Application(self.qapplication, self.lp, None) - self.main_window = MainWindow.startup(self.application) - - def tearDown(self): - self.main_window.widget.close() - self.tearDownQuamash() - - def test_menubar(self): - children = self.main_window.ui.menubar.children() - menus = [] - """:type: list[QMenu]""" - for child in children: - if isinstance(child, QMenu): - menus.append(child) - self.assertEqual(len(menus), 4) - self.assertEqual(menus[0].objectName(), 'menu_file') - self.assertEqual(menus[1].objectName(), 'menu_account') - self.assertEqual(menus[2].objectName(), 'menu_help') - self.assertEqual(menus[3].objectName(), 'menu_duniter') - - def test_menu_account(self): - actions = self.main_window.ui.menu_account.actions() - """:type: list[QAction]""" - self.assertEqual('action_configure_parameters', actions[1].objectName()) - self.assertEqual('action_add_account', actions[2].objectName()) - self.assertEqual('actionCertification', actions[4].objectName()) - self.assertEqual('actionTransfer_money', actions[5].objectName()) - self.assertEqual('action_add_a_contact', actions[7].objectName()) - self.assertEqual(11, len(actions)) - - def test_menu_actions(self): - actions = self.main_window.ui.menu_help.actions() - """:type: list[QAction]""" - self.assertEqual(len(actions), 1) - self.assertEqual(actions[0].objectName(), 'actionAbout') - -if __name__ == '__main__': - unittest.main() diff --git a/src/sakia/tests/functional/preferences/test_preferences_dialog.py b/src/sakia/tests/functional/preferences/test_preferences_dialog.py deleted file mode 100644 index f2f1ce3bbbc99572818c07a34c4530fe465bf325..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/preferences/test_preferences_dialog.py +++ /dev/null @@ -1,52 +0,0 @@ -import sys -import unittest -import asyncio -import quamash -import logging -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.preferences import PreferencesDialog -from sakia.core.app import Application -from sakia.tests import QuamashTest -from duniterpy.api import bma - - -class TestPreferencesDialog(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - def tearDown(self): - self.tearDownQuamash() - - def test_preferences_default(self): - self.application = Application(self.qapplication, self.lp, self.identities_registry) - preferences_dialog = PreferencesDialog(self.application) - self.assertEqual(preferences_dialog.combo_account.currentText(), - self.application.preferences['account']) - self.assertEqual(preferences_dialog.combo_language.currentText(), - self.application.preferences['lang']) - self.assertEqual(preferences_dialog.combo_referential.currentIndex(), - self.application.preferences['ref']) - self.assertEqual(preferences_dialog.checkbox_expertmode.isChecked(), - self.application.preferences['expert_mode']) - self.assertEqual(preferences_dialog.checkbox_maximize.isChecked(), - self.application.preferences['maximized']) - self.assertEqual(preferences_dialog.checkbox_notifications.isChecked(), - self.application.preferences['notifications']) - self.assertEqual(preferences_dialog.checkbox_proxy.isChecked(), - self.application.preferences['enable_proxy']) - self.assertEqual(preferences_dialog.edit_proxy_address.text(), - self.application.preferences['proxy_address']) - self.assertEqual(preferences_dialog.spinbox_proxy_port.value(), - self.application.preferences['proxy_port']) - self.assertEqual(preferences_dialog.checkbox_international_system.isChecked(), - self.application.preferences['international_system_of_units']) - self.assertEqual(preferences_dialog.checkbox_auto_refresh.isChecked(), - self.application.preferences['auto_refresh']) - -if __name__ == '__main__': - logging.basicConfig(stream=sys.stderr) - logging.getLogger().setLevel(logging.DEBUG) - unittest.main() diff --git a/src/sakia/tests/functional/process_cfg_account/test_add_account.py b/src/sakia/tests/functional/process_cfg_account/test_add_account.py deleted file mode 100644 index d5afff6e96935f24ee8a1015be9d3995e42cab8b..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/process_cfg_account/test_add_account.py +++ /dev/null @@ -1,95 +0,0 @@ -import sys -import unittest -import asyncio -import logging -from duniterpy.key import ScryptParams -from PyQt5.QtCore import QLocale, Qt -from PyQt5.QtTest import QTest -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.process_cfg_account import ProcessConfigureAccount -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.core.app import Application -from sakia.core.account import Account -from sakia.tests import QuamashTest - - -class ProcessAddCommunity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "B7J4sopyfqzi3uh4Gzsdnp1XHc87NaxY7rqW2exgivCa", - ScryptParams(4096, 16, 1), - "test", [], [], [], self.identities_registry) - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_create_account(self): - process_account = ProcessConfigureAccount(self.application, - None) - - def close_dialog(): - if process_account.isVisible(): - process_account.close() - - async def exec_test(): - QTest.keyClicks(process_account.edit_account_name, "test") - self.assertEqual(process_account.stacked_pages.currentWidget(), - process_account.page_init, - msg="Current widget : {0}".format(process_account.stacked_pages.currentWidget().objectName())) - QTest.mouseClick(process_account.button_next, Qt.LeftButton) - await asyncio.sleep(1) - - self.assertEqual(process_account.stacked_pages.currentWidget(), - process_account.page_gpg, - msg="Current widget : {0}".format(process_account.stacked_pages.currentWidget().objectName())) - QTest.keyClicks(process_account.edit_salt, "testsakia") - self.assertFalse(process_account.button_next.isEnabled()) - self.assertFalse(process_account.button_generate.isEnabled()) - QTest.keyClicks(process_account.edit_password, "testsakia") - self.assertFalse(process_account.button_next.isEnabled()) - self.assertFalse(process_account.button_generate.isEnabled()) - QTest.keyClicks(process_account.edit_password_repeat, "wrongpassword") - self.assertFalse(process_account.button_next.isEnabled()) - self.assertFalse(process_account.button_generate.isEnabled()) - process_account.edit_password_repeat.setText("") - QTest.keyClicks(process_account.edit_password_repeat, "testsakia") - self.assertTrue(process_account.button_next.isEnabled()) - self.assertTrue(process_account.button_generate.isEnabled()) - QTest.mouseClick(process_account.button_generate, Qt.LeftButton) - self.assertEqual(process_account.label_info.text(), - "B7J4sopyfqzi3uh4Gzsdnp1XHc87NaxY7rqW2exgivCa") - QTest.mouseClick(process_account.button_next, Qt.LeftButton) - await asyncio.sleep(1) - - self.assertEqual(process_account.stacked_pages.currentWidget(), - process_account.page__communities, - msg="Current widget : {0}".format(process_account.stacked_pages.currentWidget().objectName())) - process_account.password_asker.password = "testsakia" - process_account.password_asker.remember = True - await asyncio.sleep(1) - QTest.mouseClick(process_account.button_next, Qt.LeftButton) - self.assertEqual(len(self.application.accounts), 1) - await asyncio.sleep(0.1) - self.assertEqual(self.application.current_account.name, "test") - self.assertEqual(self.application.preferences['account'], "test") - self.assertEqual(len(self.application.current_account.wallets), 1) - await asyncio.sleep(1) - - self.lp.call_later(10, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(process_account.async_exec()) - -if __name__ == '__main__': - logging.basicConfig( stream=sys.stderr ) - logging.getLogger().setLevel( logging.DEBUG ) - unittest.main() diff --git a/src/sakia/tests/functional/process_cfg_community/test_add_community.py b/src/sakia/tests/functional/process_cfg_community/test_add_community.py deleted file mode 100644 index fc47726728f4f0537b8fe8b5a7797b31ccc1f7db..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/process_cfg_community/test_add_community.py +++ /dev/null @@ -1,238 +0,0 @@ -import sys -import unittest -import asyncio -import quamash -import logging -import time -from PyQt5.QtWidgets import QDialog -from PyQt5.QtCore import QLocale, Qt -from PyQt5.QtTest import QTest -from sakia.tests.mocks.bma import new_blockchain, nice_blockchain -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.process_cfg_community import ProcessConfigureCommunity -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.core.app import Application -from sakia.core.account import Account -from sakia.tests import QuamashTest -from duniterpy.key import ScryptParams - - -class ProcessAddCommunity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - ScryptParams(4096, 16, 1), - "john", [], [], [], self.identities_registry) - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_register_community_empty_blockchain(self): - mock = new_blockchain.get_mock(self.lp) - time.sleep(2) - process_community = ProcessConfigureCommunity(self.application, - self.account, - None, self.password_asker) - - def close_dialog(): - if process_community.isVisible(): - process_community.close() - - async def exec_test(): - srv, port, url = await mock.create_server() - self.addCleanup(srv.close) - await asyncio.sleep(1) - QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) - QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") - QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) - process_community.spinbox_port.setValue(port) - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_node, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") - self.assertEqual(process_community.spinbox_port.value(), port) - QTest.mouseClick(process_community.button_register, Qt.LeftButton) - await asyncio.sleep(1) - self.assertEqual(process_community.label_error.text(), "Broadcasting identity...") - await asyncio.sleep(1) - - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_add_nodes, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - await mock.close() - QTest.mouseClick(process_community.button_next, Qt.LeftButton) - - self.lp.call_later(15, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(process_community.async_exec()) - self.assertEqual(process_community.result(), QDialog.Accepted) - - def test_connect_community_empty_blockchain(self): - mock = new_blockchain.get_mock(self.lp) - time.sleep(2) - process_community = ProcessConfigureCommunity(self.application, - self.account, - None, self.password_asker) - - def close_dialog(): - if process_community.isVisible(): - process_community.close() - - async def exec_test(): - srv, port, url = await mock.create_server() - self.addCleanup(srv.close) - - await asyncio.sleep(1) - QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) - QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") - QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) - process_community.spinbox_port.setValue(port) - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_node, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") - self.assertEqual(process_community.spinbox_port.value(), port) - QTest.mouseClick(process_community.button_connect, Qt.LeftButton) - await asyncio.sleep(2) - self.assertEqual(mock.get_request(0).method, 'GET') - self.assertEqual(mock.get_request(0).url, '/network/peering') - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_node, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - self.assertEqual(process_community.label_error.text(), "Could not find your identity on the network.") - await mock.close() - process_community.close() - - self.lp.call_later(15, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(process_community.async_exec()) - - def test_connect_community_wrong_pubkey(self): - mock = nice_blockchain.get_mock(self.lp) - time.sleep(2) - self.account.pubkey = "wrong_pubkey" - process_community = ProcessConfigureCommunity(self.application, - self.account, - None, self.password_asker) - - def close_dialog(): - if process_community.isVisible(): - process_community.close() - - async def exec_test(): - srv, port, url = await mock.create_server() - self.addCleanup(srv.close) - await asyncio.sleep(1) - QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) - QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") - QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) - process_community.spinbox_port.setValue(port) - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_node, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") - self.assertEqual(process_community.spinbox_port.value(), port) - QTest.mouseClick(process_community.button_connect, Qt.LeftButton) - await asyncio.sleep(1) - self.assertEqual(mock.get_request(0).method, 'GET') - self.assertEqual(mock.get_request(0).url, '/network/peering') - self.assertEqual(mock.get_request(1).method, 'GET') - self.assertEqual(mock.get_request(1).url, - '/wot/certifiers-of/wrong_pubkey') - self.assertEqual(process_community.label_error.text(), """Your pubkey or UID is different on the network. -Yours : wrong_pubkey, the network : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ""") - await mock.close() - process_community.close() - - self.lp.call_later(15, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(process_community.async_exec()) - self.assertEqual(process_community.result(), QDialog.Rejected) - - def test_connect_community_wrong_uid(self): - mock = nice_blockchain.get_mock(self.lp) - time.sleep(2) - self.account.name = "wrong_uid" - process_community = ProcessConfigureCommunity(self.application, - self.account, - None, self.password_asker) - - def close_dialog(): - if process_community.isVisible(): - process_community.close() - - async def exec_test(): - srv, port, url = await mock.create_server() - self.addCleanup(srv.close) - await asyncio.sleep(1) - QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) - QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") - QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) - process_community.spinbox_port.setValue(port) - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_node, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") - self.assertEqual(process_community.spinbox_port.value(), port) - QTest.mouseClick(process_community.button_connect, Qt.LeftButton) - await asyncio.sleep(1) - self.assertEqual(mock.get_request(0).method, 'GET') - self.assertEqual(mock.get_request(0).url, '/network/peering') - self.assertEqual(process_community.label_error.text(), """Your pubkey or UID is different on the network. -Yours : wrong_uid, the network : john""") - await mock.close() - process_community.close() - - self.lp.call_later(15, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(process_community.async_exec()) - self.assertEqual(process_community.result(), QDialog.Rejected) - - def test_connect_community_success(self): - mock = nice_blockchain.get_mock(self.lp) - time.sleep(2) - process_community = ProcessConfigureCommunity(self.application, - self.account, - None, self.password_asker) - - def close_dialog(): - if process_community.isVisible(): - process_community.close() - - async def exec_test(): - srv, port, url = await mock.create_server() - QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) - QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") - QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) - process_community.spinbox_port.setValue(port) - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_node, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") - self.assertEqual(process_community.spinbox_port.value(), port) - QTest.mouseClick(process_community.button_connect, Qt.LeftButton) - await asyncio.sleep(1) - self.assertEqual(mock.get_request(0).method, 'GET') - self.assertEqual(mock.get_request(0).url, '/network/peering') - self.assertEqual(mock.get_request(1).method, 'GET') - self.assertEqual(mock.get_request(1).url, - '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') - self.assertEqual(process_community.stacked_pages.currentWidget(), - process_community.page_add_nodes, - msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) - await mock.close() - QTest.mouseClick(process_community.button_next, Qt.LeftButton) - - self.lp.call_later(15, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(process_community.async_exec()) \ No newline at end of file diff --git a/src/sakia/tests/functional/transfer/test_transfer.py b/src/sakia/tests/functional/transfer/test_transfer.py deleted file mode 100644 index 3d5e010a7c47d5d43a03fdcd1fde9a655fb6b64c..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/transfer/test_transfer.py +++ /dev/null @@ -1,92 +0,0 @@ -import sys -import unittest -import asyncio -import aiohttp -import time -import logging -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QApplication -from PyQt5.QtCore import QLocale, Qt -from PyQt5.QtTest import QTest - -from sakia.tests.mocks.bma import nice_blockchain -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.transfer import TransferMoneyDialog -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.core.app import Application -from sakia.core import Account, Community, Wallet -from sakia.core.net import Network, Node -from sakia.core.net.api.bma.access import BmaAccess -from sakia.tests import QuamashTest -from duniterpy.key import ScryptParams - - -class TestTransferDialog(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) - self.node = Node(self.mock_nice_blockchain.peer(), - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - None, Node.ONLINE, - time.time(), {}, "duniter", "0.14.0", 0, session=aiohttp.ClientSession()) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - ScryptParams(4096, 16, 1), - "john", [self.community], [self.wallet], [], self.identities_registry) - - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_transfer_nice_community(self): - transfer_dialog = TransferMoneyDialog(self.application, - self.account, - self.password_asker, - self.community, - None) - self.account.wallets[0].init_cache(self.application, self.community) - - async def open_dialog(transfer_dialog): - srv, port, url = await self.mock_nice_blockchain.create_server() - self.addCleanup(srv.close) - await asyncio.sleep(1) - result = await transfer_dialog.async_exec() - await self.mock_nice_blockchain.close() - self.assertEqual(result, QDialog.Accepted) - - def close_dialog(): - if transfer_dialog.widget.isVisible(): - transfer_dialog.widget.close() - - async def exec_test(): - self.account.wallets[0].caches[self.community.currency].available_sources = await self.wallet.sources(self.community) - QTest.mouseClick(transfer_dialog.ui.radio_pubkey, Qt.LeftButton) - QTest.keyClicks(transfer_dialog.ui.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") - transfer_dialog.ui.spinbox_amount.setValue(10) - QTest.mouseClick(transfer_dialog.ui.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) - await asyncio.sleep(1) - topWidgets = QApplication.topLevelWidgets() - for w in topWidgets: - if type(w) is QMessageBox: - QTest.keyClick(w, Qt.Key_Enter) - await asyncio.sleep(1) - - self.lp.call_later(30, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(open_dialog(transfer_dialog)) diff --git a/src/sakia/tests/functional/wot_tab/__init__.py b/src/sakia/tests/functional/wot_tab/__init__.py deleted file mode 100644 index 39ab2a0b56350baad834cb7fb0cfecb8223e1fcd..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/wot_tab/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'inso' diff --git a/src/sakia/tests/functional/wot_tab/test_wot_tab.py b/src/sakia/tests/functional/wot_tab/test_wot_tab.py deleted file mode 100644 index 90f1ace2e479bdc96ffbc11db706f3c692678d23..0000000000000000000000000000000000000000 --- a/src/sakia/tests/functional/wot_tab/test_wot_tab.py +++ /dev/null @@ -1,86 +0,0 @@ -import asyncio -import logging -import sys -import time -import aiohttp -import unittest - -from PyQt5.QtCore import QLocale -from duniterpy.key import ScryptParams -from sakia.core import Account, Community, Wallet -from sakia.core.app import Application -from sakia.core.net import Network, Node -from sakia.core.net.api.bma.access import BmaAccess -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.graphs.wot_tab import WotTabWidget -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.tests import QuamashTest -from sakia.tests.mocks.bma import nice_blockchain - - -class TestWotTab(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry() - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) - self.node = Node(self.mock_nice_blockchain.peer(), - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - None, Node.ONLINE, - time.time(), {}, "duniter", "0.14.0", 0, session=aiohttp.ClientSession()) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - ScryptParams(4096, 16, 1), - "john", [self.community], [self.wallet], [], self.identities_registry) - - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_empty_wot_tab(self): - wot_tab = WotTabWidget(self.application) - future = asyncio.Future() - - def open_widget(): - wot_tab.widget.show() - return future - - async def async_open_widget(): - srv, port, url = await self.mock_nice_blockchain.create_server() - self.addCleanup(srv.close) - await open_widget() - await self.mock_nice_blockchain.close() - - def close_dialog(): - if wot_tab.widget.isVisible(): - wot_tab.widget.close() - future.set_result(True) - - async def exec_test(): - await asyncio.sleep(1) - self.assertTrue(wot_tab.widget.isVisible()) - self.lp.call_soon(close_dialog) - - asyncio.ensure_future(exec_test()) - self.lp.call_later(15, close_dialog) - self.lp.run_until_complete(async_open_widget()) - -if __name__ == '__main__': - logging.basicConfig( stream=sys.stderr ) - logging.getLogger().setLevel( logging.DEBUG ) - unittest.main() diff --git a/src/sakia/tests/mocks/bma/__init__.py b/src/sakia/tests/mocks/bma/__init__.py deleted file mode 100644 index 7e4cdeb7cb59ef2876f24361d303fcc281ec4ef8..0000000000000000000000000000000000000000 --- a/src/sakia/tests/mocks/bma/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'ggoinvic' diff --git a/src/sakia/tests/mocks/bma/corrupted.py b/src/sakia/tests/mocks/bma/corrupted.py deleted file mode 100644 index a147e884635086bb3ded3ee2f3d76008c4939b9f..0000000000000000000000000000000000000000 --- a/src/sakia/tests/mocks/bma/corrupted.py +++ /dev/null @@ -1,29 +0,0 @@ -import json -from ..server import MockServer - -bma_memberships_empty_array = { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uid": "john", - "sigDate": 123456789, - "memberships": [ ] -} - - -bma_null_data = { - "certifications": [ - { - "written": { - }, - }, - { - "written": None, - } - ] -} - -def get_mock(loop): - mock = MockServer(loop) - - mock.add_route('GET', '/blockchain/memberships/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_memberships_empty_array) - - return mock diff --git a/src/sakia/tests/mocks/bma/init_new_community.py b/src/sakia/tests/mocks/bma/init_new_community.py deleted file mode 100644 index af732fd1caab4b2b26a35dc3714e35cd47ff1a52..0000000000000000000000000000000000000000 --- a/src/sakia/tests/mocks/bma/init_new_community.py +++ /dev/null @@ -1,121 +0,0 @@ -from ..server import MockServer -from duniterpy.api import errors - -bma_lookup_test_john = { - "partial": False, - "results": [ - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uids": [ - { - "uid": "john", - "meta": { - "timestamp": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" - }, - "self": "ZrHK0cCqrxWReROK0ciiSb45+dRphJa68qFaSjdve8bBdnGAu7+DIu0d+u/fXrNRXuObihOKMBIawaIVPNHqDw==", - "others": [], - "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", - "revoked": False, - } - ], - "signed": [] - } - ] -} - -bma_lookup_test_doe = { - "partial": False, - "results": [ - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "uids": [ - { - "uid": "doe", - "meta": { - "timestamp": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" - }, - "self": "cIkHPQQ5+xTb4cKWv85rcYcZT+E3GDtX8B2nCK9Vs12p2Yz4bVaZiMvBBwisAAy2WBOaqHS3ydpXGtADchOICw==", - "others": [], - "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", - "revoked": False, - } - ], - "signed": [] - } - ] -} - -bma_lookup_test_patrick = { - "partial": False, - "results": [ - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "uids": [ - { - "uid": "patrick", - "meta": { - "timestamp": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" - }, - "self": "QNX2HDAxcHawc47TnMqb5/ou2lwa+zYOyeNk0a52dQDJX/NWmeTzGfTjdCtjpXmSCuPSg0F1mOnLQVd60xAzDA==", - "others": [], - "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", - "revoked": False, - } - ], - "signed": [] - } - ] -} - -bma_parameters = { - "currency": "test_currency", - "c": 0.1, - "dt": 86400, - "ud0": 100, - "sigPeriod": 600, - "sigValidity": 2629800, - "sigQty": 3, - "xpercent": 0.9, - "sigStock": 10, - "sigWindow": 1000, - "msValidity": 2629800, - "stepMax": 3, - "medianTimeBlocks": 11, - "avgGenTime": 600, - "dtDiffEval": 20, - "blocksRot": 144, - "percentRot": 0.67 -} - -def get_mock(loop): - mock = MockServer(loop) - - mock.add_route('GET', '/blockchain/parameters', bma_parameters, 200) - - mock.add_route('GET', '/blockchain/block/0', {'ucode': errors.BLOCK_NOT_FOUND, - "message": "Block not found"}, 404) - - mock.add_route('GET', '/blockchain/current', {'ucode': errors.NO_CURRENT_BLOCK, - 'message': "Block not found"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, - 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/lookup/john', bma_lookup_test_john, 200) - - mock.add_route('GET', '/wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_lookup_test_john, 200) - - mock.add_route('GET', '/wot/lookup/doe', bma_lookup_test_doe, 200) - - mock.add_route('GET', '/wot/lookup/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn', bma_lookup_test_doe, 200) - - mock.add_route('GET', '/wot/lookup/patrick', bma_lookup_test_patrick, 200) - - mock.add_route('GET', '/wot/lookup/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn', bma_lookup_test_patrick, 200) - - mock.add_route('POST', '/wot/add', {}, 200) - - mock.add_route('POST', '/wot/certify', {}, 200) - - return mock diff --git a/src/sakia/tests/mocks/bma/new_blockchain.py b/src/sakia/tests/mocks/bma/new_blockchain.py deleted file mode 100644 index fc83d1241d29f383c27913c534ad744b758605a9..0000000000000000000000000000000000000000 --- a/src/sakia/tests/mocks/bma/new_blockchain.py +++ /dev/null @@ -1,77 +0,0 @@ -from ..server import MockServer -from duniterpy.api import errors - -bma_wot_add = { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uids": [ - { - "uid": "test", - "meta": { - "timestamp": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" - }, - "self": "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci", - "others": [ - ], - "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", - "revoked": False, - } - ] -} - -bma_parameters = { - "currency": "test_currency", - "c": 0.1, - "dt": 86400, - "ud0": 100, - "sigPeriod": 600, - "sigValidity": 2629800, - "sigQty": 3, - "xpercent": 0.9, - "sigStock": 10, - "sigWindow": 1000, - "msValidity": 2629800, - "stepMax": 3, - "medianTimeBlocks": 11, - "avgGenTime": 600, - "dtDiffEval": 20, - "blocksRot": 144, - "percentRot": 0.67 -} - -def get_mock(loop): - mock = MockServer(loop) - - mock.add_route('GET', '/blockchain/parameters', bma_parameters, 200) - - mock.add_route('GET', '/blockchain/block/0', {'ucode': errors.BLOCK_NOT_FOUND, - 'message': "Block not found"}, 404) - - mock.add_route('GET', '/blockchain/current', {'ucode': errors.NO_CURRENT_BLOCK, - 'message': "Block not found"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, - 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', - {'ucode': errors.NO_MATCHING_IDENTITY, - 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/lookup/john', - {'ucode': errors.NO_MATCHING_IDENTITY, - 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/john', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/lookup/doe', - {'ucode': errors.NO_MATCHING_IDENTITY, - 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/doe', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('POST', '/wot/add', bma_wot_add, 200) - - mock.add_route('POST', '/wot/certify', {}, 200) - return mock diff --git a/src/sakia/tests/mocks/bma/nice_blockchain.py b/src/sakia/tests/mocks/bma/nice_blockchain.py deleted file mode 100644 index b8ad5e293d29b5b3ddab20b05c942799ba4072f6..0000000000000000000000000000000000000000 --- a/src/sakia/tests/mocks/bma/nice_blockchain.py +++ /dev/null @@ -1,523 +0,0 @@ -from ..server import MockServer -from duniterpy.api import errors - -bma_lookup_john = { - "partial": False, - "results": [ - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uids": [ - { - "uid": "john", - "meta": { - "timestamp": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" - }, - "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", - "revoked": False, - "self": "ZrHK0cCqrxWReROK0ciiSb45+dRphJa68qFaSjdve8bBdnGAu7+DIu0d+u/fXrNRXuObihOKMBIawaIVPNHqDw==", - "others": [ - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "meta": { - "block_number": 15 - }, - "uids": [ - "doe" - ], - "isMember": True, - "wasMember": True, - "signature": "4ulycI2MtBu/8bZipy+OsXDCNm9EyUIdZ1HA7hbJ66phKRNvv70Oo2YOF/+VDRJb97z9TqWKgfIQ0NbXU15xDg==" - }, - ] - } - ], - "signed": [] - } - ] -} - -bma_membership_john = { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uid": "john", - "sigDate": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", - "memberships": - [ - { - "version": 2, - "currency": "test_currency", - "membership": "IN", - "blockNumber": 0, - "blockHash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", - "written": 10000 - } - ] -} - -bma_lookup_doe = { - "partial": False, - "results": [ - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "uids": [ - { - "uid": "doe", - "meta": { - "timestamp": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" - }, - "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", - "revoked": False, - "self": "cIkHPQQ5+xTb4cKWv85rcYcZT+E3GDtX8B2nCK9Vs12p2Yz4bVaZiMvBBwisAAy2WBOaqHS3ydpXGtADchOICw==", - "others": [] - } - ], - "signed": [ - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "meta": { - "block_number": 38580 - }, - "uids": [ - "john" - ], - "isMember": True, - "wasMember": True, - "signature": "4ulycI2MtBu/8bZipy+OsXDCNm9EyUIdZ1HA7hbJ66phKRNvv70Oo2YOF/+VDRJb97z9TqWKgfIQ0NbXU15xDg==" - }, - ] - } - ] -} - -bma_certifiers_of_john = { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uid": "john", - "isMember": True, - "certifications": [ - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "uid": "doe", - "isMember": True, - "wasMember": True, - "cert_time": { - "block": 15, - "medianTime": 1500000000 - }, - "sigDate": "101-BAD49448A1AD73C978CEDCB8F137D20A5715EBAA739DAEF76B1E28EE67B2C00C", - "written": { - "number": 15, - "hash": "0000EC88BBBAA29D530D2B815DEE264DDC9F07F4" - }, - "signature": "oliiPDhniZAGHrIFL66oHR+cqD4aTgXX+20VFLMfNHwdYPeik76hy334zxhoDC4cPODMb9df2nF/EDfCefrNBg==" - }, - ] -} - -bma_certified_by_john = { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uid": "john", - "isMember": True, - "wasMember": False, - "certifications": [ - ] -} - -bma_certified_by_doe = { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "uid": "doe", - "isMember": True, - "certifications": [ - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "uid": "john", - "isMember": True, - "wasMember": True, - "cert_time": { - "block": 15, - "medianTime": 1500000000 - }, - "sigDate": "20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", - "written": { - "number": 15, - "hash": "0000EC88BBBAA29D530D2B815DEE264DDC9F07F4" - }, - "signature": "oliiPDhniZAGHrIFL66oHR+cqD4aTgXX+20VFLMfNHwdYPeik76hy334zxhoDC4cPODMb9df2nF/EDfCefrNBg==" - }, - ] -} - -bma_parameters = { - "currency": "test_currency", - "c": 0.1, - "dt": 86400, - "ud0": 100, - "sigPeriod": 600, - "sigValidity": 2629800, - "sigQty": 3, - "xpercent": 0.9, - "sigStock": 10, - "sigWindow": 1000, - "msValidity": 2629800, - "stepMax": 3, - "medianTimeBlocks": 11, - "avgGenTime": 600, - "dtDiffEval": 20, - "blocksRot": 144, - "percentRot": 0.67 -} - -bma_blockchain_0 = { - "version": 2, - "nonce": 10144, - "number": 0, - "powMin": 3, - "time": 1421838980, - "medianTime": 1421838980, - "membersCount": 4, - "monetaryMass": 0, - "currency": "test_currency", - "issuer": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "signature": "+78w7251vvRdhoIJ6IWHEiEOLxNrmfQf45Y5sYvPdnAdXkVpO1unMV5YA/G5Vhphyz1dICrbeKCPM5qbFsoWAQ==", - "hash": "00063EB6E83F8717CEF1D25B3E2EE308374A14B1", - "inner_hash": "00063EB6E83F8717CEF1D25B3E2EE308374A14B1", - "parameters": "0.1:86400:100:604800:2629800:3:3:2629800:3:11:600:20:144:0.67", - "previousHash": None, - "previousIssuer": None, - "dividend": None, - "membersChanges": [], - "identities": [ - "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:Ot3zIp/nsHT3zgJy+2YcXPL6vaM5WFsD+F8w3qnJoBRuBG6lv761zoaExp2iyUnm8fDAyKPpMxRK2kf437QSCw==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:inso", - "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:GZKLgaxJKL+GqxVLePMt8OVLJ6qTLrib5Mr/j2gjiNRY2k485YLB2OlzhBzZVnD3xLs0xi69JUfmLnM54j3aCA==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:cgeek", - "BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:th576H89dfymkG7/sH+DAIzjlmIqNEW6zY3ONrGeAml+k3f1ver399kYnEgG5YCaKXnnVM7P0oJHah80BV3mDw==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:moul", - "37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:XRmbTYFkPeGVEU2mJzzN4h1oVNDsZ4yyNZlDAfBm9CWhBsZ82QqX9GPHye2hBxxiu4Nz1BHgQiME6B4JcAC8BA==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:galuel" - ], - "joiners": [ - "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:ccJm3F44eLMhQtnQY/7+14SWCDqVTL3Miw65hBVpV+YiUSUknIGhBNN0C0Cf+Pf0/pa1tjucW8Us3z5IklFSDg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:inso", - "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:1lFIiaR0QX0jibr5zQpXVGzBvMGqcsTRlmHiwGz5HOAZT8PTdVUb5q6YGZ6qAUZjdMjPmhLaiMIpYc47wUnzBA==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:cgeek", - "BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:ctyAhpTRrAAOhFJukWI8RBr//nqYYdQibVzjOfaCdcWLb3TNFKrNBBothNsq/YrYHr7gKrpoftucf/oxLF8zAg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:moul", - "37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:uoiGaC5b7kWqtqdPxwatPk9QajZHCNT9rf8/8ud9Rli24z/igcOf0Zr4A6RTAIKWUq9foW39VqJe+Y9R3rhACw==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:galuel" - ], - "actives": [], - "leavers": [], - "revoked": [], - "excluded": [], - "certifications": [ - "37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:0:3wmCVW8AbVxRFm2PuLXD9UTCIg93MhUblZJvlYrDldSV4xuA7mZCd8TV4vb/6Bkc0FMQgBdHtpXrQ7dpo20uBA==", - "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:0:7UMQsUjLvuiZKIzOH5rrZDdDi5rXUo69EuQulY1Zm42xpRx/Gt5CkoTcJ/Mu83oElQbcZZTz/lVJ6IS0jzMiCQ==", - "BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:0:twWSY9etI82FLEHzhdqIoHsC9ehWCA7DCPiGxDLCWGPO4TG77hwtn3RcC68qoKHCib577JCp+fcKyp2vyI6FDA==", - "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:0:7K5MHkO8ibf5SchmPkRrmsg9owEZZ23uEMJJSQYG7L3PUmAKmmV/0VSjivxXH8gJGQBGsXQoK79x1jsYnj2nAg==", - "BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:0:Jua4FcEJFptSE5OoG1/Mgzx4e9jgGnYu7t8g1sqqPujI9hRhLFNXbQXedPS1q1OD5vWivA045gKOq/gnj8opDg==", - "37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:0:R/DV4/wYjvBG09QSOGtnxd3bfPFhVjEE5Uy3BsBMVUvjLsgxjf8NgLhYVozcHTRWS43ArxlXKfS5m3+KIPhhAQ==", - "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:0:4hP+ahJK021akL4UxB6c5QLaGJXa9eapd3nfdFQe+Xy87f/XLhj8BCa22XbbOlyGdaZRT3AYzbCL2UD5tI8mCw==", - "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:0:sZTQJr0d/xQnxrIIdSePUJpSTOa8v6IYGXMF2fVDZxQU8vwfzPm2dUKTaF0nU6E9wOYszzkBHaXL85nir+WtCQ==", - "37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:0:hDuBkoFhWhR/FgOU1+9SbQGBMIr47xqUzw1ZMERaPQo4aWm0WFbZurG4lvuJZzTyG6RF/gSw4VPvYZFPxWmADg==", - "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:0:79ZVrBehElVZh82fJdR18IJx06GkEVZTbwdHH4zb0S6VaGwdtLh1rvomm4ukBvUc8r/suTweG/SScsJairXNAg==", - "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:0:e/ai9E4G5CFB9Qi329e0ffYpZMgxj8mM4rviqIr2+UESA0UG86OuAAyHO11hYeyolZRiU8I7WdtNE98B1uZuBg==", - "BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:0:q4PCneYkcPH8AHEqEvqTtYQWslhlYO2B87aReuOl1uPczn5Q3VkZFAsU48ZTYryeyWp2nxdQojdFYhlAUNchAw==" - ], - "transactions": [], - "raw": """Version: 2 -Type: Block -Currency: test_currency -Number: 0 -PoWMin: 3 -Time: 1421838980 -MedianTime: 1421838980 -Issuer: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk -Parameters: 0.1:86400:100:604800:15:604800:2629800:3:0.9:2629800:3:11:600:20:144:0.67 -MembersCount: 4 -Identities: -8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:Ot3zIp/nsHT3zgJy+2YcXPL6vaM5WFsD+F8w3qnJoBRuBG6lv761zoaExp2iyUnm8fDAyKPpMxRK2kf437QSCw==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:inso -HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:GZKLgaxJKL+GqxVLePMt8OVLJ6qTLrib5Mr/j2gjiNRY2k485YLB2OlzhBzZVnD3xLs0xi69JUfmLnM54j3aCA==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:cgeek -BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:th576H89dfymkG7/sH+DAIzjlmIqNEW6zY3ONrGeAml+k3f1ver399kYnEgG5YCaKXnnVM7P0oJHah80BV3mDw==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:moul -37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:XRmbTYFkPeGVEU2mJzzN4h1oVNDsZ4yyNZlDAfBm9CWhBsZ82QqX9GPHye2hBxxiu4Nz1BHgQiME6B4JcAC8BA==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:galuel -Joiners: -8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:ccJm3F44eLMhQtnQY/7+14SWCDqVTL3Miw65hBVpV+YiUSUknIGhBNN0C0Cf+Pf0/pa1tjucW8Us3z5IklFSDg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:inso -HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:1lFIiaR0QX0jibr5zQpXVGzBvMGqcsTRlmHiwGz5HOAZT8PTdVUb5q6YGZ6qAUZjdMjPmhLaiMIpYc47wUnzBA==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:cgeek -BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:ctyAhpTRrAAOhFJukWI8RBr//nqYYdQibVzjOfaCdcWLb3TNFKrNBBothNsq/YrYHr7gKrpoftucf/oxLF8zAg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:moul -37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:uoiGaC5b7kWqtqdPxwatPk9QajZHCNT9rf8/8ud9Rli24z/igcOf0Zr4A6RTAIKWUq9foW39VqJe+Y9R3rhACw==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:galuel -Actives: -Leavers: -Revoked: -Excluded: -Certifications: -37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:0:3wmCVW8AbVxRFm2PuLXD9UTCIg93MhUblZJvlYrDldSV4xuA7mZCd8TV4vb/6Bkc0FMQgBdHtpXrQ7dpo20uBA== -HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:0:7UMQsUjLvuiZKIzOH5rrZDdDi5rXUo69EuQulY1Zm42xpRx/Gt5CkoTcJ/Mu83oElQbcZZTz/lVJ6IS0jzMiCQ== -BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:0:twWSY9etI82FLEHzhdqIoHsC9ehWCA7DCPiGxDLCWGPO4TG77hwtn3RcC68qoKHCib577JCp+fcKyp2vyI6FDA== -8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:0:7K5MHkO8ibf5SchmPkRrmsg9owEZZ23uEMJJSQYG7L3PUmAKmmV/0VSjivxXH8gJGQBGsXQoK79x1jsYnj2nAg== -BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:0:Jua4FcEJFptSE5OoG1/Mgzx4e9jgGnYu7t8g1sqqPujI9hRhLFNXbQXedPS1q1OD5vWivA045gKOq/gnj8opDg== -37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:0:R/DV4/wYjvBG09QSOGtnxd3bfPFhVjEE5Uy3BsBMVUvjLsgxjf8NgLhYVozcHTRWS43ArxlXKfS5m3+KIPhhAQ== -8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:0:4hP+ahJK021akL4UxB6c5QLaGJXa9eapd3nfdFQe+Xy87f/XLhj8BCa22XbbOlyGdaZRT3AYzbCL2UD5tI8mCw== -HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:0:sZTQJr0d/xQnxrIIdSePUJpSTOa8v6IYGXMF2fVDZxQU8vwfzPm2dUKTaF0nU6E9wOYszzkBHaXL85nir+WtCQ== -37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:0:hDuBkoFhWhR/FgOU1+9SbQGBMIr47xqUzw1ZMERaPQo4aWm0WFbZurG4lvuJZzTyG6RF/gSw4VPvYZFPxWmADg== -8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:0:79ZVrBehElVZh82fJdR18IJx06GkEVZTbwdHH4zb0S6VaGwdtLh1rvomm4ukBvUc8r/suTweG/SScsJairXNAg== -HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:0:e/ai9E4G5CFB9Qi329e0ffYpZMgxj8mM4rviqIr2+UESA0UG86OuAAyHO11hYeyolZRiU8I7WdtNE98B1uZuBg== -BMAVuMDcGhYAV4wA27DL1VXX2ZARZGJYaMwpf7DJFMYH:37qBxM4hLV2jfyYo2bNzAjkeLngLr2r7G2HpdpKieVxw:0:q4PCneYkcPH8AHEqEvqTtYQWslhlYO2B87aReuOl1uPczn5Q3VkZFAsU48ZTYryeyWp2nxdQojdFYhlAUNchAw== -Transactions: -InnerHash: 09500111588846873CA0110602DDC17FB34AA9F4548B7CE322C845902FFC1429 -Nonce: 10144 -""" -} - -bma_blockchain_current = { - "version": 2, - "nonce": 6909, - "number": 15, - "powMin": 4, - "time": 1441618206, - "medianTime": 1441614759, - "membersCount": 20, - "monetaryMass": 11711349901120, - "currency": "test_currency", - "issuer": "EPs9qX7HmCDy6ptUoMLpTzbh9toHu4au488pBTU9DN6y", - "signature": "kz/34w1cG+8tYacuPXf3FPmsFwrvtWkwp1POLJuX1P0zYaB9Tuu7iyYJzMQS0Xa3vwuWRqfz+fgyoCGnBjBLBQ==", - "hash": "0000CB4E9CCDE6F579135331C97F13903E8B6E21", - "parameters": "", - "previousHash": "00003BDA844D77EEE7CF32A6C3C87F2ACBFCFCBB", - "previousIssuer": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - "dividend": None, - "membersChanges": [], - "identities": [], - "joiners": [], - "actives": [], - "leavers": [], - "excluded": [], - "certifications": [], - "transactions": [], - "raw": """Version: 2 -Type: Block -Currency: meta_brouzouf -Number: 30898 -PoWMin: 4 -Time: 1441618206 -MedianTime: 1441614759 -Issuer: EPs9qX7HmCDy6ptUoMLpTzbh9toHu4au488pBTU9DN6y -PreviousHash: 00003BDA844D77EEE7CF32A6C3C87F2ACBFCFCBB -PreviousIssuer: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk -MembersCount: 20 -Identities: -Joiners: -Actives: -Leavers: -Revoked: -Excluded: -Certifications: -Transactions: -InnerHash: 6BB2E0BE18BEA428379336FB5F09DCE0EB594D09CDD705085CA91AA966C27CFA -Nonce: 6909 -""" -} - -# Sent 6, received 20 + 30 -bma_txhistory_john = { - "currency": "test_currency", - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "history": - { - "sent": - [ - { - "version": 2, - "issuers": - [ - "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - ], - "inputs": - [ - "D:7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ:8" - ], - "unlocks": - [ - "SIG(0)" - ], - "outputs": - [ - "2:1:SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)", - "6:1:SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)" - ], - "comment": "", - "signatures": - [ - "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" - ], - "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", - "block_number": 2, - "time": 1421932545 - }, - ], - "received": - [ - { - "version": 2, - "issuers": - [ - "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" - ], - "inputs": - [ - "D:FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn:8" - ], - "unlocks": - [ - "SIG(0)" - ], - "outputs": - [ - "2:1:SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)", - "6:1:SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)" - ], - "comment": "", - "signatures": - [ - "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" - ], - "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", - "block_number": 2, - "time": 1421932545 - }, - { - "version": 2, - "issuers": - [ - "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" - ], - "inputs": - [ - "D:FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn:8" - ], - "unlocks": - [ - "SIG(0)" - ], - "outputs": - [ - "5:1:SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)", - "40:1:SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)" - ], - "comment": "", - "signatures": - [ - "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" - ], - "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", - "block_number": 2, - "time": 1421932545 - }, - ], - "sending": [], - "receiving": [] - } -} - -bma_udhistory_john = { - "currency": "test_currency", - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "history": - { - "history": - [ - { - "block_number": 2, - "consumed": False, - "time": 1435749971, - "amount": 5, - "base": 1 - }, - { - "block_number": 10, - "consumed": False, - "time": 1435836032, - "amount": 10, - "base": 1 - } - ] - }} - -bma_txsources_john = { - "currency": "test_currency", - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "sources": - [ - { - "type": "D", - "noffset": 2, - "identifier": "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", - "amount": 70, - "base": 0 - }, - { - "type": "D", - "noffset": 4, - "identifier": "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5", - "amount": 90, - "base": 0 - } - ]} - -bma_with_ud = { - "result": - { - "blocks": [] - } -} - - -def get_mock(loop): - mock = MockServer(loop) - - mock.add_route('GET', '/blockchain/parameters', bma_parameters, 200) - - mock.add_route('GET', '/blockchain/with/{topic}', bma_with_ud, 200) - - mock.add_route('GET', '/blockchain/current', bma_blockchain_current, 200) - - mock.add_route('GET', '/blockchain/block/0', bma_blockchain_0, 200) - - mock.add_route('GET', '/blockchain/block/15', bma_blockchain_current, 200) - - mock.add_route('GET', '/tx/history/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ/blocks/0/99', bma_txhistory_john, - 200) - - mock.add_route('GET', '/tx/sources/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_txsources_john, 200) - - mock.add_route('GET', '/ud/history/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_udhistory_john, 200) - - mock.add_route('GET', '/wot/certifiers-of/john', bma_certifiers_of_john, - 200) - - mock.add_route('GET', '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_certifiers_of_john, - 200) - - mock.add_route('GET', '/wot/certified-by/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_certified_by_john, 200) - - mock.add_route('GET', '/wot/lookup/john', bma_lookup_john, 200) - - mock.add_route('GET', '/wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_lookup_john, 200) - - mock.add_route('GET', '/wot/lookup/doe', bma_lookup_doe, 200) - - mock.add_route('GET', '/wot/lookup/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn', bma_lookup_doe, 200) - - mock.add_route('GET', '/blockchain/memberships/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ', bma_membership_john, - 200) - - mock.add_route('GET', '/wot/lookup/wrong_pubkey', - {'ucode': errors.NO_MATCHING_IDENTITY, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/wrong_pubkey', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/lookup/wrong_uid', - {'ucode': errors.NO_MATCHING_IDENTITY, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/wrong_uid', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/wot/certifiers-of/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('GET', '/blockchain/memberships/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn', - {'ucode': errors.NO_MEMBER_MATCHING_PUB_OR_UID, 'message': "No member matching this pubkey or uid"}, 404) - - mock.add_route('POST', '/tx/process', {}, 200, ) - - return mock diff --git a/src/sakia/tests/mocks/server.py b/src/sakia/tests/mocks/server.py deleted file mode 100644 index c49456267a585f2d22e07d8d696183f94934bc7b..0000000000000000000000000000000000000000 --- a/src/sakia/tests/mocks/server.py +++ /dev/null @@ -1,102 +0,0 @@ -from aiohttp import web, log, errors -import json -import socket -from duniterpy.documents import Peer - - -def bma_peering_generator(port): - return { - "version": 2, - "currency": "test_currency", - "endpoints": [ - "BASIC_MERKLED_API 127.0.0.1 {port}".format(port=port) - ], - "status": "UP", - "block": "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", - "signature": "cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA==", - "raw": "Version: 2\nType: Peer\nCurrency: meta_brouzouf\nPublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nBlock: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3\nEndpoints:\nBASIC_MERKLED_API 127.0.0.1 {port}\n".format(port=port), - "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" - } - - -def peer_document_generator(port): - return Peer.from_signed_raw("""Version: 2 -Type: Peer -Currency: meta_brouzouf -PublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk -Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 -Endpoints: -BASIC_MERKLED_API 127.0.0.1 {port} -cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA== -""".format(port=port)) - - -class Request(): - def __init__(self, method, url, content): - self.url = url - self.method = method - self.content = content - - -class MockServer(): - def __init__(self, loop): - self.lp = loop - self.requests = [] - self.app = web.Application(loop=self.lp, - middlewares=[self.middleware_factory]) - self.handler = None - self.port = self.find_unused_port() - - def get_request(self, i): - return self.requests[i] - - async def middleware_factory(self, app, handler): - async def middleware_handler(request): - try: - resp = await handler(request) - return resp - except web.HTTPNotFound: - return web.Response(status=404, body=bytes(json.dumps({"ucode":1001, - "message": "404 error"}), - "utf-8"), - headers={'Content-Type': 'application/json'}) - - return middleware_handler - - async def _handler(self, request, data_dict, http_code): - await request.read() - self.requests.append(Request(request.method, request.path, request.content)) - return web.Response(body=bytes(json.dumps(data_dict), "utf-8"), - headers={'Content-Type': 'application/json'}, - status=http_code) - - def add_route(self, req_type, url, data_dict, http_code=200): - self.app.router.add_route(req_type, url, - lambda request: self._handler(request, data_dict=data_dict, http_code=http_code)) - - def find_unused_port(self): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(('127.0.0.1', 0)) - port = s.getsockname()[1] - s.close() - return port - - def peer(self): - return peer_document_generator(self.port) - - async def create_server(self, ssl_ctx=None): - protocol = "https" if ssl_ctx else "http" - url = "{}://127.0.0.1:{}".format(protocol, self.port) - - self.add_route('GET', '/network/peering', bma_peering_generator(self.port)) - - self.handler = self.app.make_handler( - keep_alive_on=False, - access_log=log.access_logger, - ) - - srv = await self.lp.create_server(self.handler, '127.0.0.1', self.port) - return srv, self.port, url - - async def close(self): - await self.handler.finish_connections() \ No newline at end of file diff --git a/src/sakia/tests/quamash_utils.py b/src/sakia/tests/quamash_utils.py deleted file mode 100644 index 591b566d6df12daffe296d1f2ab02f3e64e10b49..0000000000000000000000000000000000000000 --- a/src/sakia/tests/quamash_utils.py +++ /dev/null @@ -1,53 +0,0 @@ -import asyncio -import quamash -import socket -from aiohttp import web, log - -_application_ = [] - - -class QuamashTest: - def setUpQuamash(self): - self.qapplication = get_application() - self.lp = quamash.QSelectorEventLoop(self.qapplication) - self.qapplication.setQuitOnLastWindowClosed(False) - asyncio.set_event_loop(self.lp) - self.lp.set_exception_handler(lambda l, c: unitttest_exception_handler(self, l, c)) - self.exceptions = [] - self.handler = None - - def tearDownQuamash(self): - try: - self.lp.close() - finally: - asyncio.set_event_loop(None) - - for exc in self.exceptions: - raise exc - - -def unitttest_exception_handler(test, loop, context): - """ - An exception handler which exists the program if the exception - was not catch - :param loop: the asyncio loop - :param context: the exception context - """ - if 'exception' in context: - exception = context['exception'] - else: - exception = BaseException(context['message']) - test.exceptions.append(exception) - - -def get_application(): - """Get the singleton QApplication""" - from quamash import QApplication - if not len(_application_): - application = QApplication.instance() - if not application: - import sys - application = QApplication(sys.argv) - _application_.append( application ) - return _application_[0] - diff --git a/src/sakia/tests/unit/core/__init__.py b/src/sakia/tests/unit/core/__init__.py deleted file mode 100644 index 39ab2a0b56350baad834cb7fb0cfecb8223e1fcd..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'inso' diff --git a/src/sakia/tests/unit/core/graph/test_base_graph.py b/src/sakia/tests/unit/core/graph/test_base_graph.py deleted file mode 100644 index f7a3bbdd3151a6f0b01e0720b5f033c7b451e710..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/graph/test_base_graph.py +++ /dev/null @@ -1,196 +0,0 @@ -import sys -import unittest -import logging -from asynctest.mock import Mock, CoroutineMock, patch -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.graph import BaseGraph -from sakia.core.graph.constants import EdgeStatus, NodeStatus - - -class TestBaseGraph(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - self.account_identity = Mock(specs='core.registry.Identity') - self.account_identity.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" - self.account_identity.uid = "account_identity" - self.account_identity.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.first_identity = Mock(specs='core.registry.Identity') - self.first_identity.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - self.first_identity.uid = "first_identity" - self.first_identity.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.second_identity = Mock(specs='core.registry.Identity') - self.second_identity.pubkey = "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" - self.second_identity.uid = "second_uid" - self.second_identity.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('time.time', Mock(return_value=50000)) - def test_arc_status(self, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - app = Mock() - - base_graph = BaseGraph(app, community) - - async def exec_test(): - self.assertEquals((await base_graph.arc_status(48000)), EdgeStatus.WEAK) - self.assertEquals((await base_graph.arc_status(49500)), EdgeStatus.STRONG) - self.assertEquals((await base_graph.arc_status(49200)), EdgeStatus.WEAK) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Application') - @patch('sakia.core.Community') - def test_node_status_member(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - - base_graph = BaseGraph(app, community) - certifier = Mock(specs='core.registry.Identity') - certifier.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - certifier.uid = "first_identity" - certifier.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False) - - account_identity = Mock(specs='core.registry.Identity') - account_identity.pubkey = "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" - account_identity.uid = "second_uid" - account_identity.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - async def exec_test(): - self.assertEquals((await base_graph.node_status(certifier, account_identity)), NodeStatus.OUT) - self.assertEquals((await base_graph.node_status(account_identity, account_identity)), NodeStatus.HIGHLIGHTED) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Application') - @patch('sakia.core.Community') - def test_confirmation_text_expert_enabled(self, app, community): - community.network.confirmations = Mock(return_value=2) - app.preferences = {'expert_mode': True} - - base_graph = BaseGraph(app, community) - - self.assertEquals(base_graph.confirmation_text(200), "2/6") - - @patch('sakia.core.Application') - @patch('sakia.core.Community') - def test_confirmation_text_expert_disabled(self, app, community): - community.network.confirmations = Mock(return_value=2) - app.preferences = {'expert_mode': False} - - base_graph = BaseGraph(app, community) - - self.assertEquals(base_graph.confirmation_text(200), "33 %") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_add_identitiers(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - app.preferences = {'expert_mode': True} - - base_graph = BaseGraph(app, community) - - certifications = [ - { - 'identity': self.first_identity, - 'cert_time': 49100, - 'block_number': 900 - }, - { - 'identity': self.second_identity, - 'cert_time': 49800, - 'block_number': 996 - } - ] - async def exec_test(): - await base_graph.add_certifier_list(certifications, self.account_identity, self.account_identity) - self.assertEqual(len(base_graph.nx_graph.nodes()), 3) - self.assertEqual(len(base_graph.nx_graph.edges()), 2) - nodes = base_graph.nx_graph.nodes(data=True) - edges = base_graph.nx_graph.edges(data=True) - - first_node = [n for n in nodes if n[0] == self.first_identity.pubkey][0] - self.assertEqual(first_node[1]['status'], NodeStatus.NEUTRAL) - self.assertEqual(first_node[1]['text'], certifications[0]['identity'].uid) - self.assertEqual(first_node[1]['tooltip'], certifications[0]['identity'].pubkey) - - second_node = [n for n in nodes if n[0] == self.second_identity.pubkey][0] - self.assertEqual(second_node[1]['status'], NodeStatus.OUT) - self.assertEqual(second_node[1]['text'], certifications[1]['identity'].uid) - self.assertEqual(second_node[1]['tooltip'], certifications[1]['identity'].pubkey) - - arc_from_first = [e for e in edges if e[0] == self.first_identity.pubkey][0] - self.assertEqual(arc_from_first[1], self.account_identity.pubkey) - self.assertEqual(arc_from_first[2]['status'], EdgeStatus.WEAK) - self.assertEqual(arc_from_first[2]['cert_time'], certifications[0]['cert_time']) - - arc_from_second = [e for e in edges if e[0] == self.second_identity.pubkey][0] - self.assertEqual(arc_from_second[1], self.account_identity.pubkey) - self.assertEqual(arc_from_second[2]['status'], EdgeStatus.STRONG) - self.assertEqual(arc_from_second[2]['cert_time'], certifications[1]['cert_time']) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_add_certified(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - app.preferences = {'expert_mode': True} - - base_graph = BaseGraph(app, community) - - certifications = [ - { - 'identity': self.first_identity, - 'cert_time': 49100, - 'block_number': 900 - }, - { - 'identity': self.second_identity, - 'cert_time': 49800, - 'block_number': 996 - } - ] - async def exec_test(): - await base_graph.add_certified_list(certifications, self.account_identity, self.account_identity) - self.assertEqual(len(base_graph.nx_graph.nodes()), 3) - self.assertEqual(len(base_graph.nx_graph.edges()), 2) - nodes = base_graph.nx_graph.nodes(data=True) - first_node = [n for n in nodes if n[0] == self.first_identity.pubkey][0] - self.assertEqual(first_node[1]['status'], NodeStatus.NEUTRAL) - self.assertEqual(first_node[1]['text'], certifications[0]['identity'].uid) - self.assertEqual(first_node[1]['tooltip'], certifications[0]['identity'].pubkey) - - second_node = [n for n in nodes if n[0] == self.second_identity.pubkey][0] - self.assertEqual(second_node[1]['status'], NodeStatus.OUT) - self.assertEqual(second_node[1]['text'], certifications[1]['identity'].uid) - self.assertEqual(second_node[1]['tooltip'], certifications[1]['identity'].pubkey) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_add_identity(self, app, community): - app.preferences = {'expert_mode': True} - - base_graph = BaseGraph(app, community) - - base_graph.add_identity(self.account_identity, NodeStatus.HIGHLIGHTED) - self.assertEqual(len(base_graph.nx_graph.nodes()), 1) - self.assertEqual(len(base_graph.nx_graph.edges()), 0) - nodes = base_graph.nx_graph.nodes(data=True) - account_node = [n for n in nodes if n[0] == self.account_identity.pubkey][0] - self.assertEqual(account_node[1]['status'], NodeStatus.HIGHLIGHTED) - self.assertEqual(account_node[1]['text'], self.account_identity.uid) - self.assertEqual(account_node[1]['tooltip'], self.account_identity.pubkey) diff --git a/src/sakia/tests/unit/core/graph/test_explorer_graph.py b/src/sakia/tests/unit/core/graph/test_explorer_graph.py deleted file mode 100644 index e3c99600bc1507f49da756241ee3c4977cf643a0..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/graph/test_explorer_graph.py +++ /dev/null @@ -1,210 +0,0 @@ -import sys -import unittest -import asyncio -from asynctest.mock import Mock, CoroutineMock, patch -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.graph import ExplorerGraph - - -class TestExplorerGraph(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - ## Graph to test : - ## - E - ## A - B - C - D - ## - ## Path : Between A and C - - self.idA = Mock(specs='core.registry.Identity') - self.idA.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" - self.idA.uid = "A" - self.idA.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.idB = Mock(specs='core.registry.Identity') - self.idB.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - self.idB.uid = "B" - self.idB.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.idC = Mock(specs='core.registry.Identity') - self.idC.pubkey = "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" - self.idC.uid = "C" - self.idC.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False) - - self.idD = Mock(specs='core.registry.Identity') - self.idD.pubkey = "6R11KGpG6w5Z6JfiwaPf3k4BCMY4dwhjCdmjGpvn7Gz5" - self.idD.uid = "D" - self.idD.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.idE = Mock(specs='core.registry.Identity') - self.idE.pubkey = "CZVDEsM6pPNxhAvXApGM8MJ6ExBZVpc8PNVyDZ7hKxLu" - self.idE.uid = "E" - self.idE.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False) - - self.idA.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - { - 'cert_time': 49800, - 'identity': self.idB, - 'block_number': 996 - } - ]) - self.idA.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[]) - - self.idB.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idC, - 'block_number': 990 - } - ]) - - self.idB.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[ - { - 'cert_time': 49800, - 'identity': self.idA, - 'block_number': 996 - } - ]) - - self.idC.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idD, - 'block_number': 990 - }, - { - 'cert_time': 49110, - 'identity': self.idE, - 'block_number': 990 - } - ]) - - self.idC.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idB, - 'block_number': 990 - } - ]) - - self.idD.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - ]) - self.idD.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idC, - 'block_number': 990 - }]) - - self.idE.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - ]) - self.idE.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idC, - 'block_number': 990 - }]) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_explore_full_from_center(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - community.nb_members = CoroutineMock(return_value = 3) - app.preferences = {'expert_mode': True} - - explorer_graph = ExplorerGraph(app, community) - - async def exec_test(): - await explorer_graph._explore(self.idB, 5) - self.assertEqual(len(explorer_graph.nx_graph.nodes()), 5) - self.assertEqual(len(explorer_graph.nx_graph.edges()), 4) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_explore_full_from_extremity(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - community.nb_members = CoroutineMock(return_value = 3) - app.preferences = {'expert_mode': True} - - explorer_graph = ExplorerGraph(app, community) - - async def exec_test(): - await explorer_graph._explore(self.idA, 5) - self.assertEqual(len(explorer_graph.nx_graph.nodes()), 5) - self.assertEqual(len(explorer_graph.nx_graph.edges()), 4) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_explore_partial(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - community.nb_members = CoroutineMock(return_value = 3) - app.preferences = {'expert_mode': True} - - explorer_graph = ExplorerGraph(app, community) - - async def exec_test(): - await explorer_graph._explore(self.idB, 1) - self.assertEqual(len(explorer_graph.nx_graph.nodes()), 3) - self.assertEqual(len(explorer_graph.nx_graph.edges()), 2) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_start_stop_exploration(self, app, community): - async def explore_mock(id, steps): - await asyncio.sleep(0.1) - await asyncio.sleep(0.1) - await asyncio.sleep(0.1) - - explorer_graph = ExplorerGraph(app, community) - explorer_graph._explore = explore_mock - - async def exec_test(): - self.assertEqual(explorer_graph.exploration_task, None) - explorer_graph.start_exploration(self.idA, 1) - self.assertNotEqual(explorer_graph.exploration_task, None) - task = explorer_graph.exploration_task - explorer_graph.start_exploration(self.idA, 1) - self.assertEqual(task, explorer_graph.exploration_task) - explorer_graph.start_exploration(self.idB, 1) - await asyncio.sleep(0) - self.assertTrue(task.cancelled()) - self.assertNotEqual(task, explorer_graph.exploration_task) - task2 = explorer_graph.exploration_task - explorer_graph.start_exploration(self.idB, 2) - await asyncio.sleep(0) - self.assertTrue(task2.cancelled()) - task3 = explorer_graph.exploration_task - explorer_graph.stop_exploration() - await asyncio.sleep(0) - self.assertTrue(task2.cancelled()) - - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/graph/test_wot_graph.py b/src/sakia/tests/unit/core/graph/test_wot_graph.py deleted file mode 100644 index e0e355a24cee102cc3e57f161304f52fb3b889ae..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/graph/test_wot_graph.py +++ /dev/null @@ -1,158 +0,0 @@ -import sys -import unittest -import logging -from asynctest.mock import Mock, CoroutineMock, patch -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.graph import WoTGraph -from sakia.core.graph.constants import EdgeStatus, NodeStatus - - -class TestWotGraph(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - ## Graph to test : - ## - ## A - B - C - ## - ## Path : Between A and C - - self.account_identity = Mock(specs='core.registry.Identity') - self.account_identity.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" - self.account_identity.uid = "A" - self.account_identity.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.idB = Mock(specs='core.registry.Identity') - self.idB.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - self.idB.uid = "B" - self.idB.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True) - - self.idC = Mock(specs='core.registry.Identity') - self.idC.pubkey = "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" - self.idC.uid = "C" - self.idC.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False) - - self.account_identity.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - { - 'cert_time': 49800, - 'identity': self.idB, - 'block_number': 996 - } - ]) - self.account_identity.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[]) - - self.idC.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certifierd_by', - return_value=[]) - - self.idC.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idB, - 'block_number': 990 - } - ]) - - self.idB.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by', - return_value=[ - { - 'cert_time': 49100, - 'identity': self.idC, - 'block_number': 996 - } - ]) - - self.idB.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of', - return_value=[ - { - 'cert_time': 49800, - 'identity': self.account_identity, - 'block_number': 996 - } - ]) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_explore_to_find_member(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - app.preferences = {'expert_mode': True} - - wot_graph = WoTGraph(app, community) - - async def exec_test(): - result = await wot_graph.explore_to_find_member(self.account_identity, self.idC) - self.assertTrue(result) - self.assertEqual(len(wot_graph.nx_graph.nodes()), 3) - self.assertEqual(len(wot_graph.nx_graph.edges()), 2) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Application') - @patch('sakia.core.Community') - @patch('time.time', Mock(return_value=50000)) - def test_explore_to_find_unknown(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - app.preferences = {'expert_mode': True} - - wot_graph = WoTGraph(app, community) - - identity_unknown = Mock(specs='core.registry.Identity') - identity_unknown.pubkey = "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" - identity_unknown.uid = "unkwn" - - async def exec_test(): - result = await wot_graph.explore_to_find_member(self.account_identity, identity_unknown) - self.assertFalse(result) - self.assertEqual(len(wot_graph.nx_graph.nodes()), 3) - self.assertEqual(len(wot_graph.nx_graph.edges()), 2) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_shortest_path(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - app.preferences = {'expert_mode': True} - - wot_graph = WoTGraph(app, community) - - async def exec_test(): - result = await wot_graph.explore_to_find_member(self.account_identity, self.idC) - self.assertTrue(result) - self.assertEqual(len(wot_graph.nx_graph.nodes()), 3) - self.assertEqual(len(wot_graph.nx_graph.edges()), 2) - path = await wot_graph.get_shortest_path_to_identity(self.account_identity, self.idC) - self.assertEqual(path[0], self.account_identity.pubkey,) - self.assertEqual(path[1], self.idB.pubkey) - self.assertEqual(path[2], self.idC.pubkey) - - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - @patch('time.time', Mock(return_value=50000)) - def test_initialize(self, app, community): - community.parameters = CoroutineMock(return_value = {'sigValidity': 1000}) - community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None) - app.preferences = {'expert_mode': True} - - wot_graph = WoTGraph(app, community) - - async def exec_test(): - await wot_graph.initialize(self.account_identity, self.account_identity) - self.assertEqual(len(wot_graph.nx_graph.nodes()), 2) - self.assertEqual(len(wot_graph.nx_graph.edges()), 1) - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_quantitative.py b/src/sakia/tests/unit/core/money/test_quantitative.py deleted file mode 100644 index dd32a301ff91a7280f982bb66e68ade163d32be4..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/money/test_quantitative.py +++ /dev/null @@ -1,144 +0,0 @@ -import unittest -from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.money import Quantitative - - -class TestQuantitative(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = Quantitative(0, community, app, None) - self.assertEqual(referential.units, "TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = Quantitative(0, community, app, None) - self.assertEqual(referential.units, "TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_value(self, app, community): - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.value() - self.assertEqual(value, 101010110) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_differential(self, app, community): - referential = Quantitative(110, community, app, None) - async def exec_test(): - value = await referential.value() - self.assertEqual(value, 110) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.localized(units=True) - self.assertEqual(value, "101,010,110 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.localized(units=True, international_system=True) - self.assertEqual(value, "101.010110 MTC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=False) - self.assertEqual(value, "101,010,110") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=True) - self.assertEqual(value, "101.010110 M") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True) - self.assertEqual(value, "101,010,110 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True, international_system=True) - self.assertEqual(value, "101.010110 MTC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=False) - self.assertEqual(value, "101,010,110") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Quantitative(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=True) - self.assertEqual(value, "101.010110 M") - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_quantitative_zsum.py b/src/sakia/tests/unit/core/money/test_quantitative_zsum.py deleted file mode 100644 index 68ecaad7c751ba4b8f6fa014c93875e0bc85ebe7..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/money/test_quantitative_zsum.py +++ /dev/null @@ -1,164 +0,0 @@ -import unittest -from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.money import QuantitativeZSum - - -class TestQuantitativeZSum(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = QuantitativeZSum(0, community, app, None) - self.assertEqual(referential.units, "Q0 TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = QuantitativeZSum(0, community, app, None) - self.assertEqual(referential.units, "Q0 TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_value(self, app, community): - referential = QuantitativeZSum(110, community, app, None) - community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) - community.monetary_mass = CoroutineMock(return_value=500) - async def exec_test(): - value = await referential.value() - self.assertEqual(value, 10) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_differential(self, app, community): - community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) - community.monetary_mass = CoroutineMock(return_value=500) - referential = QuantitativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.value() - self.assertEqual(value, 10) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) - community.monetary_mass = CoroutineMock(return_value=500) - referential = QuantitativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.localized(units=True) - self.assertEqual(value, "10 Q0 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 1000}) - community.monetary_mass = CoroutineMock(return_value=500 * 1000) - app.preferences = { - 'digits_after_comma': 6 - } - referential = QuantitativeZSum(110 * 1000, community, app, None) - async def exec_test(): - value = await referential.localized(units=True, international_system=True) - self.assertEqual(value, "109.500000 kQ0 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) - community.monetary_mass = CoroutineMock(return_value=500) - app.preferences = { - 'digits_after_comma': 6 - } - referential = QuantitativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=False) - self.assertEqual(value, "10") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 1000}) - community.monetary_mass = CoroutineMock(return_value=500 * 1000) - app.preferences = { - 'digits_after_comma': 6 - } - referential = QuantitativeZSum(110 * 1000, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=True) - self.assertEqual(value, "109.500000 kQ0 ") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 1000}) - community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) - referential = QuantitativeZSum(110 * 1000, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True) - self.assertEqual(value, "110,000 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 10}) - community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) - app.preferences = { - 'digits_after_comma': 6 - } - referential = QuantitativeZSum(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True, international_system=True) - self.assertEqual(value, "101.010110 MTC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 10}) - community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) - app.preferences = { - 'digits_after_comma': 6 - } - referential = QuantitativeZSum(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=False) - self.assertEqual(value, "101,010,110") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.get_ud_block = CoroutineMock(return_value={'membersCount': 10}) - community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) - app.preferences = { - 'digits_after_comma': 6 - } - referential = QuantitativeZSum(101010110, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=True) - self.assertEqual(value, "101.010110 M") - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_relative.py b/src/sakia/tests/unit/core/money/test_relative.py deleted file mode 100644 index ae542b05ae8f5f4110dfebd8370ed67297342b46..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/money/test_relative.py +++ /dev/null @@ -1,160 +0,0 @@ -import unittest -from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.money import Relative - - -class TestRelative(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = Relative(0, community, app, None) - self.assertEqual(referential.units, "UD TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = Relative(0, community, app, None) - self.assertEqual(referential.units, "UD TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_value(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - referential = Relative(10101011, community, app, None) - async def exec_test(): - value = await referential.value() - self.assertAlmostEqual(value, 1010.10110) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_differential(self, app, community): - community.dividend = CoroutineMock(return_value=1000) - referential = Relative(110, community, app, None) - async def exec_test(): - value = await referential.value() - self.assertAlmostEqual(value, 0.11) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(101, community, app, None) - async def exec_test(): - value = await referential.localized(units=True) - self.assertEqual(value, "0.101000 UD TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.localized(units=True, international_system=True) - self.assertEqual(value, "1.011000 mUD TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=False) - self.assertEqual(value, "0.101100") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=True) - self.assertEqual(value, "1.011000 mUD ") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True) - self.assertEqual(value, "0.101100 UD TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True, international_system=True) - self.assertEqual(value, "1.011000 mUD TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=False) - self.assertEqual(value, "0.001011") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = Relative(1011, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=True) - self.assertEqual(value, "1.011000 mUD ") - self.lp.run_until_complete(exec_test()) \ No newline at end of file diff --git a/src/sakia/tests/unit/core/money/test_relative_to_past.py b/src/sakia/tests/unit/core/money/test_relative_to_past.py deleted file mode 100644 index 61d6b05518e040ed298fff6796e4a1fd4e7e26b1..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/money/test_relative_to_past.py +++ /dev/null @@ -1,192 +0,0 @@ -import unittest -from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock -from PyQt5.QtCore import QLocale, QDateTime -from sakia.tests import QuamashTest -from sakia.core.money.relative_to_past import RelativeToPast - - -class TestRelativeToPast(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = RelativeToPast(0, community, app, 100) - self.assertEqual(referential.units, "UD(t) TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = RelativeToPast(0, community, app, 100) - self.assertEqual(referential.units, "UD(t) TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_value(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - referential = RelativeToPast(10101011, community, app, 100) - async def exec_test(): - value = await referential.value() - self.assertAlmostEqual(value, 1010.10110) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_differential(self, app, community): - community.dividend = CoroutineMock(return_value=1000) - referential = RelativeToPast(110, community, app, 100) - async def exec_test(): - value = await referential.value() - self.assertAlmostEqual(value, 0.11) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(101, community, app, 100) - async def exec_test(): - value = await referential.localized(units=True) - self.assertEqual(value, "0.101000 UD({0}) TC".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(1452663088792).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ))) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.localized(units=True, international_system=True) - self.assertEqual(value, "1.011000 mUD({0}) TC".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(1452663088792).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ))) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.localized(units=False, international_system=False) - self.assertEqual(value, "0.101100") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.localized(units=False, international_system=True) - self.assertEqual(value, "1.011000 mUD({0}) ".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(1452663088792).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ))) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.diff_localized(units=True) - self.assertEqual(value, "0.101100 UD({0}) TC".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(1452663088792).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ))) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.diff_localized(units=True, international_system=True) - self.assertEqual(value, "1.011000 mUD({0}) TC".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(1452663088792).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ))) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_no_si(self, app, community): - community.dividend = CoroutineMock(return_value=10000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=False) - self.assertEqual(value, "0.101100") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_with_si(self, app, community): - community.dividend = CoroutineMock(return_value=1000000) - community.get_ud_block = CoroutineMock(return_value={'medianTime': 1452663088792}) - type(community).short_currency = PropertyMock(return_value="TC") - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeToPast(1011, community, app, 100) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=True) - self.assertEqual(value, "1.011000 mUD({0}) ".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(1452663088792).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ))) - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_relative_zsum.py b/src/sakia/tests/unit/core/money/test_relative_zsum.py deleted file mode 100644 index f840ca5b6e2ae7a4120a3bdad2e57cca62530c86..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/money/test_relative_zsum.py +++ /dev/null @@ -1,204 +0,0 @@ -import unittest -from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core.money import RelativeZSum - - -class TestRelativeZSum(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = RelativeZSum(0, community, app, None) - self.assertEqual(referential.units, "R0 TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_units(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - referential = RelativeZSum(0, community, app, None) - self.assertEqual(referential.units, "R0 TC") - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_value(self, app, community): - referential = RelativeZSum(110, community, app, None) - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - async def exec_test(): - value = await referential.value() - self.assertAlmostEqual(value, 0.10) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_differential(self, app, community): - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - referential = RelativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.value() - self.assertAlmostEqual(value, 0.10) - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - referential = RelativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.localized(units=True) - self.assertEqual(value, "0.1 R0 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.localized(units=True, international_system=True) - self.assertEqual(value, "100.000000 mR0 TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=False) - self.assertEqual(value, "0.100000") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_localized_no_units_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeZSum(110, community, app, None) - async def exec_test(): - value = await referential.localized(units=False, international_system=True) - self.assertEqual(value, "100.000000 mR0 ") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - referential = RelativeZSum(90, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True) - self.assertEqual(value, "0.9 UD TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeZSum(90, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=True, international_system=True) - self.assertEqual(value, "900.000000 mUD TC") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_no_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeZSum(90, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=False) - self.assertEqual(value, "0.900000") - self.lp.run_until_complete(exec_test()) - - @patch('sakia.core.Community') - @patch('sakia.core.Application') - def test_diff_localized_no_units_with_si(self, app, community): - type(community).short_currency = PropertyMock(return_value="TC") - community.dividend = CoroutineMock(return_value=100) - community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ - {'membersCount': 5, "monetaryMass": 500, - "dividend": 100, 'unitbase': 0} if 'x' in kwargs \ - else {'membersCount': 5, "monetaryMass": 1050, - "dividend": 100, 'unitbase': 0} ) - app.preferences = { - 'digits_after_comma': 6 - } - referential = RelativeZSum(90, community, app, None) - async def exec_test(): - value = await referential.diff_localized(units=False, international_system=True) - self.assertEqual(value, "900.000000 mUD ") - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/test_account.py b/src/sakia/tests/unit/core/test_account.py deleted file mode 100644 index 6facbca9c59d069fab9802b5a85a5dc87750b42b..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_account.py +++ /dev/null @@ -1,157 +0,0 @@ -import unittest -from asynctest import Mock, MagicMock, CoroutineMock -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import IdentitiesRegistry, Identity -from sakia.core import Account -from sakia.tests import QuamashTest -from duniterpy.documents import BlockUID, SelfCertification -from duniterpy.key import ScryptParams - - -class TestAccount(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry() - - def tearDown(self): - self.tearDownQuamash() - - def test_load_save_account(self): - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - ScryptParams(4096, 16, 1), - "test_uid", [], [], [], self.identities_registry) - json_data = account.jsonify() - account_from_json = Account.load(json_data, self.identities_registry) - - self.assertEqual(account.name, account_from_json.name) - self.assertEqual(account.pubkey, account_from_json.pubkey) - self.assertEqual(len(account.communities), len(account_from_json.communities)) - self.assertEqual(len(account.wallets), len(account.wallets)) - - def test_add_contact(self): - called = False - - def signal_called(): - nonlocal called - called = True - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - ScryptParams(4096, 16, 1), - "test_uid", [], [], [], self.identities_registry) - account.contacts_changed.connect(signal_called) - account.add_contact({"uid":"friend", "pubkey":"FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"}) - self.assertEqual(len(account.contacts), 1) - self.assertEqual(account.contacts[0]["uid"], "friend") - self.assertEqual(account.contacts[0]["pubkey"], "FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") - self.assertTrue(called) - - def test_remove_contact(self): - called = False - - def signal_called(): - nonlocal called - called = True - contact = {"uid":"friend", "pubkey":"FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"} - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - ScryptParams(4096, 16, 1), - "test_uid", [], [], [contact], - self.identities_registry) - account.contacts_changed.connect(signal_called) - account.remove_contact(contact) - self.assertEqual(len(account.contacts), 0) - self.assertTrue(called) - - def test_edit_contact(self): - called = False - - def signal_called(): - nonlocal called - called = True - contact = {"uid":"friend", "pubkey":"FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"} - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - ScryptParams(4096, 16, 1), - "test_uid", [], [], [contact], - self.identities_registry) - account.contacts_changed.connect(signal_called) - account.edit_contact(0, {"uid": "ennemy", "pubkey": "FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"}) - self.assertEqual(len(account.contacts), 1) - self.assertEqual(account.contacts[0]["uid"], "ennemy") - self.assertEqual(account.contacts[0]["pubkey"], "FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") - self.assertTrue(called) - - def test_send_membership(self): - account = Account("test_salt", "H8uYXvyF6GWeCr8cwFJ6V5B8tNprwRdjepFNJBqivrzr", - ScryptParams(4096, 16, 1), - "test_account", [], [], [], - self.identities_registry) - account_identity = MagicMock(autospec='sakia.core.registry.Identity') - account_identity.selfcert = CoroutineMock(return_value=SelfCertification(2, "meta_brouzouf", - "H8uYXvyF6GWeCr8cwFJ6V5B8tNprwRdjepFNJBqivrzr", "test_account", 1000000000, "")) - community = MagicMock(autospec='sakia.core.Community') - community.blockUID = CoroutineMock(return_value=BlockUID(3102, "0000C5336F0B64BFB87FF4BC858AE25726B88175")) - self.identities_registry.future_find = CoroutineMock(return_value=account_identity) - community.bma_access = MagicMock(autospec='sakia.core.net.api.bma.access.BmaAccess') - response = Mock() - response.json = CoroutineMock(return_value={ - "signature": "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", - "membership": { - "version": 2, - "currency": "beta_brouzouf", - "issuer": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", - "membership": "IN", - "sigDate": 1390739944, - "uid": "superman63" - } -}) - response.status = 200 - community.bma_access.broadcast = CoroutineMock(return_value=[response]) - async def exec_test(): - result = await account.send_membership("test_password", community, "IN") - self.assertTrue(result) - - self.lp.run_until_complete(exec_test()) - - def test_send_certification(self): - cert_signal_sent = False - def check_certification_accepted(): - nonlocal cert_signal_sent - cert_signal_sent = True - - account = Account("test_salt", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - ScryptParams(4096, 16, 1), - "test_account", [], [], [], - self.identities_registry) - account.certification_accepted.connect(check_certification_accepted) - account_identity = MagicMock(autospec='sakia.core.registry.Identity') - account_identity.selfcert = CoroutineMock(return_value=SelfCertification(2, "meta_brouzouf", - "H8uYXvyF6GWeCr8cwFJ6V5B8tNprwRdjepFNJBqivrzr", "test_account", - BlockUID(1000, "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243"), - "82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw==")) - - certified = MagicMock(autospec='sakia.core.registry.Identity') - certified.uid = "john" - certified.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - certified.sigdate = 1441130831 - certified.selfcert = CoroutineMock(return_value=SelfCertification(2, "meta_brouzouf", - "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", "john", - BlockUID(1200, "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243"), - "82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw==")) - - community = MagicMock(autospec='sakia.core.Community') - community.blockUID = CoroutineMock(return_value=BlockUID(3102, "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243")) - self.identities_registry.future_find = CoroutineMock(side_effect=lambda pubkey, community :account_identity \ - if pubkey == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" else certified) - community.bma_access = MagicMock(autospec='sakia.core.net.api.bma.access.BmaAccess') - response = Mock() - response.json = CoroutineMock(return_value={}) - response.status = 200 - community.bma_access.broadcast = CoroutineMock(return_value=[response]) - async def exec_test(): - result = await account.certify("test_password", community, "") - self.assertTrue(result) - - self.lp.run_until_complete(exec_test()) - self.assertTrue(cert_signal_sent) - - - diff --git a/src/sakia/tests/unit/core/test_application.py b/src/sakia/tests/unit/core/test_application.py deleted file mode 100644 index 3065f89c792da2ad64f651751cc67999c8498f00..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_application.py +++ /dev/null @@ -1,28 +0,0 @@ -import aiohttp -import sys -import unittest -import asyncio -from asynctest.mock import Mock, CoroutineMock, patch -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.core import Application - - -class TestApplication(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - @patch('sakia.core.registry.IdentitiesRegistry') - @patch('aiohttp.get', CoroutineMock(side_effect=lambda *args, **kwargs: exec('raise aiohttp.errors.TimeoutError()'))) - def test_get_last_version_timeout(self, identities_registry): - app = Application(self.qapplication, self.lp, identities_registry) - - async def exec_test(): - app.get_last_version() - asyncio.sleep(5) - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/test_bma_access.py b/src/sakia/tests/unit/core/test_bma_access.py deleted file mode 100644 index b4a3257b403e5eb7447d3e61aee186e07435f295..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_bma_access.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -from unittest.mock import Mock -import time -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import Identity, IdentitiesRegistry, LocalState, BlockchainState - -from sakia.tests.mocks.bma import nice_blockchain, corrupted -from sakia.tests import QuamashTest -from sakia.core import Application, Community -from sakia.core.net import Network, Node -from duniterpy.documents.peer import Peer -from sakia.core.net.api.bma.access import BmaAccess - - -class TestBmaAccess(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry() - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.peer = Peer.from_signed_raw("""Version: 2 -Type: Peer -Currency: meta_brouzouf -PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU -Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 -Endpoints: -BASIC_MERKLED_API duniter.inso.ovh 80 -82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== -""") - self.node = Node(self.peer, - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - None, Node.ONLINE, - time.time(), {}, "duniter", "0.12.0", 0, Mock("aiohttp.ClientSession")) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - def tearDown(self): - self.tearDownQuamash() - - def test_compare_json_with_nonetype(self): - res = self.bma_access._compare_json({}, corrupted.bma_null_data) - self.assertFalse(res) - - def test_filter_nodes(self): - pass#TODO diff --git a/src/sakia/tests/unit/core/test_community.py b/src/sakia/tests/unit/core/test_community.py deleted file mode 100644 index 16d2fd66a49a39b449979190a7d9797eed863f74..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_community.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -from unittest.mock import Mock -from pkg_resources import parse_version -from PyQt5.QtCore import QLocale -from sakia.core.net.api.bma.access import BmaAccess -from sakia.core.net.network import Network -from sakia.core import Community -from sakia.tests import QuamashTest - - -class TestCommunity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_load_save_community(self): - network = Network("test_currency", [], Mock("aiohttp.ClientSession")) - bma_access = BmaAccess([], network) - community = Community("test_currency", network, bma_access) - - json_data = community.jsonify() - community_from_json = Community.load(json_data, parse_version('0.12.0')) - self.assertEqual(community.name, community_from_json.name) - self.assertEqual(len(community.network._nodes), len(community_from_json.network._nodes)) - community_from_json.network.session.close() - diff --git a/src/sakia/tests/unit/core/test_identities.py b/src/sakia/tests/unit/core/test_identities.py deleted file mode 100644 index f00d01847f6d5c886022d4e7dd5df5d985875d38..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_identities.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -import unittest -from unittest import mock -import asyncio -import quamash -import logging -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import Identity, IdentitiesRegistry, LocalState, BlockchainState -from sakia.tests import QuamashTest - - -class TestIdentity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_identity_from_handled_data(self): - community = mock.MagicMock() - type(community).currency = mock.PropertyMock(return_value="test_currency") - - identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", None, - LocalState.COMPLETED, BlockchainState.VALIDATED) - test_instances = { - "test_currency": {"7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ": identity} - } - identities_registry = IdentitiesRegistry(test_instances) - - identity_from_data = identities_registry.from_handled_data("john", - "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - None, - BlockchainState.VALIDATED, - community) - self.assertEqual(identity, identity_from_data) - diff --git a/src/sakia/tests/unit/core/test_identity.py b/src/sakia/tests/unit/core/test_identity.py deleted file mode 100644 index 5336bb43f7a6c870ded17517bc4da4d74603f0a3..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_identity.py +++ /dev/null @@ -1,127 +0,0 @@ -import unittest -from asynctest import Mock, CoroutineMock, patch -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import Identity, LocalState, BlockchainState - -from sakia.tests.mocks.bma import nice_blockchain, corrupted -from sakia.tests import QuamashTest -from duniterpy.api import bma -from duniterpy.documents import BlockUID -from sakia.tools.exceptions import MembershipNotFoundError - - -class TestIdentity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = Mock(spec='sakia.core.registry.IdentitiesRegistry') - self.community = Mock(spec='sakia.core.Community') - self.community.name = "test_brouzouf" - self.community.bma_access = Mock(spec='sakia.core.net.api.bma.BmaAccess') - - def tearDown(self): - self.tearDownQuamash() - - def test_identity_certifiers_of(self): - def bma_access(request, *args): - if request is bma.wot.CertifiersOf: - return nice_blockchain.bma_certifiers_of_john - if request is bma.wot.Lookup: - return nice_blockchain.bma_lookup_john - if request is bma.blockchain.Block: - return nice_blockchain.bma_blockchain_current - - def block_to_time(block_number=None): - if block_number == 15: - return 1200000200 - else: - return 1500000400 - - identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - BlockUID(20, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67"), - LocalState.COMPLETED, BlockchainState.VALIDATED) - id_doe = Identity("doe", "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - BlockUID(101, "BAD49448A1AD73C978CEDCB8F137D20A5715EBAA739DAEF76B1E28EE67B2C00C"), - LocalState.COMPLETED, BlockchainState.VALIDATED) - - self.community.bma_access.future_request = CoroutineMock(side_effect=bma_access) - self.identities_registry.from_handled_data = Mock(return_value=id_doe) - self.community.time = CoroutineMock(side_effect=block_to_time) - async def exec_test(): - certifiers = await identity.certifiers_of(self.identities_registry, self.community) - - self.assertEqual(len(certifiers), 2) - self.assertEqual(certifiers[0]['identity'].uid, "doe") - self.assertEqual(certifiers[1]['identity'].uid, "doe") - - self.lp.run_until_complete(exec_test()) - - @patch('time.time', Mock(return_value=1500000400)) - def test_identity_cert_delay(self): - def bma_access(request, *args): - if request is bma.wot.CertifiedBy: - return nice_blockchain.bma_certified_by_doe - if request is bma.wot.Lookup: - return nice_blockchain.bma_lookup_doe - if request is bma.blockchain.Block: - return nice_blockchain.bma_blockchain_current - - def block_to_time(block_number=None): - if block_number == 38580: - return 1500000200 - else: - return 1500000400 - - identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - BlockUID(20, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67"), - LocalState.COMPLETED, BlockchainState.VALIDATED) - id_doe = Identity("doe", "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - BlockUID(101, "BAD49448A1AD73C978CEDCB8F137D20A5715EBAA739DAEF76B1E28EE67B2C00C"), - LocalState.COMPLETED, BlockchainState.VALIDATED) - - self.community.bma_access.future_request = CoroutineMock(side_effect=bma_access) - self.community.parameters = CoroutineMock(side_effect=lambda: nice_blockchain.bma_parameters) - self.community.time = CoroutineMock(side_effect=block_to_time) - self.identities_registry.from_handled_data = Mock(return_value=id_doe) - async def exec_test(): - cert_delay = await identity.cert_issuance_delay(self.identities_registry, self.community) - self.assertEqual(cert_delay, 200) - - self.lp.run_until_complete(exec_test()) - - def test_identity_membership(self): - def bma_access(request, *args): - if request is bma.blockchain.Membership: - return nice_blockchain.bma_membership_john - - identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - BlockUID(20, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67"), - LocalState.COMPLETED, BlockchainState.VALIDATED) - - self.community.bma_access.future_request = CoroutineMock(side_effect=bma_access) - - async def exec_test(): - ms = await identity.membership(self.community) - self.assertEqual(ms["blockNumber"], 0) - self.assertEqual(ms["blockHash"], "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855") - self.assertEqual(ms["membership"], "IN") - self.assertEqual(ms["currency"], "test_currency") - self.assertEqual(ms["written"], 10000) - - self.lp.run_until_complete(exec_test()) - - def test_identity_corrupted_membership(self): - def bma_access(request, *args): - if request is bma.blockchain.Membership: - return corrupted.bma_memberships_empty_array - - identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - BlockUID(20, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67"), - LocalState.COMPLETED, BlockchainState.VALIDATED) - self.community.bma_access.future_request = CoroutineMock(side_effect=bma_access) - async def exec_test(): - with self.assertRaises(MembershipNotFoundError): - await identity.membership(self.community) - - self.lp.run_until_complete(exec_test()) - diff --git a/src/sakia/tests/unit/core/test_network.py b/src/sakia/tests/unit/core/test_network.py deleted file mode 100644 index 175062c5f3255ca4d711aceb8efd80405cb172d0..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_network.py +++ /dev/null @@ -1,17 +0,0 @@ -import aiohttp -import unittest -from unittest.mock import PropertyMock -from asynctest import Mock, patch -from duniterpy.documents.block import BlockUID -from PyQt5.QtCore import QLocale -from sakia.core.net import Network -from sakia.tests import QuamashTest - - -class TestCommunity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() diff --git a/src/sakia/tests/unit/core/test_node.py b/src/sakia/tests/unit/core/test_node.py deleted file mode 100644 index fd07ca043bd73ee0fc14ee9bcb4741439655a825..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_node.py +++ /dev/null @@ -1,104 +0,0 @@ -import unittest -from unittest.mock import Mock -from asynctest import CoroutineMock, patch -from duniterpy.documents import Peer, BlockUID -from PyQt5.QtCore import QLocale -from sakia.core.net import Node -from sakia.tests import QuamashTest -from sakia.tests.mocks.bma import nice_blockchain -from pkg_resources import parse_version - - -class TestNode(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_from_peer(self): - peer = Peer.from_signed_raw("""Version: 2 -Type: Peer -Currency: meta_brouzouf -PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU -Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 -Endpoints: -BASIC_MERKLED_API duniter.inso.ovh 80 -82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== -""") - node = Node.from_peer('meta_brouzouf', peer, Mock("aiohttp.ClientSession")) - self.assertEqual(node.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") - self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API duniter.inso.ovh 80") - self.assertEqual(node.currency, "meta_brouzouf") - - @patch('duniterpy.api.bma.network.Peering') - def test_from_address(self, peering): - peering.return_value.get = CoroutineMock(return_value={ - "version": 2, - "currency": "meta_brouzouf", - "endpoints": [ - "BASIC_MERKLED_API duniter.inso.ovh 80" - ], - "block": "48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8", - "signature": "82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw==", - "raw": "Version: 2\nType: Peer\nCurrency: meta_brouzouf\nPublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU\nBlock: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8\nEndpoints:\nBASIC_MERKLED_API duniter.inso.ovh 80\n", - "pubkey": "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" - }) - - async def exec_test(): - node = await Node.from_address("meta_brouzouf", "127.0.0.1", 9000, Mock("aiohttp.ClientSession")) - self.assertEqual(node.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") - self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API duniter.inso.ovh 80") - self.assertEqual(node.currency, "meta_brouzouf") - - self.lp.run_until_complete(exec_test()) - - def test_from_json_to_json(self): - json_data = {"version": "0.12.0", "state": 1, "fork_window": 0, "uid": "inso", - "block": nice_blockchain.bma_blockchain_current, - "peer": """Version: 2 -Type: Peer -Currency: meta_brouzouf -PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU -Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 -Endpoints: -BASIC_MERKLED_API duniter.inso.ovh 80 -82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== -""", - "pubkey": "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU", - "last_change": 1448199706.6561477, "software": "duniter"} - node = Node.from_json("meta_brouzouf", json_data, parse_version('0.12.0'), Mock("aiohttp.ClientSession")) - self.assertEqual(node.version, "0.12.0") - self.assertEqual(node.state, 1) - self.assertEqual(node.fork_window, 0) - self.assertEqual(node.uid, "inso") - self.assertEqual(node.block, nice_blockchain.bma_blockchain_current) - self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API duniter.inso.ovh 80") - self.assertEqual(node.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") - self.assertEqual(node.last_change, 1448199706.6561477) - self.assertEqual(node.currency, "meta_brouzouf") - self.assertEqual(node.peer.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") - self.assertEqual(node.peer.blockUID.number, 48698) - self.assertEqual(node.peer.blockUID.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") - - result = node.jsonify() - for key in result: - self.assertEqual(result[key], json_data[key], "Error with key {0}".format(key)) - - def test_jsonify_root_node(self): - peer = Peer.from_signed_raw("""Version: 2 -Type: Peer -Currency: meta_brouzouf -PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU -Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 -Endpoints: -BASIC_MERKLED_API duniter.inso.ovh 80 -82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== -""") - node = Node(peer, "inso", "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU", nice_blockchain.bma_blockchain_current, - Node.ONLINE, 1111111111, {}, "duniter", "0.12", 0, Mock("aiohttp.ClientSession")) - result = node.jsonify_root_node() - self.assertEqual(result['pubkey'], "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") - self.assertEqual(result['uid'], "inso") - self.assertEqual(result['peer'], peer.signed_raw()) \ No newline at end of file diff --git a/src/sakia/tests/unit/core/test_wallet.py b/src/sakia/tests/unit/core/test_wallet.py deleted file mode 100644 index e485c5723045970dc31fd8190c0ef2355f63bf31..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_wallet.py +++ /dev/null @@ -1,217 +0,0 @@ -import unittest -import pypeg2 -from unittest.mock import MagicMock, PropertyMock -from asynctest import CoroutineMock -from duniterpy.grammars import output -from duniterpy.documents import BlockUID -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.core import Wallet -from sakia.tests import QuamashTest - - -class TestWallet(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - def tearDown(self): - self.tearDownQuamash() - - def test_load_save_wallet(self): - wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - - json_data = wallet.jsonify() - wallet_from_json = Wallet.load(json_data, self.identities_registry) - self.assertEqual(wallet.walletid, wallet_from_json.walletid) - self.assertEqual(wallet.pubkey, wallet_from_json.pubkey) - self.assertEqual(wallet.name, wallet_from_json.name) - self.assertEqual(wallet._identities_registry, wallet_from_json._identities_registry) - - def test_prepare_tx_base_0(self): - community = MagicMock("sakia.core.Community") - community.currency = "test_currency" - cache = MagicMock("sakia.core.txhistory.TxHistory") - cache.available_sources = [{ - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "type": "D", - "noffset": 2, - "identifier": "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", - "amount": 15, - "base": 0 - }, - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "type": "D", - "noffset": 4, - "identifier": "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5", - "amount": 85, - "base": 0 - }, - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "type": "T", - "noffset": 4, - "identifier": "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", - "amount": 11, - "base": 1 - }] - wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - wallet.caches["test_currency"] = cache - tx = wallet.prepare_tx("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - BlockUID(32, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8"), - 99, "", community) - self.assertEqual(tx.version, 3) - self.assertEqual(tx.blockstamp.number, 32) - self.assertEqual(tx.blockstamp.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") - self.assertEqual(len(tx.issuers), 1) - self.assertEqual(tx.issuers[0], "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") - self.assertEqual(len(tx.inputs), 2) - self.assertEqual(tx.inputs[0].origin_id, "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365") - self.assertEqual(tx.inputs[0].source, "D") - self.assertEqual(tx.inputs[0].index, 2) - self.assertEqual(tx.inputs[1].origin_id, "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5") - self.assertEqual(tx.inputs[1].source, "D") - self.assertEqual(tx.inputs[1].index, 4) - self.assertEqual(len(tx.outputs), 2) - self.assertEqual(tx.outputs[0].amount, 99) - self.assertEqual(tx.outputs[0].base, 0) - self.assertEqual(pypeg2.compose(tx.outputs[0].conditions, output.Condition), - "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)") - self.assertEqual(tx.outputs[1].amount, 1) - self.assertEqual(tx.outputs[1].base, 0) - self.assertEqual(pypeg2.compose(tx.outputs[1].conditions, output.Condition), - "SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)") - self.assertEqual(len(tx.unlocks), 2) - self.assertEqual(tx.unlocks[0].index, 0) - self.assertEqual(tx.unlocks[0].parameters[0].index, 0) - self.assertEqual(tx.unlocks[1].index, 1) - self.assertEqual(tx.unlocks[0].parameters[0].index, 0) - - def test_prepare_tx_base_1(self): - community = MagicMock("sakia.core.Community") - community.currency = "test_currency" - cache = MagicMock("sakia.core.txhistory.TxHistory") - cache.available_sources = [{ - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "type": "D", - "noffset": 2, - "identifier": "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", - "amount": 15, - "base": 0 - }, - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "type": "D", - "noffset": 4, - "identifier": "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5", - "amount": 85, - "base": 0 - }, - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "type": "T", - "noffset": 4, - "identifier": "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", - "amount": 11, - "base": 1 - }] - wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - wallet.caches["test_currency"] = cache - tx = wallet.prepare_tx("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - BlockUID(32, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8"), - 100, "", community) - self.assertEqual(tx.version, 3) - self.assertEqual(tx.blockstamp.number, 32) - self.assertEqual(tx.blockstamp.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") - self.assertEqual(len(tx.issuers), 1) - self.assertEqual(tx.issuers[0], "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") - self.assertEqual(len(tx.inputs), 1) - self.assertEqual(tx.inputs[0].origin_id, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") - self.assertEqual(tx.inputs[0].source, "T") - self.assertEqual(tx.inputs[0].index, 4) - self.assertEqual(len(tx.outputs), 2) - self.assertEqual(tx.outputs[0].amount, 10) - self.assertEqual(tx.outputs[0].base, 1) - self.assertEqual(pypeg2.compose(tx.outputs[0].conditions, output.Condition), - "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)") - self.assertEqual(tx.outputs[1].amount, 1) - self.assertEqual(tx.outputs[1].base, 1) - self.assertEqual(pypeg2.compose(tx.outputs[1].conditions, output.Condition), - "SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)") - self.assertEqual(len(tx.unlocks), 1) - self.assertEqual(tx.unlocks[0].index, 0) - self.assertEqual(tx.unlocks[0].parameters[0].index, 0) - - def test_prepare_tx_base_1_overheads(self): - community = MagicMock("sakia.core.Community") - community.currency = "test_currency" - cache = MagicMock("sakia.core.txhistory.TxHistory") - cache.available_sources = [{ - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "type": "D", - "noffset": 2, - "identifier": "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", - "amount": 15, - "base": 0 - }, - { - "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "type": "D", - "noffset": 4, - "identifier": "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5", - "amount": 85, - "base": 0 - }, - { - "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - "type": "T", - "noffset": 4, - "identifier": "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", - "amount": 11, - "base": 1 - }] - wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - wallet.caches["test_currency"] = cache - tx = wallet.prepare_tx("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", - BlockUID(32, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8"), - 101, "", community) - self.assertEqual(tx.version, 3) - self.assertEqual(tx.blockstamp.number, 32) - self.assertEqual(tx.blockstamp.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") - self.assertEqual(len(tx.issuers), 1) - self.assertEqual(tx.issuers[0], "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") - self.assertEqual(len(tx.inputs), 2) - self.assertEqual(tx.inputs[0].origin_id, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") - self.assertEqual(tx.inputs[0].source, "T") - self.assertEqual(tx.inputs[0].index, 4) - self.assertEqual(tx.inputs[1].origin_id, "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365") - self.assertEqual(tx.inputs[1].source, "D") - self.assertEqual(tx.inputs[1].index, 2) - self.assertEqual(len(tx.outputs), 4) - self.assertEqual(tx.outputs[0].amount, 1) - self.assertEqual(tx.outputs[0].base, 0) - self.assertEqual(pypeg2.compose(tx.outputs[0].conditions, output.Condition), - "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)") - self.assertEqual(tx.outputs[1].amount, 10) - self.assertEqual(tx.outputs[1].base, 1) - self.assertEqual(pypeg2.compose(tx.outputs[1].conditions, output.Condition), - "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)") - self.assertEqual(tx.outputs[2].amount, 14) - self.assertEqual(tx.outputs[2].base, 0) - self.assertEqual(pypeg2.compose(tx.outputs[2].conditions, output.Condition), - "SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)") - self.assertEqual(tx.outputs[3].amount, 1) - self.assertEqual(tx.outputs[3].base, 1) - self.assertEqual(pypeg2.compose(tx.outputs[3].conditions, output.Condition), - "SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)") - self.assertEqual(len(tx.unlocks), 2) - self.assertEqual(tx.unlocks[0].index, 0) - self.assertEqual(tx.unlocks[0].parameters[0].index, 0) - self.assertEqual(tx.unlocks[1].index, 1) - self.assertEqual(tx.unlocks[1].parameters[0].index, 0) diff --git a/src/sakia/tests/unit/core/txhistory/__init__.py b/src/sakia/tests/unit/core/txhistory/__init__.py deleted file mode 100644 index 39ab2a0b56350baad834cb7fb0cfecb8223e1fcd..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/txhistory/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'inso' diff --git a/src/sakia/tests/unit/core/txhistory/test_txhistory_loading.py b/src/sakia/tests/unit/core/txhistory/test_txhistory_loading.py deleted file mode 100644 index 9f4ef8c0618dfc6d002da9bc615ed2f144986a12..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/txhistory/test_txhistory_loading.py +++ /dev/null @@ -1,63 +0,0 @@ -import sys -import unittest -import asyncio -import quamash -import time -import logging -from PyQt5.QtCore import QLocale, Qt -from sakia.tests.mocks.bma import nice_blockchain -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.core.app import Application -from sakia.core import Account, Community, Wallet -from sakia.core.net import Network, Node -from sakia.core.net.api.bma.access import BmaAccess -from sakia.tests import QuamashTest -from duniterpy.documents.peer import BMAEndpoint - - -class TestTxHistory(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50005) - self.node = Node("test_currency", [self.endpoint], - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - nice_blockchain.bma_blockchain_current, Node.ONLINE, - time.time(), {}, "duniter", "0.14.0", 0) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - self.wallet.init_cache(self.application, self.community) - - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "john", [self.community], [self.wallet], [], self.identities_registry) - - def tearDown(self): - self.tearDownQuamash() - - # this test fails with old algorithm - def notest_txhistory_reload(self): - mock = nice_blockchain.get_mock() - time.sleep(2) - logging.debug(mock.pretend_url) - - received_list = [] - self.lp.run_until_complete(self.wallet.caches[self.community.currency]. - refresh(self.community, received_list)) - self.assertEquals(len(received_list), 2) - received_value = sum([r.metadata['amount'] for r in received_list]) - self.assertEqual(received_value, 60) - self.assertEqual(len(self.wallet.dividends(self.community)), 2) - dividends_value = sum([ud['amount'] for ud in self.wallet.dividends(self.community)]) - self.assertEqual(dividends_value, 15) - mock.delete_mock() diff --git a/src/sakia/tests/unit/gui/test_context_menu.py b/src/sakia/tests/unit/gui/test_context_menu.py deleted file mode 100644 index c1ee0b9fa17d4cbf9f96afd36cf6dcaeebf515e6..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/test_context_menu.py +++ /dev/null @@ -1,164 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock, Mock -from asynctest.mock import CoroutineMock -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.tests.mocks.bma import nice_blockchain -from sakia.gui.widgets.context_menu import ContextMenu -from duniterpy.documents import Membership, BlockUID -from sakia.tools.exceptions import MembershipNotFoundError - - -class TestContextMenu(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - self.identity = Mock(specs='sakia.core.registry.Identity') - self.identity.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" - self.identity.uid = "A" - - self.app = MagicMock(specs='sakia.core.Application') - self.account = MagicMock(specs='sakia.core.Account') - self.community = MagicMock(specs='sakia.core.Community') - self.password_asker = MagicMock(specs='sakia.gui.password_asker.PasswordAsker') - - def tearDown(self): - self.tearDownQuamash() - - @patch('PyQt5.QtWidgets.QMenu', create=True) - def test_view_in_wot(self, qmenu): - wot_refreshed = False - - def refresh_wot(identity): - nonlocal wot_refreshed - self.assertEqual(identity, self.identity) - wot_refreshed = True - - async def exec_test(): - context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) - context_menu.view_identity_in_wot.connect(refresh_wot) - context_menu.view_wot(self.identity) - - self.lp.run_until_complete(exec_test()) - self.assertTrue(wot_refreshed) - - @patch('PyQt5.QtWidgets.QMenu', create=True) - def test_copy_pubkey_to_clipboard(self, qmenu): - app = Mock('sakia.core.Application') - - async def exec_test(): - context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) - context_menu.copy_pubkey_to_clipboard(self.identity) - - self.lp.run_until_complete(exec_test()) - self.assertEqual(self.qapplication.clipboard().text(), "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") - - @patch('PyQt5.QtWidgets.QMenu', create=True) - def test_copy_block_to_clipboard(self, qmenu): - self.community.get_block = CoroutineMock(side_effect=lambda n: nice_blockchain.bma_blockchain_current if n == 15 \ - else nice_blockchain.bma_blockchain_0) - self.qapplication.clipboard().clear() - - async def exec_test(): - context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) - context_menu.community = self.community - context_menu.copy_block_to_clipboard(15) - - self.lp.run_until_complete(exec_test()) - raw_block = "{0}{1}\n".format(nice_blockchain.bma_blockchain_current["raw"], - nice_blockchain.bma_blockchain_current["signature"]) - self.assertEqual(self.qapplication.clipboard().text(), raw_block) - - @patch('PyQt5.QtWidgets.QMenu', create=True) - def test_copy_membership_to_clipboard(self, qmenu): - ms_data = { - "version": 2, - "currency": "meta_brouzouf", - "membership": "IN", - "blockNumber": 49116, - "blockHash": "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", - "written": 49119 - } - ms_document = Membership(ms_data["version"], ms_data["currency"], self.identity.pubkey, - BlockUID(ms_data["blockNumber"], ms_data["blockHash"]), - ms_data["membership"], self.identity.uid, "49116-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", - "znWiWP7Sy9gg9pZq4YWKNpel8MM16VBM1lgBg2gWjSonnc+KVRCtQng5JB4JD0PgJJ0F8jdITuggFrRwqRfzAA==") - self.identity.membership = CoroutineMock(return_value=ms_data) - self.community.get_block = CoroutineMock(return_value={ - "version": 2, - "nonce": 127424, - "number": 49119, - "powMin": 5, - "time": 1453921638, - "medianTime": 1453912797, - "membersCount": 18, - "monetaryMass": 14028534972234185000, - "currency": "meta_brouzouf", - "issuer": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - "signature": "ZmjhoRubftJ/T2WYQ3gaDeTGGUJ3beUshtlWn1k/r5opk0vt48KG3w+9JU0T9YFR5uezllaek9efoNwAHRBLDw==", - "hash": "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243", - "innerhash": "273DE1845F8A63677D69DD427E00DAD73D9AEDBA80356A2E0D2152939D9DAF0C", - "parameters": "", - "previousHash": "000005C27A1636FE07AB01766FBA060565142D79", - "previousIssuer": "HBSSmqZjT4UQKsCntTSmZbu7iRP14HYtifLE6mW1PsBD", - "dividend": None, - "identities": [], - "joiners": [ - "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:znWiWP7Sy9gg9pZq4YWKNpel8MM16VBM1lgBg2gWjSonnc+KVRCtQng5JB4JD0PgJJ0F8jdITuggFrRwqRfzAA==:49116-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67:49116-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67:A" - ], - "actives": [], - "leavers": [], - "excluded": [], - "revoked": [], - "certifications": [], - "transactions": [], - "raw": """Version: 2 -Type: Block -Currency: meta_brouzouf -Number: 49119 -PoWMin: 5 -Time: 1453921638 -MedianTime: 1453912797 -Issuer: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk -PreviousHash: 000005C27A1636FE07AB01766FBA060565142D79 -PreviousIssuer: HBSSmqZjT4UQKsCntTSmZbu7iRP14HYtifLE6mW1PsBD -MembersCount: 18 -Identities: -Joiners: -HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:znWiWP7Sy9gg9pZq4YWKNpel8MM16VBM1lgBg2gWjSonnc+KVRCtQng5JB4JD0PgJJ0F8jdITuggFrRwqRfzAA==:49116-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67:49116-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67:A -Actives: -Leavers: -Revoked: -Excluded: -Certifications: -Transactions: -InnerHash: 273DE1845F8A63677D69DD427E00DAD73D9AEDBA80356A2E0D2152939D9DAF0C -Nonce: 127424 -""" -}) - self.qapplication.clipboard().clear() - - async def exec_test(): - context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) - context_menu.community = self.community - context_menu.copy_membership_to_clipboard(self.identity) - - self.lp.run_until_complete(exec_test()) - self.assertEqual(self.qapplication.clipboard().text(), ms_document.signed_raw()) - - @patch('PyQt5.QtWidgets.QMenu', create=True) - def test_copy_membership_to_clipboard_not_found(self, qmenu): - def raiser(): - raise MembershipNotFoundError("inso", "meta_brouzouf") - self.identity.membership = CoroutineMock(side_effect=lambda c: raiser()) - - self.qapplication.clipboard().clear() - - async def exec_test(): - context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) - context_menu.community = self.community - context_menu.copy_membership_to_clipboard(self.identity) - - self.lp.run_until_complete(exec_test()) - self.assertEqual(self.qapplication.clipboard().text(), "") diff --git a/src/sakia/tests/unit/gui/test_main_window.py b/src/sakia/tests/unit/gui/test_main_window.py deleted file mode 100644 index 0227edd5086c265d2ca1846af40cd21b8c68238c..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/test_main_window.py +++ /dev/null @@ -1,91 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock, Mock, PropertyMock -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.gui.mainwindow import MainWindow - - -class TestMainWindow(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - self.identity = Mock(spec='sakia.core.registry.Identity') - self.identity.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" - self.identity.uid = "A" - - self.app = MagicMock(autospec='sakia.core.Application') - self.account_joe = Mock(spec='sakia.core.Account') - self.account_joe.contacts_changed = Mock() - self.account_joe.contacts_changed.disconnect = Mock() - self.account_joe.contacts_changed.connect = Mock() - self.account_doe = Mock(spec='sakia.core.Account') - self.account_doe.contacts_changed = Mock() - self.account_doe.contacts_changed.disconnect = Mock() - self.account_doe.contacts_changed.connect = Mock() - - def change_current_account(account_name): - type(self.app).current_account = PropertyMock(return_value=self.account_doe) - self.app.get_account = Mock(side_effect=lambda name: self.app.accounts[name]) - self.app.change_current_account = Mock(side_effect=change_current_account) - type(self.app).current_account = PropertyMock(return_value=self.account_joe) - self.app.accounts = {'joe':self.account_joe, - 'doe': self.account_doe} - self.homescreen = MagicMock(autospec='sakia.gui.homescreen.Homescreen') - self.community_view = MagicMock(autospec='sakia.gui.community_view.CommunityView') - self.password_asker = MagicMock(autospec='sakia.gui.password_asker.PasswordAsker') - self.node_manager = MagicMock(autospec='sakia.gui.node_manager.NodeManager') - - def tearDown(self): - self.tearDownQuamash() - - def test_change_account(self): - widget = Mock(spec='PyQt5.QtWidgets.QMainWindow', create=True) - widget.installEventFilter = Mock() - ui = Mock(spec='sakia.gen_resources.mainwindow_uic.Ui_MainWindow', create=True) - ui.setupUi = Mock() - label_icon = Mock() - label_status = Mock() - label_time = Mock() - combo_referentials = Mock() - combo_referentials.currentIndexChanged = {int: Mock()} - mainwindow = MainWindow(self.app, self.account_joe, - self.homescreen, self.community_view, self.node_manager, - widget, ui, label_icon, - label_status, label_time, combo_referentials, self.password_asker) - mainwindow.refresh = Mock() - mainwindow.action_change_account("doe") - self.app.change_current_account.assert_called_once_with(self.account_doe) - mainwindow.change_account() - - self.community_view.change_account.assert_called_once_with(self.account_doe, self.password_asker) - self.password_asker.change_account.assert_called_once_with(self.account_doe) - self.account_joe.contacts_changed.disconnect.assert_called_once_with(mainwindow.refresh_contacts) - self.account_doe.contacts_changed.connect.assert_called_once_with(mainwindow.refresh_contacts) - mainwindow.refresh.assert_called_once_with() - - def test_change_account_from_none(self): - widget = Mock(spec='PyQt5.QtWidgets.QMainWindow', create=True) - widget.installEventFilter = Mock() - ui = Mock(spec='sakia.gen_resources.mainwindow_uic.Ui_MainWindow', create=True) - ui.setupUi = Mock() - label_icon = Mock() - label_status = Mock() - label_time = Mock() - combo_referentials = Mock() - combo_referentials.currentIndexChanged = {int: Mock()} - - type(self.app).current_account = PropertyMock(return_value=None) - mainwindow = MainWindow(self.app, None, self.homescreen, self.community_view, self.node_manager, - widget, ui, label_icon, - label_status, label_time, combo_referentials, self.password_asker) - mainwindow.refresh = Mock() - mainwindow.action_change_account("doe") - self.app.change_current_account.assert_called_once_with(self.account_doe) - mainwindow.change_account() - - self.community_view.change_account.assert_called_once_with(self.account_doe, self.password_asker) - self.password_asker.change_account.assert_called_once_with(self.account_doe) - self.account_joe.contacts_changed.disconnect.assert_not_called() - self.account_doe.contacts_changed.connect.assert_called_once_with(mainwindow.refresh_contacts) - mainwindow.refresh.assert_called_once_with() \ No newline at end of file diff --git a/src/sakia/tests/unit/gui/views/test_base_edge.py b/src/sakia/tests/unit/gui/views/test_base_edge.py deleted file mode 100644 index f05353bd653e425dc05be4f841960b810d3e39f0..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_base_edge.py +++ /dev/null @@ -1,34 +0,0 @@ -import unittest -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.gui.views.edges.base_edge import BaseEdge -from sakia.core.graph.constants import EdgeStatus - - -class TestBaseEdge(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_create_edge(self): - metadata = { - 'status': EdgeStatus.STRONG - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - edge = BaseEdge("A", "B", metadata, nx_pos) - self.assertEqual(edge.source, "A") - self.assertEqual(edge.destination, "B") - self.assertEqual(edge.destination_point.x(), 10) - self.assertEqual(edge.destination_point.y(), 20) - self.assertEqual(edge.source_point.x(), 0) - self.assertEqual(edge.source_point.y(), 5) - self.assertEqual(edge.status, EdgeStatus.STRONG) - - self.lp.run_until_complete(exec_test()) \ No newline at end of file diff --git a/src/sakia/tests/unit/gui/views/test_base_node.py b/src/sakia/tests/unit/gui/views/test_base_node.py deleted file mode 100644 index 3e2016837f242d1235d7658f932158a809e93606..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_base_node.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.gui.views.nodes.base_node import BaseNode -from sakia.core.graph.constants import NodeStatus - - -class TestBaseNode(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_create_edge(self): - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "UserA", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = BaseNode(("A", metadata), nx_pos) - self.assertEqual(node.id, "A") - self.assertEqual(node.metadata['status'], NodeStatus.NEUTRAL) - self.assertEqual(node.x(), 0) - self.assertEqual(node.y(), 5) - self.assertEqual(node.status_wallet, False) - self.assertEqual(node.status_member, True) - self.assertEqual(node.text, "UserA") - self.assertEqual(node.toolTip(), "UserA - TestTooltip") - - self.lp.run_until_complete(exec_test()) \ No newline at end of file diff --git a/src/sakia/tests/unit/gui/views/test_explorer_edge.py b/src/sakia/tests/unit/gui/views/test_explorer_edge.py deleted file mode 100644 index 5732190cbca2a56b3f0e6cfa14cf4eb520f3ccdd..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_explorer_edge.py +++ /dev/null @@ -1,76 +0,0 @@ -import unittest -from unittest.mock import patch -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.gui.views.edges import ExplorerEdge -from sakia.core.graph.constants import EdgeStatus - - -class TestExplorerEdge(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_create_wot_edge(self): - metadata = { - 'status': EdgeStatus.STRONG, - 'confirmation_text': "0/6" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - edge = ExplorerEdge("A", "B", metadata, nx_pos, 0, 0) - self.assertEqual(edge.source, "A") - self.assertEqual(edge.destination, "B") - self.assertAlmostEqual(edge.destination_point.x(), 10.0, delta=5) - self.assertAlmostEqual(edge.destination_point.y(), 20.0, delta=5) - self.assertAlmostEqual(edge.source_point.x(), 10.0, delta=5) - self.assertAlmostEqual(edge.source_point.y(), 20.0, delta=5) - self.assertEqual(edge.status, EdgeStatus.STRONG) - - self.lp.run_until_complete(exec_test()) - - @patch('PyQt5.QtGui.QPainter') - @patch('PyQt5.QtWidgets.QWidget') - def test_paint(self, painter, widget): - metadata = { - 'status': EdgeStatus.STRONG, - 'confirmation_text': "0/6" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - - async def exec_test(): - edge = ExplorerEdge("A", "B", metadata, nx_pos, 0, 1) - edge.paint(painter, 0, widget) - - self.lp.run_until_complete(exec_test()) - - @patch('PyQt5.QtGui.QPainter') - @patch('PyQt5.QtWidgets.QWidget') - def test_bounding_rect(self, painter, widget): - metadata = { - 'status': EdgeStatus.STRONG, - 'confirmation_text': "0/6" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - - async def exec_test(): - edge = ExplorerEdge("A", "B", metadata, nx_pos, 0, 0) - bounding_rect = edge.boundingRect() - self.assertAlmostEqual(bounding_rect.x(), 7.0, delta=5) - self.assertAlmostEqual(bounding_rect.y(), 17.0, delta=5) - self.assertAlmostEqual(bounding_rect.width(), 6.0, delta=5) - self.assertAlmostEqual(bounding_rect.height(), 6.0, delta=5) - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/gui/views/test_explorer_node.py b/src/sakia/tests/unit/gui/views/test_explorer_node.py deleted file mode 100644 index a27c75e1cc1a7724f518af2ee3c0f352e00b7b1a..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_explorer_node.py +++ /dev/null @@ -1,80 +0,0 @@ -import unittest -from unittest.mock import patch -from PyQt5.QtCore import QLocale, QPointF -from PyQt5.QtGui import QPainter -from PyQt5.QtWidgets import QStyleOptionGraphicsItem, QWidget -from sakia.tests import QuamashTest -from sakia.gui.views.nodes import ExplorerNode -from sakia.core.graph.constants import NodeStatus - - -class TestExplorerNode(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_create_explorer_node(self): - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "UserA", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = ExplorerNode(("A", metadata), QPointF(0, 0), nx_pos, 0, 1, False) - self.assertEqual(node.id, "A") - self.assertEqual(node.metadata['status'], NodeStatus.NEUTRAL) - self.assertEqual(node.x(), 0) - self.assertEqual(node.y(), 0) - self.assertEqual(node.status_wallet, False) - self.assertEqual(node.status_member, True) - self.assertEqual(node.text, "UserA") - self.assertEqual(node.toolTip(), "UserA - TestTooltip") - - self.lp.run_until_complete(exec_test()) - - def test_paint(self): - painter = QPainter() - widget = QWidget() - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "UserA", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = ExplorerNode(("A", metadata), QPointF(0, 0), nx_pos, 0, 1, False) - node.paint(painter, QStyleOptionGraphicsItem(), widget) - - self.lp.run_until_complete(exec_test()) - - @patch('PyQt5.QtGui.QPainter', spec=QPainter) - @patch('PyQt5.QtWidgets.QWidget') - def test_bounding_rect(self, painter, widget): - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "A", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = ExplorerNode(("A", metadata), QPointF(0, 0), nx_pos, 0, 1, False) - bounding_rect = node.boundingRect() - self.assertAlmostEqual(bounding_rect.x(), -0.5, delta=15) - self.assertAlmostEqual(bounding_rect.y(), -0.5, delta=15) - self.assertAlmostEqual(bounding_rect.width(), 19.59375, delta=15) - self.assertAlmostEqual(bounding_rect.height(), 37.0, delta=15) - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/gui/views/test_explorer_scene.py b/src/sakia/tests/unit/gui/views/test_explorer_scene.py deleted file mode 100644 index 7ba890170be0b25a011347b4d1a7973b3cddd3da..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_explorer_scene.py +++ /dev/null @@ -1,181 +0,0 @@ -import unittest -import networkx -import math -from unittest.mock import patch, Mock -from sakia.tests import QuamashTest -from sakia.gui.views.scenes import ExplorerScene -from sakia.core.graph.constants import NodeStatus - - -class TestExplorerScene(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - self.identities_uids = ['A', 'B', 'C', 'D', 'E'] - self.identities_pubkeys = ['pbkA', 'pbkB', 'pbkC', 'pbkD', 'pbkE'] - self.certifications = [('pbkA', 'pbkB'), - ('pbkB', 'pbkC'), - ('pbkD', 'pbkA'), - ('pbkA', 'pbkE')] - # Graph : - # A -> B -> C - # <- D - # -> E - self.identity_status = [NodeStatus.SELECTED, NodeStatus.NEUTRAL, NodeStatus.NEUTRAL, - NodeStatus.OUT, NodeStatus.NEUTRAL] - self.test_graph = networkx.MultiDiGraph() - self.test_graph.add_nodes_from(self.identities_pubkeys) - self.test_graph.add_edges_from(self.certifications) - for index, node in enumerate(self.test_graph.nodes(data=True)): - node[1]['text'] = self.identities_uids[index] - node[1]['tooltip'] = self.identities_pubkeys[index] - node[1]['status'] = self.identity_status[index] - - def tearDown(self): - self.tearDownQuamash() - - def test_init_layout(self): - data_layout = ExplorerScene._init_layout(self.test_graph) - for pubkey in self.identities_pubkeys: - self.assertEqual(data_layout[pubkey]['theta'], None) - self.assertEqual(data_layout[pubkey]['scenter'], 25) - self.assertEqual(data_layout[pubkey]['nchild'], 0) - self.assertEqual(data_layout[pubkey]['sparent'], None) - self.assertEqual(data_layout[pubkey]['stsize'], 0.0) - self.assertEqual(data_layout[pubkey]['span'], 0.0) - - def test_set_parent_nodes(self): - data_layout = ExplorerScene._init_layout(self.test_graph) - ExplorerScene._set_parent_nodes(self.test_graph, data_layout, 'pbkA') - self.assertEqual(data_layout['pbkA']['scenter'], 0) - self.assertEqual(data_layout['pbkB']['scenter'], 1) - self.assertEqual(data_layout['pbkC']['scenter'], 2) - self.assertEqual(data_layout['pbkD']['scenter'], 1) - self.assertEqual(data_layout['pbkE']['scenter'], 1) - - self.assertEqual(data_layout['pbkA']['sparent'], None) - self.assertEqual(data_layout['pbkB']['sparent'], 'pbkA') - self.assertEqual(data_layout['pbkC']['sparent'], 'pbkB') - self.assertEqual(data_layout['pbkD']['sparent'], 'pbkA') - self.assertEqual(data_layout['pbkE']['scenter'], 1) - - self.assertEqual(data_layout['pbkA']['nchild'], 3) - self.assertEqual(data_layout['pbkB']['nchild'], 1) - self.assertEqual(data_layout['pbkC']['nchild'], 0) - self.assertEqual(data_layout['pbkD']['nchild'], 0) - self.assertEqual(data_layout['pbkE']['nchild'], 0) - - def test_set_subtree_size(self): - data_layout = ExplorerScene._init_layout(self.test_graph) - - data_layout['pbkA']['sparent'] = None - data_layout['pbkB']['sparent'] = 'pbkA' - data_layout['pbkC']['sparent'] = 'pbkB' - data_layout['pbkD']['sparent'] = 'pbkA' - data_layout['pbkE']['sparent'] = 'pbkA' - - data_layout['pbkA']['nchild'] = 2 - data_layout['pbkB']['nchild'] = 1 - data_layout['pbkC']['nchild'] = 0 - data_layout['pbkD']['nchild'] = 0 - data_layout['pbkE']['nchild'] = 0 - - ExplorerScene._set_subtree_size(self.test_graph, data_layout) - self.assertAlmostEqual(data_layout['pbkA']['stsize'], 3.0) - self.assertAlmostEqual(data_layout['pbkB']['stsize'], 1.0) - self.assertAlmostEqual(data_layout['pbkC']['stsize'], 1.0) - self.assertAlmostEqual(data_layout['pbkD']['stsize'], 1.0) - self.assertAlmostEqual(data_layout['pbkE']['stsize'], 1.0) - - def test_set_subtree_span(self): - data_layout = ExplorerScene._init_layout(self.test_graph) - - data_layout['pbkA']['sparent'] = None - data_layout['pbkB']['sparent'] = 'pbkA' - data_layout['pbkC']['sparent'] = 'pbkB' - data_layout['pbkD']['sparent'] = 'pbkA' - data_layout['pbkE']['sparent'] = 'pbkA' - - data_layout['pbkA']['nchild'] = 2 - data_layout['pbkB']['nchild'] = 1 - data_layout['pbkC']['nchild'] = 0 - data_layout['pbkD']['nchild'] = 0 - data_layout['pbkE']['nchild'] = 0 - - data_layout['pbkA']['stsize'] = 3.0 - data_layout['pbkB']['stsize'] = 1.0 - data_layout['pbkC']['stsize'] = 1.0 - data_layout['pbkD']['stsize'] = 1.0 - data_layout['pbkE']['stsize'] = 1.0 - - data_layout['pbkA']['span'] = 2 * math.pi - - ExplorerScene._set_subtree_spans(self.test_graph, data_layout, 'pbkA') - self.assertAlmostEqual(data_layout['pbkA']['span'], 2 * math.pi) - self.assertAlmostEqual(data_layout['pbkB']['span'], 2 / 3 * math.pi) - self.assertAlmostEqual(data_layout['pbkC']['span'], 2 / 3 * math.pi) - self.assertAlmostEqual(data_layout['pbkD']['span'], 2 / 3 * math.pi) - self.assertAlmostEqual(data_layout['pbkE']['span'], 2 / 3 * math.pi) - - @patch('networkx.MultiDiGraph') - def test_set_subtree_position(self, mock_graph): - # We mock the edges generator to ensure the order in which they appear - undirected = Mock('networkx.MultiDiGraph') - undirected.edges = Mock(return_value=self.certifications) - mock_graph.to_undirected = Mock(return_value=undirected) - data_layout = {} - - for pubkey in self.identities_pubkeys: - data_layout[pubkey] = { - 'theta': None - } - - data_layout['pbkA']['sparent'] = None - data_layout['pbkB']['sparent'] = 'pbkA' - data_layout['pbkC']['sparent'] = 'pbkB' - data_layout['pbkD']['sparent'] = 'pbkA' - data_layout['pbkE']['sparent'] = 'pbkA' - - data_layout['pbkA']['nchild'] = 2 - data_layout['pbkB']['nchild'] = 1 - data_layout['pbkC']['nchild'] = 0 - data_layout['pbkD']['nchild'] = 0 - data_layout['pbkE']['nchild'] = 0 - - data_layout['pbkA']['span'] = 2 * math.pi - data_layout['pbkB']['span'] = 2 / 3 * math.pi - data_layout['pbkC']['span'] = 2 / 3 * math.pi - data_layout['pbkD']['span'] = 2 / 3 * math.pi - data_layout['pbkE']['span'] = 2 / 3 * math.pi - - data_layout['pbkA']['theta'] = 0.0 - ExplorerScene._set_positions(mock_graph, data_layout, 'pbkA') - self.assertAlmostEqual(data_layout['pbkA']['theta'], 0.0) - self.assertAlmostEqual(data_layout['pbkB']['theta'], 1 / 3 * math.pi) - self.assertAlmostEqual(data_layout['pbkC']['theta'], 1 / 3 * math.pi) - self.assertAlmostEqual(data_layout['pbkD']['theta'], math.pi) - self.assertAlmostEqual(data_layout['pbkE']['theta'], 5 / 3 * math.pi) - - @patch('networkx.MultiDiGraph') - @patch('networkx.MultiGraph') - @patch('networkx.shortest_path_length', return_value={'pbkA': 0, 'pbkB': 1, 'pbkC': 2, 'pbkD': 1, 'pbkE': 1}) - def test_twopi_layout(self, mock_graph, mock_undirected, mock_paths): - # We mock the edges generator to ensure the order in which they appear - mock_graph.edges = Mock(return_value=self.certifications) - mock_graph.nodes = Mock(return_value=self.identities_pubkeys) - mock_graph.to_undirected = Mock(return_value=mock_undirected) - mock_undirected.nodes = Mock(return_value=self.identities_pubkeys) - mock_undirected.edges = Mock(return_value=self.certifications) - - pos = ExplorerScene.twopi_layout(mock_graph, 'pbkA') - - self.assertAlmostEqual(pos['pbkA'][0], 1 * math.cos(0.0) * 100) - self.assertAlmostEqual(pos['pbkB'][0], 2 * math.cos(1 / 3 * math.pi) * 100) - self.assertAlmostEqual(pos['pbkC'][0], 3 * math.cos(1 / 3 * math.pi) * 100) - self.assertAlmostEqual(pos['pbkD'][0], 2 * math.cos(math.pi) * 100) - self.assertAlmostEqual(pos['pbkE'][0], 2 * math.cos(5 / 3 * math.pi) * 100) - - self.assertAlmostEqual(pos['pbkA'][1], 1 * math.sin(0.0) * 100) - self.assertAlmostEqual(pos['pbkB'][1], 2 * math.sin(1 / 3 * math.pi) * 100) - self.assertAlmostEqual(pos['pbkC'][1], 3 * math.sin(1 / 3 * math.pi) * 100) - self.assertAlmostEqual(pos['pbkD'][1], 2 * math.sin(math.pi) * 100) - self.assertAlmostEqual(pos['pbkE'][1], 2 * math.sin(5 / 3 * math.pi) * 100) \ No newline at end of file diff --git a/src/sakia/tests/unit/gui/views/test_wot_edge.py b/src/sakia/tests/unit/gui/views/test_wot_edge.py deleted file mode 100644 index 6c0632b97e15f9e5eace680549297129cec79ba9..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_wot_edge.py +++ /dev/null @@ -1,76 +0,0 @@ -import unittest -from unittest.mock import patch -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.gui.views.edges import WotEdge -from sakia.core.graph.constants import EdgeStatus - - -class TestWotEdge(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_create_wot_edge(self): - metadata = { - 'status': EdgeStatus.STRONG, - 'confirmation_text': "0/6" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - edge = WotEdge("A", "B", metadata, nx_pos) - self.assertEqual(edge.source, "A") - self.assertEqual(edge.destination, "B") - self.assertEqual(edge.destination_point.x(), 10) - self.assertEqual(edge.destination_point.y(), 20) - self.assertEqual(edge.source_point.x(), 0) - self.assertEqual(edge.source_point.y(), 5) - self.assertEqual(edge.status, EdgeStatus.STRONG) - - self.lp.run_until_complete(exec_test()) - - @patch('PyQt5.QtGui.QPainter') - @patch('PyQt5.QtWidgets.QWidget') - def test_paint(self, painter, widget): - metadata = { - 'status': EdgeStatus.STRONG, - 'confirmation_text': "0/6" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - - async def exec_test(): - edge = WotEdge("A", "B", metadata, nx_pos) - edge.paint(painter, 0, widget) - - self.lp.run_until_complete(exec_test()) - - @patch('PyQt5.QtGui.QPainter') - @patch('PyQt5.QtWidgets.QWidget') - def test_bounding_rect(self, painter, widget): - metadata = { - 'status': EdgeStatus.STRONG, - 'confirmation_text': "0/6" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - - async def exec_test(): - edge = WotEdge("A", "B", metadata, nx_pos) - bounding_rect = edge.boundingRect() - self.assertAlmostEqual(bounding_rect.x(), -3.0, delta=5) - self.assertAlmostEqual(bounding_rect.y(), 2.0, delta=5) - self.assertAlmostEqual(bounding_rect.width(), 16.0, delta=5) - self.assertAlmostEqual(bounding_rect.height(), 21.0, delta=5) - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/gui/views/test_wot_node.py b/src/sakia/tests/unit/gui/views/test_wot_node.py deleted file mode 100644 index 9577447d7408b2b06cdc323781bc56919c1e5ff7..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/gui/views/test_wot_node.py +++ /dev/null @@ -1,80 +0,0 @@ -import unittest -from unittest.mock import patch -from PyQt5.QtCore import QLocale -from PyQt5.QtGui import QPainter -from PyQt5.QtWidgets import QStyleOptionGraphicsItem, QWidget -from sakia.tests import QuamashTest -from sakia.gui.views.nodes import WotNode -from sakia.core.graph.constants import NodeStatus - - -class TestWotNode(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_create_wot_node(self): - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "UserA", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = WotNode(("A", metadata), nx_pos) - self.assertEqual(node.id, "A") - self.assertEqual(node.metadata['status'], NodeStatus.NEUTRAL) - self.assertEqual(node.x(), 0) - self.assertEqual(node.y(), 5) - self.assertEqual(node.status_wallet, False) - self.assertEqual(node.status_member, True) - self.assertEqual(node.text, "UserA") - self.assertEqual(node.toolTip(), "UserA - TestTooltip") - - self.lp.run_until_complete(exec_test()) - - def test_paint(self): - painter = QPainter() - widget = QWidget() - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "UserA", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = WotNode(("A", metadata), nx_pos) - node.paint(painter, QStyleOptionGraphicsItem(), widget) - - self.lp.run_until_complete(exec_test()) - - @patch('PyQt5.QtGui.QPainter', spec=QPainter) - @patch('PyQt5.QtWidgets.QWidget') - def test_bounding_rect(self, painter, widget): - metadata = { - 'status': NodeStatus.NEUTRAL, - 'text': "A", - 'tooltip': "TestTooltip" - } - nx_pos = { - "A": (0, 5), - "B": (10, 20) - } - async def exec_test(): - node = WotNode(("A", metadata), nx_pos) - bounding_rect = node.boundingRect() - self.assertAlmostEqual(bounding_rect.x(), -0.5, delta=1) - self.assertAlmostEqual(bounding_rect.y(), -0.5, delta=1) - self.assertAlmostEqual(bounding_rect.width(), 19.59375, delta=15) - self.assertAlmostEqual(bounding_rect.height(), 37.0, delta=15) - - self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/tools/test_decorators.py b/src/sakia/tests/unit/tools/test_decorators.py deleted file mode 100644 index 60a5eb33c64ddce10f015a2d7ac6ef6ecb169d30..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/tools/test_decorators.py +++ /dev/null @@ -1,114 +0,0 @@ -import unittest -import asyncio -from PyQt5.QtCore import QLocale -from sakia.tests import QuamashTest -from sakia.tools.decorators import asyncify, once_at_a_time, cancel_once_task - - -class TestDecorators(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_run_only_once(self): - class TaskRunner: - def __init__(self): - pass - - @once_at_a_time - @asyncify - async def some_long_task(self, name, callback): - await asyncio.sleep(1) - callback(name) - - task_runner = TaskRunner() - calls = {'A': 0, 'B': 0, 'C': 0} - - def incrementer(name): - nonlocal calls - calls[name] += 1 - - async def exec_test(): - await asyncio.sleep(3) - - self.lp.call_soon(lambda: task_runner.some_long_task("A", incrementer)) - self.lp.call_soon(lambda: task_runner.some_long_task("B", incrementer)) - self.lp.call_soon(lambda: task_runner.some_long_task("C", incrementer)) - self.lp.run_until_complete(exec_test()) - self.assertEqual(calls["A"], 0) - self.assertEqual(calls["B"], 0) - self.assertEqual(calls["C"], 1) - - def test_cancel_once(self): - class TaskRunner: - def __init__(self): - pass - - @once_at_a_time - @asyncify - async def some_long_task(self, name, callback): - await asyncio.sleep(1) - callback(name) - await asyncio.sleep(1) - callback(name) - - def cancel_long_task(self): - cancel_once_task(self, self.some_long_task) - - task_runner = TaskRunner() - calls = {'A': 0, 'B': 0} - - def incrementer(name): - nonlocal calls - calls[name] += 1 - - async def exec_test(): - await asyncio.sleep(3) - - self.lp.call_soon(lambda: task_runner.some_long_task("A", incrementer)) - self.lp.call_soon(lambda: task_runner.some_long_task("B", incrementer)) - self.lp.call_later(1.5, lambda: task_runner.cancel_long_task()) - self.lp.run_until_complete(exec_test()) - self.assertEqual(calls["A"], 0) - self.assertEqual(calls["B"], 1) - - def test_cancel_once_two_times(self): - class TaskRunner: - def __init__(self): - pass - - @once_at_a_time - @asyncify - async def some_long_task(self, name, callback): - await asyncio.sleep(1) - callback(name) - await asyncio.sleep(1) - callback(name) - - def cancel_long_task(self): - cancel_once_task(self, self.some_long_task) - - task_runner = TaskRunner() - calls = {'A': 0, 'B': 0, 'C': 0, 'D': 0} - - def incrementer(name): - nonlocal calls - calls[name] += 1 - - async def exec_test(): - await asyncio.sleep(6) - - self.lp.call_soon(lambda: task_runner.some_long_task("A", incrementer)) - self.lp.call_soon(lambda: task_runner.some_long_task("B", incrementer)) - self.lp.call_later(1.5, lambda: task_runner.cancel_long_task()) - self.lp.call_later(2, lambda: task_runner.some_long_task("C", incrementer)) - self.lp.call_later(2.1, lambda: task_runner.some_long_task("D", incrementer)) - self.lp.call_later(3.5, lambda: task_runner.cancel_long_task()) - self.lp.run_until_complete(exec_test()) - self.assertEqual(calls["A"], 0) - self.assertEqual(calls["B"], 1) - self.assertEqual(calls["C"], 0) - self.assertEqual(calls["D"], 1) diff --git a/src/sakia/tools/exceptions.py b/src/sakia/tools/exceptions.py deleted file mode 100644 index 6044c9b6a354c40bc7444303ebd5f91adf8ebcd6..0000000000000000000000000000000000000000 --- a/src/sakia/tools/exceptions.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Created on 9 févr. 2014 - -@author: inso -""" - - -class Error(Exception): - - def __init__(self, message): - """ - Constructor - """ - self.message = "Error : " + message - - def __str__(self): - return self.message - - -class NotMemberOfCommunityError(Error): - - """ - Exception raised when adding a community the account is not a member of - """ - - def __init__(self, account, community): - """ - Constructor - """ - super() \ - .__init__(account + " is not a member of " + community) - - -class LookupFailureError(Error): - - """ - Exception raised when looking for a person in a community - who isnt present in key list - """ - - def __init__(self, value, community): - """ - Constructor - """ - super() .__init__( - "Person looked by {0} in {1} not found ".format(value, community)) - - -class MembershipNotFoundError(Error): - - """ - Exception raised when looking for a person in a community - who isnt present in key list - """ - - def __init__(self, value, community): - """ - Constructor - """ - super() .__init__( - "Membership searched by " + - value + - " in " + - community + - " not found ") - - -class AlgorithmNotImplemented(Error): - - """ - Exception raised when a coin uses an algorithm not known - """ - - def __init__(self, algo_name): - """ - Constructor - """ - super() \ - .__init__("Algorithm " + algo_name + " not implemented.") - - -class KeyAlreadyUsed(Error): - - """ - Exception raised trying to add an account using - a key already used for another account. - """ - - def __init__(self, new_account, keyid, found_account): - """ - Constructor - """ - super() .__init__( -"""Cannot add account {0} : -the key {1} is already used by {2}""".format(new_account, - keyid, - found_account) - ) - - -class NameAlreadyExists(Error): - - """ - Exception raised trying to add an account using - a key already used for another account. - """ - - def __init__(self, account_name): - """ - Constructor - """ - super() .__init__( - "Cannot add account " + - account_name + - " the name already exists") - - -class BadAccountFile(Error): - - """ - Exception raised trying to add an account using - a key already used for another account. - """ - - def __init__(self, path): - """ - Constructor - """ - super() .__init__( - "File " + path + " is not an account file") - - -class NotEnoughMoneyError(Error): - - """ - Exception raised trying to add an account using - a key already used for another account. - """ - - def __init__(self, available, currency, nb_inputs, requested): - """ - Constructor - """ - super() .__init__( - "Only {0} {1} available in {2} sources, needs {3}" - .format(available, - currency, - nb_inputs, - requested)) - - -class NoPeerAvailable(Error): - """ - Exception raised when a community doesn't have any - peer available. - """ - def __init__(self, currency, nbpeers): - """ - Constructor - """ - super() .__init__( - "No peer answered in {0} community ({1} peers available)" - .format(currency, nbpeers)) - - -class InvalidNodeCurrency(Error): - """ - Exception raised when a node doesn't use the intended currency - """ - def __init__(self, currency, node_currency): - """ - Constructor - """ - super() .__init__( - "Node is working for {0} currency, but should be {1}" - .format(node_currency, currency)) - - -class ContactAlreadyExists(Error): - """ - Exception raised when a community doesn't have any - peer available. - """ - def __init__(self, new_contact, already_contact): - """ - Constructor - """ - super() .__init__( - "Cannot add {0}, he/she has the same pubkey as {1} contact" - .format(new_contact, already_contact)) diff --git a/src/sakia/tools/__init__.py b/tests/__init__.py similarity index 100% rename from src/sakia/tools/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..c2cbb2e651204380e57f24d099c3f106faa4f130 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,223 @@ +import pytest +import asyncio +import quamash +import sqlite3 +import mirage +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) + +from duniterpy.documents import BlockUID +from sakia.app import Application +from sakia.options import SakiaOptions +from sakia.data.files import AppDataFile +from sakia.data.entities import * +from sakia.data.repositories import * +from sakia.services import DocumentsService + +_application_ = [] + + +@pytest.yield_fixture +def event_loop(): + qapplication = get_application() + loop = quamash.QSelectorEventLoop(qapplication) + exceptions = [] + loop.set_exception_handler(lambda l, c: unitttest_exception_handler(exceptions, l, c)) + yield loop + try: + loop.close() + finally: + asyncio.set_event_loop(None) + + for exc in exceptions: + raise exc + + +@pytest.fixture +def meta_repo(): + sqlite3.register_adapter(BlockUID, str) + sqlite3.register_adapter(bool, int) + sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) + con = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES) + meta_repo = SakiaDatabase(con, + ConnectionsRepo(con), IdentitiesRepo(con), + BlockchainsRepo(con), CertificationsRepo(con), TransactionsRepo(con), + NodesRepo(con), SourcesRepo(con), DividendsRepo(con)) + meta_repo.prepare() + meta_repo.upgrade_database() + return meta_repo + + +@pytest.fixture +def sakia_options(tmpdir): + return SakiaOptions(tmpdir.dirname) + + +@pytest.fixture +def app_data(sakia_options): + return AppDataFile.in_config_path(sakia_options.config_path).load_or_init() + + +@pytest.fixture +def user_parameters(): + return UserParameters() + +@pytest.fixture +def application(event_loop, meta_repo, sakia_options, app_data, user_parameters): + app = Application(qapp=get_application(), + loop=event_loop, + options=sakia_options, + app_data=app_data, + parameters=user_parameters, + db=meta_repo) + app.documents_service = DocumentsService.instanciate(app) + return app + + +@pytest.fixture +def fake_server(event_loop): + return event_loop.run_until_complete(mirage.Node.start(None, "test_currency", "12356", "123456", event_loop)) + + +@pytest.fixture +def alice(): + return mirage.User.create("test_currency", "alice", "alicesalt", "alicepassword", BlockUID.empty()) + + +@pytest.fixture +def bob(): + return mirage.User.create("test_currency", "bob", "bobsalt", "bobpassword", BlockUID.empty()) + + +@pytest.fixture +def wrong_bob_uid(): + return mirage.User.create("test_currency", "wrong_bob", "bobsalt", "bobpassword", BlockUID.empty()) + + +@pytest.fixture +def wrong_bob_pubkey(): + return mirage.User.create("test_currency", "bob", "wrongbobsalt", "bobpassword", BlockUID.empty()) + + +@pytest.fixture +def simple_fake_server(fake_server, alice, bob): + fake_server.forge.push(alice.identity()) + fake_server.forge.push(bob.identity()) + fake_server.forge.push(alice.join(BlockUID.empty())) + fake_server.forge.push(bob.join(BlockUID.empty())) + fake_server.forge.push(alice.certify(bob, BlockUID.empty())) + fake_server.forge.push(bob.certify(alice, BlockUID.empty())) + fake_server.forge.forge_block() + fake_server.forge.set_member(alice.key.pubkey, True) + fake_server.forge.set_member(bob.key.pubkey, True) + for i in range(0, 10): + new_user = mirage.User.create("test_currency", "user{0}".format(i), + "salt{0}".format(i), "password{0}".format(i), + fake_server.forge.blocks[-1].blockUID) + fake_server.forge.push(new_user.identity()) + fake_server.forge.push(new_user.join(fake_server.forge.blocks[-1].blockUID)) + fake_server.forge.forge_block() + fake_server.forge.set_member(new_user.key.pubkey, True) + fake_server.forge.generate_dividend() + fake_server.forge.forge_block() + return fake_server + + +@pytest.fixture +def application_with_one_connection(application, simple_fake_server, bob): + current_block = simple_fake_server.forge.blocks[-1] + last_ud_block = [b for b in simple_fake_server.forge.blocks if b.ud][-1] + previous_ud_block = [b for b in simple_fake_server.forge.blocks if b.ud][-2] + origin_block = simple_fake_server.forge.blocks[0] + connection = Connection(currency="test_currency", + pubkey=bob.key.pubkey, + salt=bob.salt, uid=bob.uid, + scrypt_N=4096, scrypt_r=4, scrypt_p=2, + blockstamp=bob.blockstamp) + application.db.connections_repo.insert(connection) + blockchain_parameters = BlockchainParameters(*origin_block.parameters) + blockchain = Blockchain(parameters=blockchain_parameters, + current_buid=current_block.blockUID, + current_members_count=current_block.members_count, + current_mass=simple_fake_server.forge.monetary_mass(current_block.number), + median_time=current_block.mediantime, + last_members_count=previous_ud_block.members_count, + last_ud=last_ud_block.ud, + last_ud_base=last_ud_block.unit_base, + last_ud_time=last_ud_block.mediantime, + previous_mass=simple_fake_server.forge.monetary_mass(previous_ud_block.number), + previous_members_count=previous_ud_block.members_count, + previous_ud=previous_ud_block.ud, + previous_ud_base=previous_ud_block.unit_base, + previous_ud_time=previous_ud_block.mediantime, + currency=simple_fake_server.forge.currency) + application.db.blockchains_repo.insert(blockchain) + for s in simple_fake_server.forge.user_identities[bob.key.pubkey].sources: + application.db.sources_repo.insert(Source(currency=simple_fake_server.forge.currency, + pubkey=bob.key.pubkey, + identifier=s.origin_id, + noffset=s.index, + type=s.source, + amount=s.amount, + base=s.base)) + bob_blockstamp = simple_fake_server.forge.user_identities[bob.key.pubkey].blockstamp + bob_user_identity = simple_fake_server.forge.user_identities[bob.key.pubkey] + bob_ms = bob_user_identity.memberships[-1] + bob_identity = Identity(currency=simple_fake_server.forge.currency, + pubkey=bob.key.pubkey, + uid=bob.uid, + blockstamp=bob_blockstamp, + signature=bob_user_identity.signature, + timestamp=simple_fake_server.forge.blocks[bob_blockstamp.number].mediantime, + written_on=0, + revoked_on=0, + member=bob_user_identity.member, + membership_buid=bob_ms.blockstamp, + membership_timestamp=simple_fake_server.forge.blocks[bob_ms.blockstamp.number].mediantime, + membership_type=bob_ms.type, + membership_written_on=simple_fake_server.forge.blocks[bob_ms.written_on].number) + application.db.identities_repo.insert(bob_identity) + application.instanciate_services() + application.db.nodes_repo.insert(Node(currency=simple_fake_server.forge.currency, + pubkey=simple_fake_server.forge.key.pubkey, + endpoints=simple_fake_server.peer_doc().endpoints, + peer_blockstamp=simple_fake_server.peer_doc().blockUID, + uid="", + current_buid=BlockUID(current_block.number, current_block.sha_hash), + current_ts=current_block.mediantime, + state=Node.ONLINE, + software="duniter", + version="0.40.2")) + application.switch_language() + + return application + + +def unitttest_exception_handler(exceptions, loop, context): + """ + An exception handler which exists the program if the exception + was not catch + :param loop: the asyncio loop + :param context: the exception context + """ + if 'exception' in context: + exception = context['exception'] + else: + exception = BaseException(context['message']) + exceptions.append(exception) + + +def get_application(): + """Get the singleton QApplication""" + from quamash import QApplication + if not len(_application_): + application = QApplication.instance() + if not application: + import sys + application = QApplication(sys.argv) + application.setQuitOnLastWindowClosed(False) + _application_.append(application) + return _application_[0] + diff --git a/res/ui/tx_lifecycle.png b/tests/functional/__init__.py similarity index 100% rename from res/ui/tx_lifecycle.png rename to tests/functional/__init__.py diff --git a/tests/functional/test_certification_dialog.py b/tests/functional/test_certification_dialog.py new file mode 100644 index 0000000000000000000000000000000000000000..0dbd1e0cefa39ba431dc128ee147e76f9b4f5e97 --- /dev/null +++ b/tests/functional/test_certification_dialog.py @@ -0,0 +1,42 @@ +import asyncio +import pytest +from duniterpy.documents import Certification +from PyQt5.QtCore import QLocale, Qt, QEvent +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QDialogButtonBox, QApplication, QMessageBox +from sakia.gui.dialogs.certification.controller import CertificationController + + +@pytest.mark.asyncio +async def test_certification_init_community(application_with_one_connection, fake_server, bob, alice): + certification_dialog = CertificationController.create(None, application_with_one_connection) + + def close_dialog(): + if certification_dialog.view.isVisible(): + certification_dialog.view.close() + + async def exec_test(): + certification_dialog.model.connection.password = bob.password + QTest.keyClicks(certification_dialog.view.search_user.combobox_search.lineEdit(), "nothing") + await asyncio.sleep(1) + certification_dialog.search_user.view.search() + await asyncio.sleep(1) + assert certification_dialog.user_information.model.identity is None + assert not certification_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled() + certification_dialog.view.search_user.combobox_search.lineEdit().clear() + QTest.keyClicks(certification_dialog.view.search_user.combobox_search.lineEdit(), alice.key.pubkey) + await asyncio.sleep(0.1) + certification_dialog.search_user.view.search() + await asyncio.sleep(0.1) + certification_dialog.search_user.view.node_selected.emit(0) + await asyncio.sleep(1) + assert certification_dialog.user_information.model.identity.uid == "alice" + assert certification_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled() + QTest.mouseClick(certification_dialog.view.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) + await asyncio.sleep(0.1) + assert isinstance(fake_server.forge.pool[0], Certification) + + application_with_one_connection.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await certification_dialog.async_exec() + await fake_server.close() diff --git a/tests/functional/test_connection_cfg_dialog.py b/tests/functional/test_connection_cfg_dialog.py new file mode 100644 index 0000000000000000000000000000000000000000..fb831912765a57585bb20ae5ebb7d66c01afd3bc --- /dev/null +++ b/tests/functional/test_connection_cfg_dialog.py @@ -0,0 +1,142 @@ +import asyncio +import pytest +from PyQt5.QtCore import Qt +from PyQt5.QtTest import QTest +from sakia.data.processors import ConnectionsProcessor +from sakia.gui.dialogs.connection_cfg import ConnectionConfigController + + +def assert_key_parameters_behaviour(connection_config_dialog, user): + QTest.keyClicks(connection_config_dialog.view.edit_uid, user.uid) + QTest.keyClicks(connection_config_dialog.view.edit_salt, user.salt) + QTest.keyClicks(connection_config_dialog.view.edit_salt_bis, user.salt) + assert connection_config_dialog.view.button_next.isEnabled() is False + assert connection_config_dialog.view.button_generate.isEnabled() is False + QTest.keyClicks(connection_config_dialog.view.edit_password, user.password) + connection_config_dialog.view.button_next.isEnabled() is False + connection_config_dialog.view.button_generate.isEnabled() is False + QTest.keyClicks(connection_config_dialog.view.edit_password_repeat, user.password + "wrong") + assert connection_config_dialog.view.button_next.isEnabled() is False + assert connection_config_dialog.view.button_generate.isEnabled() is False + connection_config_dialog.view.edit_password_repeat.setText("") + QTest.keyClicks(connection_config_dialog.view.edit_password_repeat, user.password) + assert connection_config_dialog.view.button_next.isEnabled() is True + assert connection_config_dialog.view.button_generate.isEnabled() is True + QTest.mouseClick(connection_config_dialog.view.button_generate, Qt.LeftButton) + assert connection_config_dialog.view.label_info.text() == user.key.pubkey + + +@pytest.mark.asyncio +async def test_register_empty_blockchain(application, fake_server, bob): + connection_config_dialog = ConnectionConfigController.create_connection(None, application) + + def close_dialog(): + if connection_config_dialog.view.isVisible(): + connection_config_dialog.view.close() + + async def exec_test(): + QTest.keyClicks(connection_config_dialog.view.edit_server, fake_server.peer_doc().endpoints[0].ipv4) + connection_config_dialog.view.spinbox_port.setValue(fake_server.peer_doc().endpoints[0].port) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_node + await asyncio.sleep(0.6) + QTest.mouseClick(connection_config_dialog.view.button_register, Qt.LeftButton) + await asyncio.sleep(0.6) + + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_connection + assert_key_parameters_behaviour(connection_config_dialog, bob) + QTest.mouseClick(connection_config_dialog.view.button_next, Qt.LeftButton) + connection_config_dialog.model.connection.password = bob.password + await asyncio.sleep(1) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_services + assert len(ConnectionsProcessor.instanciate(application).connections(fake_server.forge.currency)) == 1 + + application.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await connection_config_dialog.async_exec() + await fake_server.close() + + +@pytest.mark.asyncio +async def test_connect(application, simple_fake_server, bob): + connection_config_dialog = ConnectionConfigController.create_connection(None, application) + + def close_dialog(): + if connection_config_dialog.view.isVisible(): + connection_config_dialog.view.close() + + async def exec_test(): + QTest.keyClicks(connection_config_dialog.view.edit_server, simple_fake_server.peer_doc().endpoints[0].ipv4) + connection_config_dialog.view.spinbox_port.setValue(simple_fake_server.peer_doc().endpoints[0].port) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_node + QTest.mouseClick(connection_config_dialog.view.button_connect, Qt.LeftButton) + await asyncio.sleep(1) + + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_connection + assert_key_parameters_behaviour(connection_config_dialog, bob) + QTest.mouseClick(connection_config_dialog.view.button_next, Qt.LeftButton) + await asyncio.sleep(1) + + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_services + assert len(ConnectionsProcessor.instanciate(application).connections(simple_fake_server.forge.currency)) == 1 + + application.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await connection_config_dialog.async_exec() + await simple_fake_server.close() + + +@pytest.mark.asyncio +async def test_connect_wrong_uid(application, simple_fake_server, wrong_bob_uid, bob): + connection_config_dialog = ConnectionConfigController.create_connection(None, application) + + def close_dialog(): + if connection_config_dialog.view.isVisible(): + connection_config_dialog.view.close() + + async def exec_test(): + await asyncio.sleep(1) + QTest.keyClicks(connection_config_dialog.view.edit_server, simple_fake_server.peer_doc().endpoints[0].ipv4) + connection_config_dialog.view.spinbox_port.setValue(simple_fake_server.peer_doc().endpoints[0].port) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_node + QTest.mouseClick(connection_config_dialog.view.button_connect, Qt.LeftButton) + await asyncio.sleep(1) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_connection + assert_key_parameters_behaviour(connection_config_dialog, wrong_bob_uid) + QTest.mouseClick(connection_config_dialog.view.button_next, Qt.LeftButton) + assert connection_config_dialog.view.label_info.text(), """Your pubkey or UID is different on the network. +Yours : {0}, the network : {1}""".format(wrong_bob_uid.uid, bob.uid) + connection_config_dialog.view.close() + + application.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await connection_config_dialog.async_exec() + await simple_fake_server.close() + + +@pytest.mark.asyncio +async def test_connect_wrong_pubkey(application, simple_fake_server, wrong_bob_pubkey, bob): + connection_config_dialog = ConnectionConfigController.create_connection(None, application) + + def close_dialog(): + if connection_config_dialog.view.isVisible(): + connection_config_dialog.view.close() + + async def exec_test(): + await asyncio.sleep(1) + QTest.keyClicks(connection_config_dialog.view.edit_server, simple_fake_server.peer_doc().endpoints[0].ipv4) + connection_config_dialog.view.spinbox_port.setValue(simple_fake_server.peer_doc().endpoints[0].port) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_node + QTest.mouseClick(connection_config_dialog.view.button_connect, Qt.LeftButton) + await asyncio.sleep(1) + assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_connection + assert_key_parameters_behaviour(connection_config_dialog, wrong_bob_pubkey) + QTest.mouseClick(connection_config_dialog.view.button_next, Qt.LeftButton) + assert connection_config_dialog.view.label_info.text(), """Your pubkey or UID is different on the network. +Yours : {0}, the network : {1}""".format(wrong_bob_pubkey.pubkey, bob.pubkey) + connection_config_dialog.view.close() + + application.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await connection_config_dialog.async_exec() + await simple_fake_server.close() + diff --git a/tests/functional/test_preferences_dialog.py b/tests/functional/test_preferences_dialog.py new file mode 100644 index 0000000000000000000000000000000000000000..e9e69371427dd063cae795b6dab7c202c0df523d --- /dev/null +++ b/tests/functional/test_preferences_dialog.py @@ -0,0 +1,13 @@ +from sakia.gui.preferences import PreferencesDialog + + +def test_preferences_default(application): + preferences_dialog = PreferencesDialog(application) + assert preferences_dialog.combo_language.currentText() == application.parameters.lang + assert preferences_dialog.combo_referential.currentIndex() == application.parameters.referential + assert preferences_dialog.checkbox_expertmode.isChecked() == application.parameters.expert_mode + assert preferences_dialog.checkbox_maximize.isChecked() == application.parameters.maximized + assert preferences_dialog.checkbox_notifications.isChecked() == application.parameters.notifications + assert preferences_dialog.checkbox_proxy.isChecked() == application.parameters.enable_proxy + assert preferences_dialog.edit_proxy_address.text() == application.parameters.proxy_address + assert preferences_dialog.spinbox_proxy_port.value() == application.parameters.proxy_port diff --git a/tests/functional/test_transfer_dialog.py b/tests/functional/test_transfer_dialog.py new file mode 100644 index 0000000000000000000000000000000000000000..74a63db9cb32b88eb6c3ce43f8dad0abac573b2f --- /dev/null +++ b/tests/functional/test_transfer_dialog.py @@ -0,0 +1,32 @@ +import asyncio +import pytest +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QApplication +from sakia.gui.dialogs.transfer.controller import TransferController +from duniterpy.documents import Transaction + + +@pytest.mark.asyncio +async def test_transfer(application_with_one_connection, simple_fake_server, bob, alice): + transfer_dialog = TransferController.create(None, application_with_one_connection) + + def close_dialog(): + if transfer_dialog.view.isVisible(): + transfer_dialog.view.close() + + async def exec_test(): + transfer_dialog.model.connection.password = bob.password + QTest.mouseClick(transfer_dialog.view.radio_pubkey, Qt.LeftButton) + QTest.keyClicks(transfer_dialog.view.edit_pubkey, alice.key.pubkey) + transfer_dialog.view.spinbox_amount.setValue(10) + await asyncio.sleep(0.1) + assert transfer_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled() + QTest.mouseClick(transfer_dialog.view.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) + await asyncio.sleep(0.1) + assert isinstance(simple_fake_server.forge.pool[0], Transaction) + + application_with_one_connection.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await transfer_dialog.async_exec() + await simple_fake_server.close() diff --git a/tests/technical/__init__.py b/tests/technical/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/technical/test_identities_service.py b/tests/technical/test_identities_service.py new file mode 100644 index 0000000000000000000000000000000000000000..d2e2bb9cf26ae6a4fa92d9828edf8a379cb02209 --- /dev/null +++ b/tests/technical/test_identities_service.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.mark.asyncio +async def test_new_block_with_unknown_identities(application_with_one_connection, fake_server, bob, alice): + pass \ No newline at end of file diff --git a/tests/technical/test_transactions_service.py b/tests/technical/test_transactions_service.py new file mode 100644 index 0000000000000000000000000000000000000000..47bd10bda8b2e0085b22976ae2a67ced55b7d675 --- /dev/null +++ b/tests/technical/test_transactions_service.py @@ -0,0 +1,56 @@ +import pytest +from sakia.data.entities import Transaction + + +@pytest.mark.asyncio +async def test_send_tx_then_validate(application_with_one_connection, fake_server, bob, alice): + tx_before_send = application_with_one_connection.transactions_services[fake_server.forge.currency].transfers(bob.key.pubkey) + bob_connection = application_with_one_connection.db.connections_repo.get_one(pubkey=bob.key.pubkey) + await application_with_one_connection.documents_service.send_money(bob_connection, + bob.password, + alice.key.pubkey, 10, 0, "Test comment") + tx_after_send = application_with_one_connection.transactions_services[fake_server.forge.currency].transfers(bob.key.pubkey) + assert len(tx_before_send) + 1 == len(tx_after_send) + assert tx_after_send[-1].state is Transaction.AWAITING + fake_server.forge.forge_block() + fake_server.forge.forge_block() + fake_server.forge.forge_block() + new_blocks = fake_server.forge.blocks[-3:] + await application_with_one_connection.transactions_services[fake_server.forge.currency].handle_new_blocks(new_blocks) + tx_after_parse = application_with_one_connection.transactions_services[fake_server.forge.currency].transfers(bob.key.pubkey) + assert tx_after_parse[-1].state is Transaction.VALIDATED + await fake_server.close() + + +@pytest.mark.asyncio +async def test_receive_tx(application_with_one_connection, fake_server, bob, alice): + tx_before_send = application_with_one_connection.transactions_services[fake_server.forge.currency].transfers(bob.key.pubkey) + fake_server.forge.push(alice.send_money(10, fake_server.forge.user_identities[alice.key.pubkey].sources, bob, + fake_server.forge.blocks[-1].blockUID, "Test receive")) + fake_server.forge.forge_block() + fake_server.forge.forge_block() + fake_server.forge.forge_block() + new_blocks = fake_server.forge.blocks[-3:] + await application_with_one_connection.transactions_services[fake_server.forge.currency].handle_new_blocks(new_blocks) + tx_after_parse = application_with_one_connection.transactions_services[fake_server.forge.currency].transfers(bob.key.pubkey) + assert tx_after_parse[-1].state is Transaction.VALIDATED + assert len(tx_before_send) + 1 == len(tx_after_parse) + await fake_server.close() + + +@pytest.mark.asyncio +async def test_issue_dividend(application_with_one_connection, fake_server, bob): + dividends_before_send = application_with_one_connection.transactions_services[fake_server.forge.currency].dividends(bob.key.pubkey) + fake_server.forge.forge_block() + fake_server.forge.generate_dividend() + fake_server.forge.forge_block() + fake_server.forge.forge_block() + fake_server.forge.generate_dividend() + fake_server.forge.forge_block() + fake_server.forge.forge_block() + new_blocks = fake_server.forge.blocks[-5:] + await application_with_one_connection.transactions_services[fake_server.forge.currency].handle_new_blocks(new_blocks) + dividends_after_parse = application_with_one_connection.transactions_services[fake_server.forge.currency].dividends(bob.key.pubkey) + assert len(dividends_before_send) + 2 == len(dividends_after_parse) + await fake_server.close() + diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/unit/data/__init__.py b/tests/unit/data/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/unit/data/test_appdata_file.py b/tests/unit/data/test_appdata_file.py new file mode 100644 index 0000000000000000000000000000000000000000..99912fc5f0c3befb719c954adcb1d36aa6b66f7d --- /dev/null +++ b/tests/unit/data/test_appdata_file.py @@ -0,0 +1,16 @@ +from sakia.data.entities import AppData +from sakia.data.files import AppDataFile +import tempfile +import unittest +import os + + +class TestAppDataFile(unittest.TestCase): + def test_init_save_load(self): + file = os.path.join(tempfile.mkdtemp(), "params.json") + app_data = AppData() + app_data_file = AppDataFile(file) + app_data.profiles.append("default") + app_data_file.save(app_data) + app_data_2 = app_data_file.load_or_init() + self.assertEqual(app_data, app_data_2) diff --git a/tests/unit/data/test_blockchains_repo.py b/tests/unit/data/test_blockchains_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..869683a6a33d9097bfec4bc26f4437c59becfaf2 --- /dev/null +++ b/tests/unit/data/test_blockchains_repo.py @@ -0,0 +1,187 @@ +from duniterpy.documents import BlockUID + +from sakia.data.entities import Blockchain, BlockchainParameters +from sakia.data.repositories import BlockchainsRepo + + +def test_add_get_drop_blockchain(meta_repo): + blockchains_repo = BlockchainsRepo(meta_repo.conn) + blockchains_repo.insert(Blockchain( + parameters=BlockchainParameters( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66), + current_buid="20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + current_members_count = 10, + current_mass = 1000000, + median_time = 86400, + last_members_count = 5, + last_ud = 100000, + last_ud_base = 0, + last_ud_time = 86400, + previous_mass = 999999, + previous_members_count = 10, + previous_ud = 6543, + previous_ud_base = 0, + previous_ud_time = 86400, + currency = "testcurrency" + )) + blockchain = blockchains_repo.get_one(currency="testcurrency") + assert blockchain.parameters == BlockchainParameters( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66) + assert blockchain.currency == "testcurrency" + assert blockchain.current_buid == BlockUID(20, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + assert blockchain.current_members_count == 10 + + blockchains_repo.drop(blockchain) + blockchain = blockchains_repo.get_one(currency="testcurrency") + assert blockchain is None + +def test_add_get_multiple_blockchain(meta_repo): + blockchains_repo = BlockchainsRepo(meta_repo.conn) + blockchains_repo.insert(Blockchain( + parameters=BlockchainParameters( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66), + + current_buid="20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + current_members_count = 10, + current_mass = 1000000, + median_time = 86400, + last_members_count = 5, + last_ud = 100000, + last_ud_base = 0, + last_ud_time = 86400, + previous_mass = 999999, + previous_members_count = 10, + previous_ud = 6543, + previous_ud_base = 0, + previous_ud_time = 86400, + currency = "testcurrency" + )) + blockchains_repo.insert(Blockchain( + BlockchainParameters( + 0.1, + 86400 * 365, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66), + current_buid="20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + current_members_count = 20, + current_mass = 1000000, + median_time = 86400, + last_members_count = 5, + last_ud = 100000, + last_ud_base = 0, + last_ud_time = 86400, + previous_mass = 999999, + previous_members_count = 10, + previous_ud = 6543, + previous_ud_base = 0, + previous_ud_time = 86400, + currency = "testcurrency2" + )) + + blockchains = blockchains_repo.get_all() + # result sorted by currency name by default + assert 86400 == blockchains[0].parameters.dt + assert "testcurrency" == blockchains[0].currency + assert 10 == blockchains[0].current_members_count + + assert 86400*365 == blockchains[1].parameters.dt + assert "testcurrency2" == blockchains[1].currency + assert 20 == blockchains[1].current_members_count + +def test_add_update_blockchain(meta_repo): + blockchains_repo = BlockchainsRepo(meta_repo.conn) + blockchain = Blockchain( + BlockchainParameters( + 0.1, + 86400, + 100000, + 10800, + 40, + 2629800, + 31557600, + 1, + 0.9, + 604800, + 5, + 12, + 300, + 25, + 10, + 0.66), + current_buid="20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + current_members_count = 10, + current_mass = 1000000, + median_time = 86400, + last_members_count = 5, + last_ud = 100000, + last_ud_base = 0, + last_ud_time = 86400, + previous_mass = 999999, + previous_members_count = 10, + previous_ud = 6543, + previous_ud_base = 0, + previous_ud_time = 86400, + currency = "testcurrency" + ) + blockchains_repo.insert(blockchain) + blockchain.current_members_count = 30 + blockchains_repo.update(blockchain) + blockchain2 = blockchains_repo.get_one(currency="testcurrency") + assert 30 == blockchain2.current_members_count diff --git a/tests/unit/data/test_certifications_repo.py b/tests/unit/data/test_certifications_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..6e76e40213f5d55894eeeeec067d7a50ffe9124c --- /dev/null +++ b/tests/unit/data/test_certifications_repo.py @@ -0,0 +1,73 @@ +from sakia.data.repositories import CertificationsRepo +from sakia.data.entities import Certification + + +def test_add_get_drop_blockchain(meta_repo): + certifications_repo = CertificationsRepo(meta_repo.conn) + certifications_repo.insert(Certification("testcurrency", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 20, + 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 0)) + certification = certifications_repo.get_one(currency="testcurrency", + certifier="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + certified="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + block=20) + assert certification.currency == "testcurrency" + assert certification.certifier == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" + assert certification.certified == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + assert certification.block == 20 + assert certification.timestamp == 1473108382 + assert certification.signature == "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==" + assert certification.written_on == 0 + certifications_repo.drop(certification) + certification = certifications_repo.get_one(currency="testcurrency", + certifier="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + certified="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + block=20) + assert certification is None + + +def test_add_get_multiple_certification(meta_repo): + certifications_repo = CertificationsRepo(meta_repo.conn) + certifications_repo.insert(Certification("testcurrency", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 20, 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 22)) + certifications_repo.insert(Certification("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + 101, 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 105)) + certifications = certifications_repo.get_all(currency="testcurrency") + assert "testcurrency" in [i.currency for i in certifications] + assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [i.certifier for i in certifications] + assert "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" in [i.certifier for i in certifications] + assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [i.certified for i in certifications] + assert "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" in [i.certified for i in certifications] + + +def test_add_update_certification(meta_repo): + certifications_repo = CertificationsRepo(meta_repo.conn) + certification = Certification("testcurrency", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 20, + 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 0) + + certifications_repo.insert(certification) + certification.written_on = 22 + certifications_repo.update(certification) + cert2 = certifications_repo.get_one(currency="testcurrency", + certifier="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + certified="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + block=20) + assert cert2.written_on == 22 + diff --git a/tests/unit/data/test_connections_repo.py b/tests/unit/data/test_connections_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..0f4911312a22f0519e3038e45d8652d9340b64b9 --- /dev/null +++ b/tests/unit/data/test_connections_repo.py @@ -0,0 +1,19 @@ +from sakia.data.repositories import ConnectionsRepo +from sakia.data.entities import Connection + +def test_add_get_drop_connection(meta_repo): + connections_repo = ConnectionsRepo(meta_repo.conn) + connections_repo.insert(Connection("testcurrency", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "somesalt")) + connection = connections_repo.get_one(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + salt="somesalt") + assert connection.currency == "testcurrency" + assert connection.pubkey == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" + assert connection.salt == "somesalt" + connections_repo.drop(connection) + connection = connections_repo.get_one(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + salt="somesalt") + assert connection is None diff --git a/tests/unit/data/test_dividends_repo.py b/tests/unit/data/test_dividends_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..d211b08f288e0d35e32bf707eaefba14c1947e3d --- /dev/null +++ b/tests/unit/data/test_dividends_repo.py @@ -0,0 +1,37 @@ +from sakia.data.repositories import DividendsRepo +from sakia.data.entities import Dividend + + +def test_add_get_drop_dividend(meta_repo): + dividends_repo = DividendsRepo(meta_repo.conn) + dividends_repo.insert(Dividend("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 3, 1346543453, 1565, 1)) + dividend = dividends_repo.get_one(pubkey="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + assert dividend.currency == "testcurrency" + assert dividend.pubkey == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + assert dividend.timestamp == 1346543453 + assert dividend.block_number == 3 + assert dividend.base == 1 + assert dividend.amount == 1565 + + dividends_repo.drop(dividend) + source = dividends_repo.get_one(pubkey="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + assert source is None + + +def test_add_get_multiple_dividends(meta_repo): + dividends_repo = DividendsRepo(meta_repo.conn) + dividends_repo.insert(Dividend("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 3, 1346543453, 1565, 1)) + dividends_repo.insert(Dividend("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 243, 4235252353, 45565, 2)) + dividends = dividends_repo.get_all(currency="testcurrency", pubkey="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + assert "testcurrency" in [s.currency for s in dividends] + assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [s.pubkey for s in dividends] + assert 4235252353 in [s.timestamp for s in dividends] + assert 1346543453 in [s.timestamp for s in dividends] + assert 45565 in [s.amount for s in dividends] + assert 1565 in [s.amount for s in dividends] diff --git a/tests/unit/data/test_identies_repo.py b/tests/unit/data/test_identies_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..42fa6ad057b9ab7d2a4ac79d24087c67b951d89d --- /dev/null +++ b/tests/unit/data/test_identies_repo.py @@ -0,0 +1,70 @@ +from sakia.data.repositories import IdentitiesRepo +from sakia.data.entities import Identity +from duniterpy.documents import BlockUID + + +def test_add_get_drop_identity(meta_repo): + identities_repo = IdentitiesRepo(meta_repo.conn) + identities_repo.insert(Identity("testcurrency", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", + "20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 1473108382)) + identity = identities_repo.get_one(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + uid="john", + blockstamp=BlockUID(20, + "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + ) + assert identity.currency == "testcurrency" + assert identity.pubkey == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" + assert identity.uid == "john" + assert identity.blockstamp.number == 20 + assert identity.blockstamp.sha_hash == "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67" + assert identity.timestamp == 1473108382 + assert identity.signature == "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==" + assert identity.member == False + assert identity.membership_buid == BlockUID.empty() + assert identity.membership_timestamp == 0 + assert identity.membership_written_on == 0 + identities_repo.drop(identity) + identity = identities_repo.get_one(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + uid="john", + blockstamp=BlockUID(20, + "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + ) + assert identity is None + + +def test_add_get_multiple_identity(meta_repo): + identities_repo = IdentitiesRepo(meta_repo.conn) + identities_repo.insert(Identity("testcurrency", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", + "20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 1473108382)) + identities_repo.insert(Identity("testcurrency", "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "doe", + "101-BAD49448A1AD73C978CEDCB8F137D20A5715EBAA739DAEF76B1E28EE67B2C00C", + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 1455433535)) + identities = identities_repo.get_all(currency="testcurrency") + assert "testcurrency" in [i.currency for i in identities] + assert "john" in [i.uid for i in identities] + assert "doe" in [i.uid for i in identities] + + +def test_add_update_identity(meta_repo): + identities_repo = IdentitiesRepo(meta_repo.conn) + identity = Identity("testcurrency", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", + "20-7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + 1473108382) + identities_repo.insert(identity) + identity.member = True + identities_repo.update(identity) + identity2 = identities_repo.get_one(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") + assert identity2.member is True diff --git a/tests/unit/data/test_node_connector.py b/tests/unit/data/test_node_connector.py new file mode 100644 index 0000000000000000000000000000000000000000..8f0515aaf6f07c11484c3fbfac07d63490889d6f --- /dev/null +++ b/tests/unit/data/test_node_connector.py @@ -0,0 +1,18 @@ +from duniterpy.documents import Peer +from sakia.data.connectors import NodeConnector + + +def test_from_peer(): + peer = Peer.from_signed_raw("""Version: 2 +Type: Peer +Currency: meta_brouzouf +PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU +Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 +Endpoints: +BASIC_MERKLED_API duniter.inso.ovh 80 +82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== +""") + connector = NodeConnector.from_peer('meta_brouzouf', peer, None) + assert connector.node.pubkey == "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" + assert connector.node.endpoints[0].inline() == "BASIC_MERKLED_API duniter.inso.ovh 80" + assert connector.node.currency == "meta_brouzouf" diff --git a/tests/unit/data/test_nodes_repo.py b/tests/unit/data/test_nodes_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..ac83284f2442f52db2fd4be24cf4ff86fc36f85d --- /dev/null +++ b/tests/unit/data/test_nodes_repo.py @@ -0,0 +1,94 @@ +from sakia.data.repositories import NodesRepo +from sakia.data.entities import Node +from duniterpy.documents import BlockUID, BMAEndpoint, UnknownEndpoint, block_uid + + +def test_add_get_drop_node(meta_repo): + nodes_repo = NodesRepo(meta_repo.conn) + inserted = Node(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + endpoints="""BASIC_MERKLED_API test-net.duniter.fr 13.222.11.22 9201 +BASIC_MERKLED_API testnet.duniter.org 80 +UNKNOWNAPI some useless information""", + peer_blockstamp=BlockUID.empty(), + uid="doe", + current_buid="15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + current_ts=12376543345, + previous_buid="14-AEFFCB00E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + state=Node.ONLINE, + software="duniter", + version="0.30.17") + nodes_repo.insert(inserted) + node = nodes_repo.get_one(currency="testcurrency", + pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") + assert node.currency == "testcurrency" + assert node.pubkey == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" + assert node.endpoints[0] == BMAEndpoint("test-net.duniter.fr", "13.222.11.22", None, 9201) + assert node.endpoints[1] == BMAEndpoint("testnet.duniter.org", None, None, 80) + assert node.endpoints[2] == UnknownEndpoint("UNKNOWNAPI", ["some", "useless", "information"]) + assert node.previous_buid == block_uid("14-AEFFCB00E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + assert node.current_buid == block_uid("15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + assert node.state == Node.ONLINE + assert node.software == "duniter" + assert node.version == "0.30.17" + assert node.merkle_peers_root == Node.MERKLE_EMPTY_ROOT + assert node.merkle_peers_leaves == tuple() + + nodes_repo.drop(node) + node = nodes_repo.get_one(pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") + assert node is None + + +def test_add_get_multiple_node(meta_repo): + nodes_repo = NodesRepo(meta_repo.conn) + nodes_repo.insert(Node("testcurrency", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + """BASIC_MERKLED_API test-net.duniter.fr 13.222.11.22 9201 +BASIC_MERKLED_API testnet.duniter.org 80 +UNKNOWNAPI some useless information""", + BlockUID.empty(), + "doe", + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 12376543345, + "14-AEFFCB00E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + Node.ONLINE, + "duniter", + "0.30.17")) + nodes_repo.insert(Node("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "BASIC_MERKLED_API test-net.duniter.org 22.22.22.22 9201", + BlockUID.empty(), + "doe", + "18-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 12376543345, + "12-AEFFCB00E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + Node.ONLINE, + "duniter", + "0.30.2a5")) + nodes = nodes_repo.get_all(currency="testcurrency") + assert "testcurrency" in [t.currency for t in nodes] + assert "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" in [n.pubkey for n in nodes] + assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [n.pubkey for n in nodes] + + +def test_add_update_node(meta_repo): + nodes_repo = NodesRepo(meta_repo.conn) + node = Node("testcurrency", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + """BASIC_MERKLED_API test-net.duniter.fr 13.222.11.22 9201 +BASIC_MERKLED_API testnet.duniter.org 80 +UNKNOWNAPI some useless information""", + BlockUID.empty(), + "doe", + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 12376543345, + "14-AEFFCB00E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + Node.ONLINE, + "duniter") + nodes_repo.insert(node) + node.previous_buid = node.current_buid + node.current_buid = "16-77543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67" + nodes_repo.update(node) + node2 = nodes_repo.get_one(pubkey="7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") + assert node2.current_buid == block_uid("16-77543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + assert node2.previous_buid == block_uid("15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") diff --git a/tests/unit/data/test_sources_repo.py b/tests/unit/data/test_sources_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..89fadbfb6e2268396a2edab7562ca2a9af29953f --- /dev/null +++ b/tests/unit/data/test_sources_repo.py @@ -0,0 +1,51 @@ +from sakia.data.repositories import SourcesRepo +from sakia.data.entities import Source + + +def test_add_get_drop_source( meta_repo): + sources_repo = SourcesRepo(meta_repo.conn) + sources_repo.insert(Source("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843", + 3, + "T", + 1565, + 1)) + source = sources_repo.get_one(identifier="0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843") + assert source.currency == "testcurrency" + assert source.pubkey == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + assert source.type == "T" + assert source.amount == 1565 + assert source.base == 1 + assert source.noffset == 3 + + sources_repo.drop(source) + source = sources_repo.get_one(identifier="0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843") + assert source is None + + +def test_add_get_multiple_source(meta_repo): + sources_repo = SourcesRepo(meta_repo.conn) + sources_repo.insert(Source("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843", + 3, + "T", + 1565, + 1)) + sources_repo.insert(Source("testcurrency", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "2pyPsXM8UCB88jP2NRM4rUHxb63qm89JMEWbpoRrhyDK", + 22635, + "D", + 726946, + 1)) + sources = sources_repo.get_all(currency="testcurrency", pubkey="FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + assert "testcurrency" in [s.currency for s in sources] + assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [s.pubkey for s in sources] + assert "2pyPsXM8UCB88jP2NRM4rUHxb63qm89JMEWbpoRrhyDK" in [s.identifier for s in sources] + assert "T" in [s.type for s in sources] + assert "D" in [s.type for s in sources] + assert 726946 in [s.amount for s in sources] + assert 1565 in [s.amount for s in sources] + assert "0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843" in [s.identifier for s in sources] diff --git a/tests/unit/data/test_transactions_repo.py b/tests/unit/data/test_transactions_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..6b2010d5c31856c2bf6cc31f4173c15f0f537e13 --- /dev/null +++ b/tests/unit/data/test_transactions_repo.py @@ -0,0 +1,92 @@ +from sakia.data.repositories import TransactionsRepo +from sakia.data.entities import Transaction + + +def test_add_get_drop_transaction(meta_repo): + transactions_repo = TransactionsRepo(meta_repo.conn) + transactions_repo.insert(Transaction("testcurrency", + "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 1565, + 1, + "", + 0, + Transaction.TO_SEND)) + transaction = transactions_repo.get_one(sha_hash="FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365") + assert transaction.currency == "testcurrency" + assert transaction.sha_hash == "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365" + assert transaction.written_block == 20 + assert transaction.blockstamp.number == 15 + assert transaction.blockstamp.sha_hash == "76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67" + assert transaction.timestamp == 1473108382 + assert transaction.signature == "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==" + assert transaction.amount == 1565 + assert transaction.amount_base == 1 + assert transaction.issuer == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" + assert transaction.receiver == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + assert transaction.comment == "" + assert transaction.txid == 0 + transactions_repo.drop(transaction) + transaction = transactions_repo.get_one(sha_hash="FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365") + assert transaction is None + + +def test_add_get_multiple_transaction(meta_repo): + transactions_repo = TransactionsRepo(meta_repo.conn) + transactions_repo.insert(Transaction("testcurrency", + "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5", + 12, + "543-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + 14, + 2, + "Test", + 2, + Transaction.TO_SEND)) + transactions_repo.insert(Transaction("testcurrency", + "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 1565, + 1, + "", + 0, + Transaction.TO_SEND)) + transactions = transactions_repo.get_all(currency="testcurrency") + assert "testcurrency" in [t.currency for t in transactions] + assert "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" in [t.receiver for t in transactions] + assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [t.issuer for t in transactions] + + +def test_add_update_transaction(meta_repo): + transactions_repo = TransactionsRepo(meta_repo.conn) + transaction = Transaction("testcurrency", + "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + 1473108382, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + 1565, + 1, + "", + 0, + Transaction.TO_SEND) + transactions_repo.insert(transaction) + transaction.written_on = None + transactions_repo.update(transaction) + transaction2 = transactions_repo.get_one(sha_hash="FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365") + assert transaction2.written_block == 20 diff --git a/tests/unit/data/test_user_parameters_file.py b/tests/unit/data/test_user_parameters_file.py new file mode 100644 index 0000000000000000000000000000000000000000..a001f1998c106de489924fa1e9e786436673bba7 --- /dev/null +++ b/tests/unit/data/test_user_parameters_file.py @@ -0,0 +1,14 @@ +from sakia.data.entities import UserParameters +from sakia.data.files import UserParametersFile +import tempfile +import os + + +def test_init_save_load(): + file = os.path.join(tempfile.mkdtemp(), "params.json") + user_parameters = UserParameters() + user_parameters_file = UserParametersFile(file) + user_parameters.proxy_address = "test.fr" + user_parameters_file.save(user_parameters) + user_parameters_2 = user_parameters_file.load_or_init() + assert user_parameters == user_parameters_2 diff --git a/tests/unit/gui/__init__.py b/tests/unit/gui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/unit/gui/test_generic_tree.py b/tests/unit/gui/test_generic_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..8a462f5ffe6319a2fd57451e29f1b6056b1332d1 --- /dev/null +++ b/tests/unit/gui/test_generic_tree.py @@ -0,0 +1,56 @@ +from PyQt5.QtCore import QModelIndex +from sakia.models.generic_tree import GenericTreeModel + + +def test_generic_tree(): + data = [ + { + 'node': { + 'title': "Default Profile" + }, + 'children': [ + { + 'node': { + 'title': "Test net (inso)" + }, + 'children': [ + { + 'node': { + 'title': "Transactions" + }, + 'children': [] + }, + { + 'node': { + 'title': "Network" + }, + 'children': [] + } + ] + }, + { + 'node': { + 'title': "Le sou" + }, + 'children': [ + { + 'node': { + 'title': "Transactions" + }, + 'children': {} + }, + { + 'node': { + 'title': "Network" + }, + 'children': { + } + } + ] + } + ], + } + ] + tree_model = GenericTreeModel.create("Test", data) + assert tree_model.columnCount(QModelIndex()) == 1 + assert tree_model.rowCount(QModelIndex()) == 1 diff --git a/tests/unit/money/__init__.py b/tests/unit/money/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/unit/money/test_quantitative.py b/tests/unit/money/test_quantitative.py new file mode 100644 index 0000000000000000000000000000000000000000..8ae1f3e482068eb61b93dc6e595a019f799526f4 --- /dev/null +++ b/tests/unit/money/test_quantitative.py @@ -0,0 +1,90 @@ +from sakia.money import Quantitative + + +def test_units(application_with_one_connection, bob): + referential = Quantitative(0, bob.currency, application_with_one_connection, None) + assert referential.units == "TC" + + +def test_diff_units(application_with_one_connection, bob): + referential = Quantitative(0, bob.currency, application_with_one_connection, None) + assert referential.units == "TC" + + +def test_value(application_with_one_connection, bob): + referential = Quantitative(101010110, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == 1010101.10 + + +def test_differential(application_with_one_connection, bob): + referential = Quantitative(110, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == 1.10 + + +def test_localized_no_si(application_with_one_connection, bob): + referential = Quantitative(101010110, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True) + assert value == "1,010,101.10 TC" + + +def test_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Quantitative(101010000, bob.currency, application_with_one_connection, None) + blockchain = application_with_one_connection.db.blockchains_repo.get_one(currency=bob.currency) + blockchain.last_ud_base = 3 + application_with_one_connection.db.blockchains_repo.update(blockchain) + value = referential.localized(units=True, show_base=True) + assert value == "1,010.10 x10³ TC" + + +def test_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Quantitative(101010110, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=False) + assert value == "1,010,101.10" + + +def test_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Quantitative(101010000, bob.currency, application_with_one_connection, None) + blockchain = application_with_one_connection.db.blockchains_repo.get_one(currency=bob.currency) + blockchain.last_ud_base = 3 + application_with_one_connection.db.blockchains_repo.update(blockchain) + value = referential.localized(units=False, show_base=True) + assert value == "1,010.10 x10³" + + +def test_diff_localized_no_si(application_with_one_connection, bob): + referential = Quantitative(101010110, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=True) + assert value == "1,010,101.10 TC" + + +def test_diff_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Quantitative(101010000, bob.currency, application_with_one_connection, None) + blockchain = application_with_one_connection.db.blockchains_repo.get_one(currency=bob.currency) + blockchain.last_ud_base = 3 + application_with_one_connection.db.blockchains_repo.update(blockchain) + + value = referential.diff_localized(units=True, show_base=True) + assert value == "1,010.10 x10³ TC" + + +def test_diff_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Quantitative(101010110, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=False, show_base=False) + assert value == "1,010,101.10" + + +def test_diff_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Quantitative(10100000000, bob.currency, application_with_one_connection, None) + blockchain = application_with_one_connection.db.blockchains_repo.get_one(currency=bob.currency) + blockchain.last_ud_base = 6 + application_with_one_connection.db.blockchains_repo.update(blockchain) + value = referential.diff_localized(units=False, show_base=True) + assert value == "101.00 x10â¶" diff --git a/tests/unit/money/test_quantitative_zsum.py b/tests/unit/money/test_quantitative_zsum.py new file mode 100644 index 0000000000000000000000000000000000000000..2bbe25dc84a5baf65ac626a2bc7d2a5e0e8bcba3 --- /dev/null +++ b/tests/unit/money/test_quantitative_zsum.py @@ -0,0 +1,83 @@ +from sakia.money import QuantitativeZSum + + +def test_units(application_with_one_connection, bob): + referential = QuantitativeZSum(0, bob.currency, application_with_one_connection, None) + assert referential.units == "Q0 TC" + + +def test_diff_units(application_with_one_connection, bob): + referential = QuantitativeZSum(0, bob.currency, application_with_one_connection, None) + assert referential.units == "Q0 TC" + + +def test_value(application_with_one_connection, bob): + referential = QuantitativeZSum(110, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == -10.79 + + +def test_differential(application_with_one_connection, bob): + referential = QuantitativeZSum(110, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == -10.79 + + +def test_localized_no_si(application_with_one_connection, bob): + referential = QuantitativeZSum(110, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True) + assert value == "-10.79 Q0 TC" + + +def test_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = QuantitativeZSum(110 * 1000, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True, show_base=True) + assert value == "1,088.11 Q0 TC" + + +def test_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = QuantitativeZSum(110, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=False) + assert value == "-10.79" + + +def test_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = QuantitativeZSum(110 * 1000, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=True) + assert value == "1,088.11 Q0" + + +def test_diff_localized_no_si(application_with_one_connection, bob): + referential = QuantitativeZSum(110 * 1000, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=True) + assert value == "1,100.00 TC" + + +def test_diff_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = QuantitativeZSum(101000000, bob.currency, application_with_one_connection, None) + blockchain = application_with_one_connection.db.blockchains_repo.get_one(currency=bob.currency) + blockchain.last_ud_base = 3 + application_with_one_connection.db.blockchains_repo.update(blockchain) + value = referential.diff_localized(units=True, show_base=True) + assert value == "1,010.00 x10³ TC" + + +def test_diff_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = QuantitativeZSum(101010110, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=False, show_base=False) + assert value == "1,010,101.10" + + +def test_diff_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = QuantitativeZSum(101000000, bob.currency, application_with_one_connection, None) + blockchain = application_with_one_connection.db.blockchains_repo.get_one(currency=bob.currency) + blockchain.last_ud_base = 3 + application_with_one_connection.db.blockchains_repo.update(blockchain) + value = referential.diff_localized(units=False, show_base=True) + assert value == "1,010.00 x10³" diff --git a/tests/unit/money/test_relative.py b/tests/unit/money/test_relative.py new file mode 100644 index 0000000000000000000000000000000000000000..0bb50bf24fdefbb07b782489eb1a8231c1335c7e --- /dev/null +++ b/tests/unit/money/test_relative.py @@ -0,0 +1,80 @@ +import pytest +from sakia.money import Relative + + +def test_units(application_with_one_connection, bob): + referential = Relative(0, bob.currency, application_with_one_connection, None) + assert referential.units == "UD TC" + + +def test_diff_units(application_with_one_connection, bob): + referential = Relative(0, bob.currency, application_with_one_connection, None) + assert referential.units == "UD TC" + + +def test_value(application_with_one_connection, bob): + referential = Relative(13555300, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == pytest.approx(58177.253218) + + +def test_differential(application_with_one_connection, bob): + referential = Relative(11, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == pytest.approx(0.0472103) + + +def test_localized_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(11, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True) + assert value == "0.047210 UD TC" + + +def test_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(1, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True, show_base=True) + assert value == "0.004292 UD TC" + + +def test_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(11, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=False) + assert value == "0.047210" + + +def test_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(1, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=True) + assert value == "0.004292" + + +def test_diff_localized_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(11, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=True) + assert value == "0.047210 UD TC" + + +def test_diff_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(1, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=True, show_base=True) + assert value, "9.090909 x10â» UD TC" + + +def test_diff_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(1, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=False, show_base=False) + assert value == "0.004292" + + +def test_diff_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = Relative(1, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=False, show_base=True) + assert value == "0.004292" diff --git a/tests/unit/money/test_relative_zsum.py b/tests/unit/money/test_relative_zsum.py new file mode 100644 index 0000000000000000000000000000000000000000..e25b410d6bbba7ddb59c4e63507cb3f50ff816cc --- /dev/null +++ b/tests/unit/money/test_relative_zsum.py @@ -0,0 +1,82 @@ +from pytest import approx +from sakia.money import RelativeZSum + + +def test_units(application_with_one_connection, bob): + referential = RelativeZSum(0, bob.currency, application_with_one_connection, None) + assert referential.units == "R0 TC" + + +def test_diff_units(application_with_one_connection, bob): + referential = RelativeZSum(0, bob.currency, application_with_one_connection, None) + assert referential.units == "R0 TC" + + +def test_value(application_with_one_connection, bob): + referential = RelativeZSum(2702, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == approx(8.70007) + + +def test_differential(application_with_one_connection, bob): + referential = RelativeZSum(111, bob.currency, application_with_one_connection, None) + value = referential.value() + assert value == approx(-3.521619496) + + +def test_localized_no_si(application_with_one_connection, fake_server, bob): + referential = RelativeZSum(110, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True) + assert value == "-3.53 R0 TC" + + +def test_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + + referential = RelativeZSum(1, bob.currency, application_with_one_connection, None) + value = referential.localized(units=True, show_base=True) + assert value == "-4.040487 R0 TC" + + +def test_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + + referential = RelativeZSum(110, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=False) + assert value == "-3.526336" + + +def test_localized_no_units_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + + referential = RelativeZSum(1, bob.currency, application_with_one_connection, None) + value = referential.localized(units=False, show_base=True) + assert value == "-4.040487" + + +def test_diff_localized_no_si(application_with_one_connection, bob): + referential = RelativeZSum(11, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=True) + assert value == "0.05 UD TC" + + +def test_diff_localized_with_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + + referential = RelativeZSum(1, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=True, show_base=True) + assert value == "0.004292 UD TC" + + +def test_diff_localized_no_units_no_si(application_with_one_connection, bob): + application_with_one_connection.parameters.digits_after_comma = 6 + referential = RelativeZSum(90, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=False, show_base=False) + assert value == "0.386266" + + +def test_diff_localized_no_units_with_si(application_with_one_connection, bob): + + referential = RelativeZSum(90, bob.currency, application_with_one_connection, None) + value = referential.diff_localized(units=False, show_base=True) + assert value == "0.39" diff --git a/tests/unit/test_decorators.py b/tests/unit/test_decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..ca38c4332ca7f254d5e5e74edb9a4f960c657dd7 --- /dev/null +++ b/tests/unit/test_decorators.py @@ -0,0 +1,152 @@ +import asyncio +import pytest +from sakia.decorators import asyncify, once_at_a_time, cancel_once_task + + +@pytest.mark.asyncio +async def test_run_only_once(): + class TaskRunner: + def __init__(self): + pass + + @once_at_a_time + @asyncify + async def some_long_task(self, name, callback): + await asyncio.sleep(1) + callback(name) + + task_runner = TaskRunner() + calls = {'A': 0, 'B': 0, 'C': 0} + + def incrementer(name): + nonlocal calls + calls[name] += 1 + + async def exec_test(): + await asyncio.sleep(3) + + asyncio.ensure_future(task_runner.some_long_task("A", incrementer)) + asyncio.ensure_future(task_runner.some_long_task("B", incrementer)) + asyncio.ensure_future(task_runner.some_long_task("C", incrementer)) + await exec_test() + assert calls["A"] == 0 + assert calls["B"] == 0 + assert calls["C"] == 1 + + +@pytest.mark.asyncio +async def test_cancel_once(application): + class TaskRunner: + def __init__(self): + pass + + @once_at_a_time + @asyncify + async def some_long_task(self, name, callback): + await asyncio.sleep(1) + callback(name) + await asyncio.sleep(1) + callback(name) + + def cancel_long_task(self): + cancel_once_task(self, self.some_long_task) + + task_runner = TaskRunner() + calls = {'A': 0, 'B': 0} + + def incrementer(name): + nonlocal calls + calls[name] += 1 + + async def exec_test(): + await asyncio.sleep(3) + + application.loop.call_soon(lambda: task_runner.some_long_task("A", incrementer)) + application.loop.call_soon(lambda: task_runner.some_long_task("B", incrementer)) + application.loop.call_later(1.5, lambda: task_runner.cancel_long_task()) + await exec_test() + assert calls["A"] == 0 + assert calls["B"] == 1 + + +@pytest.mark.asyncio +async def test_cancel_once_two_times(application): + class TaskRunner: + def __init__(self): + pass + + @once_at_a_time + @asyncify + async def some_long_task(self, name, callback): + await asyncio.sleep(1) + callback(name) + await asyncio.sleep(1) + callback(name) + + def cancel_long_task(self): + cancel_once_task(self, self.some_long_task) + + task_runner = TaskRunner() + calls = {'A': 0, 'B': 0, 'C': 0, 'D': 0} + + def incrementer(name): + nonlocal calls + calls[name] += 1 + + async def exec_test(): + await asyncio.sleep(6) + + application.loop.call_soon(lambda: task_runner.some_long_task("A", incrementer)) + application.loop.call_soon(lambda: task_runner.some_long_task("B", incrementer)) + application.loop.call_later(1.5, lambda: task_runner.cancel_long_task()) + application.loop.call_later(2, lambda: task_runner.some_long_task("C", incrementer)) + application.loop.call_later(2.1, lambda: task_runner.some_long_task("D", incrementer)) + application.loop.call_later(3.5, lambda: task_runner.cancel_long_task()) + await exec_test() + assert calls["A"] == 0 + assert calls["B"] == 1 + assert calls["C"] == 0 + assert calls["D"] == 1 + + +@pytest.mark.asyncio +async def test_two_runners(): + class TaskRunner: + def __init__(self, name): + self.some_long_task(name, incrementer) + + @classmethod + def create(cls, name): + return cls(name) + + @once_at_a_time + @asyncify + async def some_long_task(self, name, callback): + await asyncio.sleep(1) + callback(name) + await asyncio.sleep(1) + callback(name) + + def cancel_long_task(self): + cancel_once_task(self, self.some_long_task) + + calls = {'A': 0, 'B': 0, 'C': 0} + + def incrementer(name): + nonlocal calls + calls[name] += 1 + + async def exec_test(): + tr1 = TaskRunner.create("A") + tr2 = TaskRunner.create("B") + tr3 = TaskRunner.create("C") + await asyncio.sleep(1.5) + tr1.some_long_task("A", incrementer) + tr2.some_long_task("B", incrementer) + tr3.some_long_task("C", incrementer) + await asyncio.sleep(1.5) + + await exec_test() + assert calls["A"] == 2 + assert calls["B"] == 2 + assert calls["C"] == 2