From 05e57f630b18546bff824749fc83713412dbf0ea Mon Sep 17 00:00:00 2001 From: sinus Date: Sat, 21 Dec 2024 22:51:19 +0100 Subject: [PATCH] netinfo ico + Solana Address checker tool --- application.py | 2341 ++++++++++++++++++----------------- icons.qrc | 1 + rc_icons.py | 302 +++-- res/icons/check_address.png | Bin 0 -> 2203 bytes 4 files changed, 1461 insertions(+), 1183 deletions(-) create mode 100644 res/icons/check_address.png diff --git a/application.py b/application.py index 7100543..5887612 100755 --- a/application.py +++ b/application.py @@ -2,7 +2,7 @@ """Khadhroony SRLPv4 main script""" import argparse -# import base58 +import base58 import base64 # import hashlib # import io @@ -95,6 +95,7 @@ from PySide6.QtWidgets import ( QSystemTrayIcon, QTableView, QTabWidget, + QTextEdit, QToolButton, QToolBar, QVBoxLayout, @@ -213,7 +214,10 @@ from solders.rpc.responses import ( # LogsNotificationResult, SubscriptionError, SubscriptionResult, - parse_websocket_message + parse_websocket_message, GetBalanceResp, GetAccountInfoResp, + GetTokenAccountsByOwnerResp, GetTokenSupplyResp, GetProgramAccountsResp, + GetSignaturesForAddressResp, GetTokenAccountBalanceResp, + GetTokenAccountsByDelegateResp ) from solders.signature import Signature from solders.transaction import VersionedTransaction, Transaction @@ -221,33 +225,24 @@ from solders.transaction_status import TransactionDetails, UiTransactionEncoding from threading import Lock from typing import Any, IO, List, NamedTuple, Optional, Sequence, Union -APP_NAME: str = "KhadhroonySRLPv4" -"""Application Name""" -APP_ABOUT_NAME: str = "Khadhroony Sol RLPv4 Trading App" -"""Application Full Name""" -APP_FULL_NAME: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading App" -"""Application Full Name""" -APP_DESC: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application" -"""Application Version""" -APP_VERSION_INFO = ('1', '0', '1') -APP_VERSION = '.'.join(APP_VERSION_INFO) -"""Application Name + Version""" +APP_NAME: str = "KhadhroonySRLPv4" # Application Name +APP_ABOUT_NAME: str = "Khadhroony Sol RLPv4 Trading App" # Application Full Name +APP_FULL_NAME: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading App" # Application Full Name +APP_DESC: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application" # Application Version +APP_VERSION_INFO = ('1', '0', '2') # Application Version (tuple) +APP_VERSION = '.'.join(APP_VERSION_INFO) # Application Version (str) APP_NAME_VERSION = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application @ " + APP_VERSION -"""Application Langs""" -APP_LANGS = ["en", "fr", "de", "ar"] -APP_RTL_LANGS = ["ae", "aeb", "aec", "ar", "arb", "arc", "arq", "ary", "arz", "ayl", "ckb", "dv", "fa", "glk", "he", "khw", "mzn", "ota", "otk", "pnb", "ps", "syr", "tmr", "ug", "ur"] -APP_SALT = APP_NAME + " @ " + APP_VERSION_INFO[0] + "." + APP_VERSION_INFO[1] -"""Application Salt for wallet encrypting""" -WALLET_FILE_EXT = "kew" -WALLET_FILE_DESC = "Khadhroony Encrypted Wallet" -WALLETS_FOLDER = "./wallets" -"""Wallets descriptors""" -is_rtl = False -"""global rtl var""" -URL_Ws_MainNet = "wss://api.mainnet-beta.solana.com" -URL_Http_MainNet = "https://api.mainnet-beta.solana.com" -URL_Ws_DevNet = "wss://api.devnet.solana.com" -URL_Http_DevNet = "https://api.devnet.solana.com" +APP_LANGS = ["en", "fr", "de", "ar"] # supported languages +APP_RTL_LANGS = ["ae", "aeb", "aec", "ar", "arb", "arc", "arq", "ary", "arz", "ayl", "ckb", "dv", "fa", "glk", "he", "khw", "mzn", "ota", "otk", "pnb", "ps", "syr", "tmr", "ug", "ur"] # rtl langs +is_rtl = False # global var used to indicate if the current language select is RTL or LTR +APP_SALT = APP_NAME + " @ " + APP_VERSION_INFO[0] + "." + APP_VERSION_INFO[1] # Application Salt for wallet encrypting""" +WALLET_FILE_EXT = "kew" # Wallet files extention +WALLET_FILE_DESC = "Khadhroony Encrypted Wallet" # Wallet files description +WALLETS_FOLDER = "./wallets" # Wallet files default folder +URL_Ws_MainNet = "wss://api.mainnet-beta.solana.com" # Websocket Solana Mainnet URL +URL_Http_MainNet = "https://api.mainnet-beta.solana.com" # Http Solana Mainnet URL +URL_Ws_DevNet = "wss://api.devnet.solana.com" # Websocket Solana Devnet URL +URL_Http_DevNet = "https://api.devnet.solana.com" # Http Solana Devnet URL """ Solana URLs Maximum number of requests per 10 seconds per IP: 100 Maximum number of requests per 10 seconds per IP for a single RPC: 40 @@ -255,84 +250,49 @@ Maximum concurrent connections per IP: 40 Maximum connection rate per 10 seconds per IP: 40 Maximum amount of data per 30 second: 100 MB """ -LAMPORTS_PER_SOL: int = 1_000_000_000 -"""Number of lamports per SOL, where 1 SOL equals 1 billion lamports.""" -MINT_LEN: int = 82 -"""Data length of a token mint account.""" -ACCOUNT_LEN: int = 165 -"""Data length of a token account.""" -MULTISIG_LEN: int = 355 -"""Data length of a multisig token account.""" -SYSTEM_PROGRAM_ID: str = "11111111111111111111111111111111" -"""Program ID for the System Program (str).""" -PK_SYSTEM_PROGRAM_ID = Pubkey.from_string(SYSTEM_PROGRAM_ID) -"""Program ID for the System Program (Pubkey).""" -CONFIG_PROGRAM_ID: str = "Config1111111111111111111111111111111111111" -"""Program ID for the Config Program (str).""" -PK_CONFIG_PROGRAM_ID: Pubkey = Pubkey.from_string(CONFIG_PROGRAM_ID) -"""Program ID for the Config Program (Pubkey).""" -STAKE_PROGRAM_ID: str = "Stake11111111111111111111111111111111111111" -"""Program ID for the Stake Program (str).""" -PK_STAKE_PROGRAM_ID: Pubkey = Pubkey.from_string(STAKE_PROGRAM_ID) -"""Program ID for the Stake Program (Pubkey).""" -VOTE_PROGRAM_ID: str = "Vote111111111111111111111111111111111111111" -"""Program ID for the Vote Program (str).""" -PK_VOTE_PROGRAM_ID: Pubkey = Pubkey.from_string(VOTE_PROGRAM_ID) -"""Program ID for the Vote Program (Pubkey).""" -ADDRESS_LOOKUP_TABLE_PROGRAM_ID: str = "AddressLookupTab1e1111111111111111111111111" -"""Program ID for the Address Lookup Table Program (str).""" -PK_ADDRESS_LOOKUP_TABLE_PROGRAM_ID: Pubkey = Pubkey.from_string(ADDRESS_LOOKUP_TABLE_PROGRAM_ID) -"""Program ID for the Address Lookup Table Program (Pubkey).""" -BPF_LOADER_PROGRAM_ID: str = "BPFLoaderUpgradeab1e11111111111111111111111" -"""Program ID for the BPF Loader Program (str).""" -PK_BPF_LOADER_PROGRAM_ID: Pubkey = Pubkey.from_string(BPF_LOADER_PROGRAM_ID) -"""Program ID for the BPF Loader Program (Pubkey).""" -ED25519_PROGRAM_ID: str = "Ed25519SigVerify111111111111111111111111111" -"""Program ID for the Ed25519 Program (str).""" -PK_ED25519_PROGRAM_ID: Pubkey = Pubkey.from_string(ED25519_PROGRAM_ID) -"""Program ID for the Ed25519 Program (Pubkey).""" -SECP256K1_PROGRAM_ID: str = "KeccakSecp256k11111111111111111111111111111" -"""Program ID for the Secp256k1 Program (str).""" -PK_SECP256K1_PROGRAM_ID: Pubkey = Pubkey.from_string(SECP256K1_PROGRAM_ID) -"""Program ID for the Secp256k1 Program (Pubkey).""" -ASSOCIATED_TOKEN_PROGRAM_ID: str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" -"""Program ID for the associated token account program (str).""" -PK_ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = Pubkey.from_string(ASSOCIATED_TOKEN_PROGRAM_ID) -"""Program ID for the associated token account program (Pubkey).""" -TOKEN_PROGRAM_ID: str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" -"""Public key that identifies the SPL token program (str).""" -PK_TOKEN_PROGRAM_ID: Pubkey = Pubkey.from_string(TOKEN_PROGRAM_ID) -"""Public key that identifies the SPL token program (Pubkey).""" -TOKEN_2022_PROGRAM_ID: str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" -"""Public key that identifies the SPL token 2022 program (str).""" -PK_TOKEN_2022_PROGRAM_ID: Pubkey = Pubkey.from_string(TOKEN_2022_PROGRAM_ID) -"""Public key that identifies the SPL token 2022 program (Pubkey).""" -WRAPPED_SOL_MINT: str = "So11111111111111111111111111111111111111112" -"""Public key of the "Native Mint" for wrapping SOL to SPL token (str). -The Token Program can be used to wrap native SOL. Doing so allows native SOL to be treated like any +LAMPORTS_PER_SOL: int = 1_000_000_000 # Number of lamports per SOL, where 1 SOL equals 1 billion lamports. +MINT_LEN: int = 82 # Data length of a token mint account. +ACCOUNT_LEN: int = 165 # Data length of a token account. +MULTISIG_LEN: int = 355 # Data length of a multisig token account. +SYSTEM_PROGRAM_ID: str = "11111111111111111111111111111111" # Program ID for the System Program (str). +PK_SYSTEM_PROGRAM_ID = Pubkey.from_string(SYSTEM_PROGRAM_ID) # Program ID for the System Program (Pubkey). +CONFIG_PROGRAM_ID: str = "Config1111111111111111111111111111111111111" # Program ID for the Config Program (str). +PK_CONFIG_PROGRAM_ID: Pubkey = Pubkey.from_string(CONFIG_PROGRAM_ID) # Program ID for the Config Program (Pubkey). +STAKE_PROGRAM_ID: str = "Stake11111111111111111111111111111111111111" # Program ID for the Stake Program (str). +PK_STAKE_PROGRAM_ID: Pubkey = Pubkey.from_string(STAKE_PROGRAM_ID) # Program ID for the Stake Program (Pubkey). +VOTE_PROGRAM_ID: str = "Vote111111111111111111111111111111111111111" # Program ID for the Vote Program (str). +PK_VOTE_PROGRAM_ID: Pubkey = Pubkey.from_string(VOTE_PROGRAM_ID) # Program ID for the Vote Program (Pubkey). +ADDRESS_LOOKUP_TABLE_PROGRAM_ID: str = "AddressLookupTab1e1111111111111111111111111" # Program ID for the Address Lookup Table Program (str). +PK_ADDRESS_LOOKUP_TABLE_PROGRAM_ID: Pubkey = Pubkey.from_string(ADDRESS_LOOKUP_TABLE_PROGRAM_ID) # Program ID for the Address Lookup Table Program (Pubkey). +BPF_LOADER_PROGRAM_ID: str = "BPFLoaderUpgradeab1e11111111111111111111111" # Program ID for the BPF Loader Program (str). +PK_BPF_LOADER_PROGRAM_ID: Pubkey = Pubkey.from_string(BPF_LOADER_PROGRAM_ID) # Program ID for the BPF Loader Program (Pubkey). +ED25519_PROGRAM_ID: str = "Ed25519SigVerify111111111111111111111111111" # Program ID for the Ed25519 Program (str). +PK_ED25519_PROGRAM_ID: Pubkey = Pubkey.from_string(ED25519_PROGRAM_ID) # Program ID for the Ed25519 Program (Pubkey). +SECP256K1_PROGRAM_ID: str = "KeccakSecp256k11111111111111111111111111111" # Program ID for the Secp256k1 Program (str). +PK_SECP256K1_PROGRAM_ID: Pubkey = Pubkey.from_string(SECP256K1_PROGRAM_ID) # Program ID for the Secp256k1 Program (Pubkey). +ASSOCIATED_TOKEN_PROGRAM_ID: str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" # Program ID for the associated token account program (str). +PK_ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = Pubkey.from_string(ASSOCIATED_TOKEN_PROGRAM_ID) # Program ID for the associated token account program (Pubkey). +TOKEN_PROGRAM_ID: str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" # Public key that identifies the SPL token program (str). +PK_TOKEN_PROGRAM_ID: Pubkey = Pubkey.from_string(TOKEN_PROGRAM_ID) # Public key that identifies the SPL token program (Pubkey). +TOKEN_2022_PROGRAM_ID: str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" # Public key that identifies the SPL token 2022 program (str). +PK_TOKEN_2022_PROGRAM_ID: Pubkey = Pubkey.from_string(TOKEN_2022_PROGRAM_ID) # Public key that identifies the SPL token 2022 program (Pubkey). +WRAPPED_SOL_MINT: str = "So11111111111111111111111111111111111111112" # Public key of the "Native Mint" for wrapping SOL to SPL token (str). +""" The Token Program can be used to wrap native SOL. Doing so allows native SOL to be treated like any other Token program token type and can be useful when being called from other programs that interact -with the Token Program's interface.""" -PK_WRAPPED_SOL_MINT: Pubkey = Pubkey.from_string(WRAPPED_SOL_MINT) -"""Public key of the "Native Mint" for wrapping SOL to SPL token (Pubkey). -The Token Program can be used to wrap native SOL. Doing so allows native SOL to be treated like any +with the Token Program's interface. """ +PK_WRAPPED_SOL_MINT: Pubkey = Pubkey.from_string(WRAPPED_SOL_MINT) # Public key of the "Native Mint" for wrapping SOL to SPL token (Pubkey). +""" The Token Program can be used to wrap native SOL. Doing so allows native SOL to be treated like any other Token program token type and can be useful when being called from other programs that interact -with the Token Program's interface.""" -MEMO_PROGRAM_ID: str = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" -"""Public key that identifies the Memo program (str).""" -PK_MEMO_PROGRAM_ID: Pubkey = Pubkey.from_string(MEMO_PROGRAM_ID) -"""Public key that identifies the Memo program (Pubkey).""" -RaydiumLPV4: str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" -"""Public key that identifies the Raydium Liquidity Pool V4 (str).""" -PK_RaydiumLPV4: Pubkey = Pubkey.from_string(RaydiumLPV4) -"""Public key that identifies the Raydium Liquidity Pool V4 (Pubkey).""" -RPC_WS_REQ = ["accountSubscribe", "accountUnsubscribe", "blockSubscribe", "blockUnsubscribe", "logsSubscribe", "logsUnsubscribe", "programSubscribe", "programUnsubscribe", "rootSubscribe", "rootUnsubscribe", "signatureSubscribe", "signatureUnsubscribe", "slotSubscribe", "slotUnsubscribe", "slotsUpdatesSubscribe", "slotsUpdatesUnsubscribe", "voteSubscribe", "voteUnsubscribe"] -"""RPC WebSocket Requests""" -RPC_HTTP_REQ = ["getAccountInfo", "getBalance", "getBlock", "getBlockCommitment", "getBlockHeight", "getBlockProduction", "getBlockTime", "getBlocks", "getBlocksWithLimit", "getClusterNodes", "getEpochInfo", "getEpochSchedule", "getFeeForMessage", "getFirstAvailableBlock", "getGenesisHash", "getHealth", "getHighestSnapshotSlot", "getIdentity", "getInflationGovernor", "getInflationRate", "getInflationReward", "getLargestAccounts", "getLatestBlockhash", "getLeaderSchedule", "getMaxRetransmitSlot", "getMaxShredInsertSlot", "getMinimumBalanceForRentExemption", "getMultipleAccounts", "getProgramAccounts", "getRecentPerformanceSamples", "getRecentPrioritizationFees", "getSignatureStatuses", "getSignaturesForAddress", "getSlot", "getSlotLeader", "getSlotLeaders", "getStakeMinimumDelegation", "getSupply", "getTokenAccountBalance", "getTokenAccountsByDelegate", "getTokenAccountsByOwner", "getTokenLargestAccounts", "getTokenSupply", "getTransaction", "getTransactionCount", "getVersion", "getVoteAccounts", "isBlockhashValid", "minimumLedgerSlot", "requestAirdrop", "sendTransaction", "simulateTransaction"] -"""RPC Http Requests""" -YAML_CONFIG_FILE = "KhadhroonySRLPv4.yaml" -"""Yaml config file""" +with the Token Program's interface. """ +MEMO_PROGRAM_ID: str = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" # Public key that identifies the Memo program (str). +PK_MEMO_PROGRAM_ID: Pubkey = Pubkey.from_string(MEMO_PROGRAM_ID) # Public key that identifies the Memo program (Pubkey). +RaydiumLPV4: str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" # Public key that identifies the Raydium Liquidity Pool V4 (str).""" +PK_RaydiumLPV4: Pubkey = Pubkey.from_string(RaydiumLPV4) # Public key that identifies the Raydium Liquidity Pool V4 (Pubkey). +RPC_WS_REQ = ["accountSubscribe", "accountUnsubscribe", "blockSubscribe", "blockUnsubscribe", "logsSubscribe", "logsUnsubscribe", "programSubscribe", "programUnsubscribe", "rootSubscribe", "rootUnsubscribe", "signatureSubscribe", "signatureUnsubscribe", "slotSubscribe", "slotUnsubscribe", "slotsUpdatesSubscribe", "slotsUpdatesUnsubscribe", "voteSubscribe", "voteUnsubscribe"] # RPC WebSocket Requests +RPC_HTTP_REQ = ["getAccountInfo", "getBalance", "getBlock", "getBlockCommitment", "getBlockHeight", "getBlockProduction", "getBlockTime", "getBlocks", "getBlocksWithLimit", "getClusterNodes", "getEpochInfo", "getEpochSchedule", "getFeeForMessage", "getFirstAvailableBlock", "getGenesisHash", "getHealth", "getHighestSnapshotSlot", "getIdentity", "getInflationGovernor", "getInflationRate", "getInflationReward", "getLargestAccounts", "getLatestBlockhash", "getLeaderSchedule", "getMaxRetransmitSlot", "getMaxShredInsertSlot", "getMinimumBalanceForRentExemption", "getMultipleAccounts", "getProgramAccounts", "getRecentPerformanceSamples", "getRecentPrioritizationFees", "getSignatureStatuses", "getSignaturesForAddress", "getSlot", "getSlotLeader", "getSlotLeaders", "getStakeMinimumDelegation", "getSupply", "getTokenAccountBalance", "getTokenAccountsByDelegate", "getTokenAccountsByOwner", "getTokenLargestAccounts", "getTokenSupply", "getTransaction", "getTransactionCount", "getVersion", "getVoteAccounts", "isBlockhashValid", "minimumLedgerSlot", "requestAirdrop", "sendTransaction", "simulateTransaction"] # RPC Http Requests +YAML_CONFIG_FILE = "KhadhroonySRLPv4.yaml" # Yaml config file DEFAULT_YAML_CONFIG = { - """default Yaml config""" + """ default Yaml config """ "lastFile": "", "lastFiles": [], "defaultLang": "en", @@ -343,7 +303,7 @@ DEFAULT_YAML_CONFIG = { def load_yaml_app_config(): - """Load the configuration file or create a default one.""" + """ Load the configuration file or create a default one. """ if not os.path.exists(YAML_CONFIG_FILE): logger.debug(f"{YAML_CONFIG_FILE} not found, creating new one") with open(YAML_CONFIG_FILE, "w") as f: @@ -352,7 +312,7 @@ def load_yaml_app_config(): with open(YAML_CONFIG_FILE, "r") as f: data = yaml.safe_load(f) logger.debug(f"{YAML_CONFIG_FILE} safe loaded") - # Validation et complétion des données + # Data validation and completion for key, val in DEFAULT_YAML_CONFIG.items(): if key not in data: logger.debug(f"{key} not found") @@ -364,23 +324,24 @@ def load_yaml_app_config(): def save_yaml_app_config(cfg): - """Save the configuration to the file.""" + """ Save the configuration to the file. """ with open(YAML_CONFIG_FILE, "w") as f: yaml.dump(cfg, f) def create_wallets_folder(): - """Vérifie si le répertoire wallets existe et le créé dans le cas contraire.""" + """ Checks if the wallets directory exists and creates it if not. """ if not os.path.exists(WALLETS_FOLDER): os.makedirs(WALLETS_FOLDER) def derive_key(password: str, salt: bytes) -> bytes: - """ - Derive a symmetric key from the given password and salt. - :param password: The password to derive the key from. - :param salt: A random salt for the key derivation. - :return: The derived key. + """ Derive a symmetric key from the given password and salt. + Args: + password (str): The password to derive the key from. + salt (bytes): A random salt for the key derivation. + Returns: + bytes: The derived key. """ kdf = PBKDF2HMAC( algorithm = hashes.SHA256(), @@ -392,52 +353,56 @@ def derive_key(password: str, salt: bytes) -> bytes: return base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8"))) -def encrypt_string(password: str, salt: bytes, plaintext: str) -> str: - """ - Encrypts a string using a password. - :param password: The password to derive the encryption key. - :param salt: A salt for the key derivation. - :param plaintext: The string to encrypt. - :return: A tuple containing the salt and the encrypted string. +def encrypt_string(password: str, salt: bytes, clear_txt: str) -> str: + """ Encrypts a string using a password. + Args: + password (str): The password to derive the encryption key. + salt (bytes): A salt for the key derivation. + clear_txt (str): The clear string to encrypt. + Returns: + tuple: containing the salt and the encrypted string. """ key = derive_key(password, salt) # Derive the encryption key fernet = Fernet(key) - encrypted_text = fernet.encrypt(plaintext.encode("utf-8")) + encrypted_text = fernet.encrypt(clear_txt.encode("utf-8")) return encrypted_text.decode() -def center_on_screen(widget: QWidget): - """Centre la fenêtre sur le moniteur actif (celui où le curseur est présent).""" - screen = QApplication.screenAt(QCursor().pos()) # Obtenir le moniteur actif - if not screen: - screen = QApplication.primaryScreen() # Par défaut, utiliser l'écran principal - - screen_geometry = screen.availableGeometry() # Dimensions de l'écran - widget_geometry = widget.frameGeometry() - # Calcul des coordonnées pour centrer la fenêtre - x = screen_geometry.x() + (screen_geometry.width() - widget_geometry.width()) // 2 - y = screen_geometry.y() + (screen_geometry.height() - widget_geometry.height()) // 2 - widget.move(x, y) - - -def decrypt_string(password: str, salt: bytes, encrypted_text: Union[bytes, str]) -> str: +def decrypt_string(password: str, salt: bytes, encrypted_txt: Union[bytes, str]) -> str: + """ Decrypts an encrypted string using the given password and salt. + Args: + password (str): The password to derive the decryption key. + salt (bytes): The salt used during encryption. + encrypted_txt (Union[bytes, str]): The encrypted string to decrypt. + Returns: + str: The decrypted string. """ - Decrypts an encrypted string using the given password and salt. - :param password: The password to derive the decryption key. - :param salt: The salt used during encryption. - :param encrypted_text: The encrypted string to decrypt. - :return: The decrypted string. - """ - enc_text = encrypted_text - if isinstance(encrypted_text, str): - enc_text = encrypted_text.encode("utf-8") + enc_text = encrypted_txt + if isinstance(encrypted_txt, str): + enc_text = encrypted_txt.encode("utf-8") key = derive_key(password, salt) # Derive the decryption key fernet = Fernet(key) return fernet.decrypt(enc_text).decode() -def set_app_dir(lang_code): - """Set the application language.""" +def center_on_screen(widget: QWidget): + """ Centers the window on the active monitor (the one where the cursor is present). """ + screen = QApplication.screenAt(QCursor().pos()) # Get Active Monitor + if not screen: + screen = QApplication.primaryScreen() # By default, use the main screen + screen_geometry = screen.availableGeometry() # Screen dimensions + widget_geometry = widget.frameGeometry() + # Calculating coordinates to center the window + x = screen_geometry.x() + (screen_geometry.width() - widget_geometry.width()) // 2 + y = screen_geometry.y() + (screen_geometry.height() - widget_geometry.height()) // 2 + widget.move(x, y) + + +def set_app_dir(lang_code: str): + """ Set the application layout direction depending of the language code. + Args: + lang_code (str): The language code. + """ global is_rtl if lang_code in APP_RTL_LANGS: is_rtl = True @@ -446,24 +411,21 @@ def set_app_dir(lang_code): def get_qt_dir() -> Qt.LayoutDirection: - """ - return the Qt layout direction. + """ get the Qt layout direction depending on is_rtl. + Returns: + Qt.LayoutDirection: Enum LTR/RTL. """ global is_rtl return Qt.LayoutDirection.RightToLeft if is_rtl else Qt.LayoutDirection.LeftToRight def get_qt_align_center() -> Qt.AlignmentFlag: - """ - return the Qt alignment center. - """ + """ return the Qt alignment center. """ return Qt.AlignmentFlag.AlignCenter def get_qt_align_vchl(rtl: bool = False) -> Qt.AlignmentFlag: - """ - return the Qt alignment vertical center and to left if ltr or to right if rtl. - """ + """ return the Qt alignment vertical center and to left if ltr or to right if rtl. """ if rtl: return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight else: @@ -471,9 +433,7 @@ def get_qt_align_vchl(rtl: bool = False) -> Qt.AlignmentFlag: def get_qt_align_vchr(rtl: bool = False) -> Qt.AlignmentFlag: - """ - return the Qt alignment vertical center and to right if ltr or to left if rtl. - """ + """ return the Qt alignment vertical center and to right if ltr or to left if rtl. """ if rtl: return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft else: @@ -481,16 +441,12 @@ def get_qt_align_vchr(rtl: bool = False) -> Qt.AlignmentFlag: def get_qt_align_hcenter() -> Qt.AlignmentFlag: - """ - return the Qt alignment to horizontal center. - """ + """ return the Qt alignment to horizontal center. """ return Qt.AlignmentFlag.AlignHCenter def get_qt_align_hl(rtl: bool = False) -> Qt.AlignmentFlag: - """ - return the Qt alignment to horizontal left if ltr or to horizontal right if rtl. - """ + """ return the Qt alignment to horizontal left if ltr or to horizontal right if rtl. """ if rtl: return Qt.AlignmentFlag.AlignRight else: @@ -498,9 +454,7 @@ def get_qt_align_hl(rtl: bool = False) -> Qt.AlignmentFlag: def get_qt_align_hr(rtl: bool = False) -> Qt.AlignmentFlag: - """ - return the Qt alignment to horizontal right if ltr or to horizontal left if rtl. - """ + """ return the Qt alignment to horizontal right if ltr or to horizontal left if rtl. """ if rtl: return Qt.AlignmentFlag.AlignLeft else: @@ -508,35 +462,30 @@ def get_qt_align_hr(rtl: bool = False) -> Qt.AlignmentFlag: class LockedFile: - """ - Une classe pour gérer les fichiers avec verrouillage exclusif. - Utilise portalocker pour éviter les accès simultanés. - """ - - def __init__(self, file_path, mode = "r"): - """ - Initialise un Verrou. - - :param file_path: Chemin vers le fichier à verrouiller. - :param mode: Mode d'ouverture du fichier ('r', 'w', 'a', etc.). + """ A class for handling files with exclusive locking. + Use portalocker to avoid concurrent access. """ + def __init__(self, file_path: str, mode: str = "r"): + """ Initializes a Lock. + Args: + file_path (str): Chemin vers le fichier à verrouiller. + mode (str): Mode d'ouverture du fichier ('r', 'w', 'a', etc.). """ self._file = None self._filePath = file_path self._mode = mode def __enter__(self) -> IO[Any]: - """ - Ouvre et verrouille le fichier. + """ Opens and locks the file. + Returns: + IO[Any]: file stream """ self._file = open(file = self._filePath, mode = self._mode) - # Application d'un verrou exclusif + # Application of an exclusive lock portalocker.lock(self._file, portalocker.LOCK_EX) return self._file def __exit__(self, exc_type, exc_val, exc_tb): - """ - Libère le verrou et ferme le fichier. - """ + """ Releases the lock and closes the file. """ try: portalocker.unlock(self._file) finally: @@ -546,139 +495,190 @@ class LockedFile: class Map: def __init__(self): - """Initialise un dictionnaire interne pour stocker les éléments.""" + """ Initializes an internal dictionary to store items. """ self._data = {} def put(self, key, value): - """Ajoute ou met à jour une entrée dans le map.""" + """ Adds or updates an entry in the map. + Args: + key: the unique key of the map + value: the value associated to the key + """ previous_value = self._data.get(key) self._data[key] = value return previous_value - def get(self, key): - """Récupère la valeur associée à une clé, ou None si la clé n'existe pas.""" + def get(self, key) -> Any | None: + """ Retrieves the value associated with a key, or None if the key does not exist. + Args: + key: the key to check + Returns: + Any | None: the value associated with a key or None + """ return self._data.get(key) - def remove(self, key): - """Supprime une clé et retourne sa valeur, ou None si la clé n'existe pas.""" + def remove(self, key) -> Any | None: + """ Deletes a key and returns its value, or None if the key does not exist. + Args: + key: the key to remove + Returns: + Any | None: the value associated with a key or None + """ return self._data.pop(key, None) - def contains_key(self, key): - """Vérifie si une clé est présente dans le map.""" + def contains_key(self, key) -> bool: + """ Checks if a key is present in the map. + Args: + key: the key to check + Returns: + bool: True if exists + """ return key in self._data - def contains_value(self, value): - """Vérifie si une valeur est présente dans le map.""" + def contains_value(self, value) -> bool: + """ Checks if a value is present in the map. + Args: + value: the value to check + Returns: + bool: True if exists + """ return value in self._data.values() - def size(self): - """Retourne le nombre d'éléments dans le map.""" + def size(self) -> int: + """ Returns the number of elements in the map. + Returns: + int: number of elements in the map + """ return len(self._data) - def is_empty(self): - """Retourne True si le map est vide, False sinon.""" + def is_empty(self) -> bool: + """ Check if the map is empty. + Returns: + bool: True if the map is empty, False otherwise. + """ return len(self._data) == 0 def keys(self): - """Retourne un itérateur sur les clés du map.""" + """ Returns an iterator over the keys of the map. """ return iter(self._data.keys()) def values(self): - """Retourne un itérateur sur les valeurs du map.""" + """ Returns an iterator over the values ​​in the map. """ return iter(self._data.values()) def items(self): - """Retourne un itérateur sur les paires clé-valeur du map.""" + """ Returns an iterator over the key-value pairs in the map. """ return iter(self._data.items()) def clear(self): - """Supprime tous les éléments du map.""" + """ Removes all elements from the map. """ self._data.clear() def __repr__(self): - """Représentation lisible du map.""" + """ Readable representation of the map. """ return f"Map({self._data})" - class DataSliceOpts(NamedTuple): - """Option to limit the returned account data, only available for "base58" or "base64" encoding.""" + """ Option to limit the returned account data, only available for "base58" or "base64" encoding. + Attributes: + offset (int): Limit the returned account data using the provided offset: . + length (int): Limit the returned account data using the provided length: . + """ offset: int - """Limit the returned account data using the provided offset: .""" length: int - """Limit the returned account data using the provided length: .""" class MemcmpOpts(NamedTuple): - """Option to compare a provided series of bytes with program account data at a particular offset.""" + """ Option to compare a provided series of bytes with program account data at a particular offset. + Attributes: + offset (int): Offset into program account data to start comparison: . + bytes (str): Data to match, as base-58 encoded string: . + """ offset: int - """Offset into program account data to start comparison: .""" bytes: str - """Data to match, as base-58 encoded string: .""" class TokenAccountOpts(NamedTuple): - """Options when querying token accounts. - Provide one of mint or programId. + """ Options when querying token accounts. Provide one of mint or programId. + Attributes: + mint (Optional[Pubkey]): Public key of the specific token Mint to limit accounts to. + programId (Optional[Pubkey]): Public key of the Token program ID that owns the accounts. + encoding (UiAccountEncoding): Encoding for Account data, either "base58" (slow) or "base64". + dataSlice (Optional[DataSliceOpts]): Option to limit the returned account data, only available for "base58" or "base64" encoding. """ mint: Optional[Pubkey] = None - """Public key of the specific token Mint to limit accounts to.""" programId: Optional[Pubkey] = None - """Public key of the Token program ID that owns the accounts.""" encoding: UiAccountEncoding = UiAccountEncoding.Base64 - """Encoding for Account data, either "base58" (slow) or "base64".""" dataSlice: Optional[DataSliceOpts] = None - """Option to limit the returned account data, only available for "base58" or "base64" encoding.""" class TxOpts(NamedTuple): - """Options to specify when broadcasting a transaction.""" + """ Options to specify when broadcasting a transaction. + Attributes: + skipConfirmation (bool): If false, `sendTransaction` will try to confirm that the transaction was successfully broadcasted. When confirming a transaction, `sendTransaction` will block for a maximum of 30 seconds. + skipPreflight (bool]): If true, skip the preflight transaction checks. + prefLightCommitment (CommitmentLevel): Commitment level to use for preflight. Default to CommitmentLevel.Finalized. + prefEncoding (UiTransactionEncoding): Encoding to use. Default to UiTransactionEncoding.Base64. + maxRetries (Optional[int]): Maximum number of times for the RPC node to retry sending the transaction to the leader. If this parameter not provided, the RPC node will retry the transaction until it is finalized or until the blockhash expires. + lastValidBlockHeight (Optional[int]): Pass the latest valid block height here, to be consumed by confirm_transaction. Valid only if skip_confirmation is False. + """ skipConfirmation: bool = True - """If false, `sendTransaction` will try to confirm that the transaction was successfully broadcasted. - When confirming a transaction, `sendTransaction` will block for a maximum of 30 seconds. - """ skipPreflight: bool = False - """If true, skip the preflight transaction checks.""" prefLightCommitment: CommitmentLevel = CommitmentLevel.Finalized - """Commitment level to use for preflight.""" prefEncoding: UiTransactionEncoding = UiTransactionEncoding.Base64 - """Encoding to use.""" maxRetries: Optional[int] = None - """Maximum number of times for the RPC node to retry sending the transaction to the leader. - If this parameter not provided, the RPC node will retry the transaction until it is finalized - or until the blockhash expires. - """ lastValidBlockHeight: Optional[int] = None - """Pass the latest valid block height here, to be consumed by confirm_transaction. - Valid only if skip_confirmation is False. - """ class SharedCounter: - """Classe pour un compteur auto-incrémentable, thread safe et partagé.""" - _counter = itercount() # Compteur partagé - _lock = Lock() # Verrou pour assurer la sécurité des threads + """ Class for a self-incrementing, thread safe, shared counter. """ + _counter = itercount() # Shared counter + _lock = Lock() # Lock to ensure thread safety @classmethod def reset_id(cls): - """ - Remet les compteurs à sa valeur initiale - Thread-safe grâce à l'utilisation d'un verrou. - """ + """ Resets the counters to their initial value. Thread-safe through the use of a lock. """ with cls._lock: cls._counter = itercount() @classmethod def get_next_id(cls): - """ - Retourne le prochain identifiant unique. - Thread-safe grâce à l'utilisation d'un verrou. - """ + """ Returns the next unique identifier. Thread-safe due to the use of a lock. """ with cls._lock: return next(cls._counter) + 1 +class WsError: + """ Websocket error """ + def __init__(self, code: QAbstractSocket.SocketError, body: str): + self.code = code + self.body = body + + +class NetworkReplyError: + """ HTTP Network Error Reply """ + def __init__(self, code: QNetworkReply.NetworkError, body: str): + self.code = code + self.body = body + + +class ResponseError: + """ Http Response with code error between 400 and 600 """ + def __init__(self, code: int, headers: dict, body: str, resp_size: int): + self.code = code + self.headers = headers + self.body = body + self.size = resp_size + + +class ResponseOk: + """ Http Response Ok with size """ + def __init__(self, body: str, resp_size: int): + self.body = body + self.size = resp_size + class SimpleSignal(QObject): - """ Objet émettant un Signal """ + """ Object emitting a Signal """ _sig = Signal() def signal(self): @@ -693,34 +693,9 @@ class SimpleSignal(QObject): """ disconnect Signal """ self._sig.disconnect() -class WsError: - def __init__(self, code: QAbstractSocket.SocketError, msg: str): - self.code = code - self.msg = msg - - -class ResponseError: - def __init__(self, code: int, headers: dict, body: str, resp_size: int): - self.code = code - self.headers = headers - self.body = body - self.size = resp_size - - -class ResponseOk: - def __init__(self, body: str, resp_size: int): - self.body = body - self.size = resp_size - - -class NetworkReplyError: - def __init__(self, code: QNetworkReply.NetworkError, msg: str): - self.code = code - self.msg = msg - class StrSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(str) def signal(self, data_str: str): @@ -737,7 +712,7 @@ class StrSignal(QObject): class SocketStateSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(QAbstractSocket.SocketState) def signal(self, data_qasss: QAbstractSocket.SocketState): @@ -754,7 +729,7 @@ class SocketStateSignal(QObject): class SocketErrorSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(WsError) def signal(self, ws_err: WsError): @@ -771,7 +746,7 @@ class SocketErrorSignal(QObject): class NetworkReplyErrorSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(NetworkReplyError) def signal(self, reply: NetworkReplyError): @@ -787,10 +762,8 @@ class NetworkReplyErrorSignal(QObject): self._sig.disconnect() - - class ResponseErrorSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(ResponseError) def signal(self, resp: ResponseError): @@ -807,7 +780,7 @@ class ResponseErrorSignal(QObject): class ResponseSuccessSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(ResponseOk) def signal(self, resp: ResponseOk): @@ -824,7 +797,7 @@ class ResponseSuccessSignal(QObject): class DictSignal(QObject): - """ Objet émettrant un Signal """ + """ Object emitting a Signal """ _sig = Signal(dict) def signal(self, data: dict): @@ -842,7 +815,6 @@ class DictSignal(QObject): class WebSocketClient(QThread): """Runnable WebSocket Client (wrap for a QWebSocket) """ - def __init__(self, base_url: str, max_retry: int = 3, reconnect_in: int = 2000, parent = None): super().__init__(parent) self.socketStateSig = SocketStateSignal() # Signal avec état de connexion @@ -861,48 +833,48 @@ class WebSocketClient(QThread): self._threadRunning = False def _connect(self): - """Ouvre la connexion et tente des reconnexions en cas d'échec.""" + """ Opens the connection and attempts reconnections if unsuccessful. """ if self._threadRunning: if self._attempts != 0: self.logSig.signal("New connexion attempt") self._websocket.open(QUrl(self._baseUrl)) def _on_connected(self): - """Gestion de la connexion réussie.""" - self._attempts = 0 # Réinitialiser les tentatives de reconnexion - self.socketStateSig.signal(self._websocket.state()) # Émettre un signal de connexion réussie + """ Connection successful handling. """ + self._attempts = 0 # Reset reconnection attempts + self.socketStateSig.signal(self._websocket.state()) # Emit websocket state def _on_disconnected(self): - """Gestion de la déconnexion.""" - self.socketStateSig.signal(self._websocket.state()) # Émettre un signal de déconnexion + """ Disconnection handling. """ + self.socketStateSig.signal(self._websocket.state()) # Emit websocket state if self._threadRunning and self._attempts < self._maxRetry: self._attempts += 1 - QTimer.singleShot(self._reconnectIn, self._connect) # Attendre 2 secondes avant de réessayer + QTimer.singleShot(self._reconnectIn, self._connect) # Wait "self._reconnectIn" seconds before trying again elif self._threadRunning: - self.logSig.signal("Maximum reconnection attempts reached.") # info + self.logSig.signal("Maximum reconnection attempts reached.") # Log signal self._threadRunning = False def _on_error(self, code: QAbstractSocket.SocketError): - """Gestion des erreurs.""" + """ Error handling. """ ws_err = WsError(code, self._websocket.errorString()) self.socketErrorSig.signal(ws_err) # error def _on_message(self, resp: str): - """Gestion des messages reçus.""" + """ Management of received messages. """ self.responseOkSig.signal(resp) def run(self): - """QThread Run signals""" + """ QThread Run """ self.exec() def get_socket_state(self) -> QAbstractSocket.SocketState: - """Obtenir l'état actuel du socket.""" + """ Get the current state of the websocket. """ return self._websocket.state() def open(self) -> bool: - """Ouvre la connexion WebSocket.""" + """ Opens the WebSocket connection. """ if self._threadRunning: - self.logSig.signal("WebSocket is already connected.") + self.logSig.signal("WebSocket is already connected.") # if this happend thant something is wrong in code logic return False self.socketStateSig.signal(QAbstractSocket.SocketState.ConnectingState) self._threadRunning = True @@ -912,6 +884,7 @@ class WebSocketClient(QThread): return True def send_msg(self, message: Union[bytes, str]) -> bool: + """ Send an msg to Websocket server """ if self._websocket.state() == QAbstractSocket.SocketState.ConnectedState: if isinstance(message, bytes): self._websocket.sendBinaryMessage(message) @@ -921,7 +894,7 @@ class WebSocketClient(QThread): return False def close(self): - """Ferme la connexion WebSocket.""" + """ Close connexion WebSocket connexion. """ self._threadRunning = False self._attempts = 0 if self._websocket.state() != QAbstractSocket.SocketState.UnconnectedState: @@ -931,16 +904,16 @@ class WebSocketClient(QThread): class HttpPostClient(QObject): - """Sync Client for HTTP POST Request""" - + """ Sync Client for HTTP POST Request """ def __init__(self, base_url: str, parent = None): super().__init__(parent) self._baseUrl = base_url self._defaultHeader = "application/json" self._networkManager = QNetworkAccessManager() - def _process_reply(self, reply: QNetworkReply) -> Union[NetworkReplyError, ResponseError, ResponseOk]: - """Sync Process Reply""" + @staticmethod + def _process_reply(reply: QNetworkReply) -> Union[NetworkReplyError, ResponseError, ResponseOk]: + """ Sync Process Reply """ if reply.error() != QNetworkReply.NetworkError.NoError: rep_err = NetworkReplyError(reply.error(), reply.errorString()) reply.deleteLater() @@ -961,7 +934,7 @@ class HttpPostClient(QObject): return ResponseOk(body, size) def send_post(self, body: Union[bytes, str]) -> Union[NetworkReplyError, ResponseError, ResponseOk]: - """ send Request""" + """ Send HTTP POST Request """ if isinstance(body, str): body = body.encode("utf-8") request = QNetworkRequest(self._baseUrl) @@ -973,8 +946,7 @@ class HttpPostClient(QObject): class AsyncHttpPostClient(QObject): - """Async Client for HTTP POST Request""" - + """ Async Client for HTTP POST Request """ def __init__(self, base_url: str, parent = None): super().__init__(parent) self.requestSentSig = SimpleSignal() @@ -986,7 +958,7 @@ class AsyncHttpPostClient(QObject): self._networkManager = QNetworkAccessManager() def _process_reply(self, reply: QNetworkReply): - """Async Process Reply""" + """ Async Process Reply """ if reply.error() != QNetworkReply.NetworkError.NoError: self.netRepErrorSig.signal(NetworkReplyError(reply.error(), reply.errorString())) else: @@ -1005,7 +977,7 @@ class AsyncHttpPostClient(QObject): reply.deleteLater() def send_post(self, body: Union[str, bytes]): - """ send Request""" + """ Send HTTP Post Request without waiting for Response. """ if isinstance(body, str): body = body.encode("utf-8") request = QNetworkRequest(QUrl(self._baseUrl)) @@ -1016,8 +988,7 @@ class AsyncHttpPostClient(QObject): class ConnectionPool(QObject): - """Pool Client with 1 fixed websocket, a minimum of 1 websocket and 1 httpclient in Pool and maximum 20 clients (ws+http) in Pool""" - + """ Client Pool with 1 fixed websocket, a minimum of 1 websocket and 1 httpclient in Pool and maxi 21 clients (ws+http) in Pool. """ def __init__(self, ws_url: str, http_url: str, num_ws_clients: int = 1, num_async_http_clients: int = 2, num_sync_http_clients: int = 2, parent = None): super().__init__(parent) self._num_ws_clients = max(num_ws_clients, 1) @@ -1026,7 +997,7 @@ class ConnectionPool(QObject): # Ensure the total number of clients does not exceed 21 total_clients = self._num_ws_clients + self._num_async_http_clients + self._num_sync_http_clients if total_clients > 21: - # Calculer le facteur de réduction + # Calculate the reduction factor reduction_factor = 21 / total_clients self._num_ws_clients = max(1, int(self._num_ws_clients * reduction_factor)) self._num_async_http_clients = max(1, int(self._num_async_http_clients * reduction_factor)) @@ -1039,27 +1010,27 @@ class ConnectionPool(QObject): self._num_ws_clients -= 1 else: self._num_async_http_clients -= 1 - # Liste des Clients + # Clients list self._wsMainClient = WebSocketClient(ws_url, 3, 2000) self._wsClients = [WebSocketClient(ws_url, 3, 2000) for _ in range(self._num_ws_clients)] - self._connectedWsClients = [] # Liste des WebSocket clients connectés + self._connectedWsClients = [] # List of connected WebSocket clients self._asyncHttpClients = [AsyncHttpPostClient(http_url) for _ in range(self._num_async_http_clients)] self._syncHttpClients = [HttpPostClient(http_url) for _ in range(self._num_sync_http_clients)] - # Liste des signaux - self.logSig = StrSignal() # nouveau log - self.wsMainConnexionStatusSig = SocketStateSignal() # changement de status de connexion pour _wsMainClient - self.wsConnexionStatusSigs = [] # changement de status de connexion pour pour _wsClients[i] + # Signals list + self.logSig = StrSignal() # new log Signal + self.wsMainConnexionStatusSig = SocketStateSignal() # connection status change for _wsMainClient + self.wsConnexionStatusSigs = [] # connection status change for _wsClients[i] for _ in range(self._num_ws_clients): self.wsConnexionStatusSigs.append(SocketStateSignal()) - self.httpPostSentSigs = [] # message envoyé via httpClient + self.httpPostSentSigs = [] # new msg sent via httpClient self.httpReplyReceivedSigs = [] # nouveau message via httpClient for _ in range(self._num_async_http_clients): self.httpPostSentSigs.append(SimpleSignal()) self.httpReplyReceivedSigs.append(SimpleSignal()) - self.wsMainReplyReceivedSig = SimpleSignal() # nouveau message reçu et stocké pour _wsMainClient - self.wsReplyReceivedSig = SimpleSignal() # nouveau ws message reçu et stocké pour _wsClients[i] - self.httpReplyReceivedSig = SimpleSignal() # nouveau ws message reçu et stocké pour _asyncHttpClients[i] - # connexion des signaux des clients + self.wsMainReplyReceivedSig = SimpleSignal() # new message received and stored for _wsMainClient + self.wsReplyReceivedSig = SimpleSignal() # new message received and stored for _wsClients[i] + self.httpReplyReceivedSig = SimpleSignal() # new message received and stored for _asyncHttpClients[i] + # connecting clients signals self._wsMainClient.socketStateSig.conn(self._handle_ws_main_connect_status) self._wsMainClient.socketErrorSig.conn(self._handle_ws_main_error) self._wsMainClient.logSig.conn(self._handle_ws_main_log) @@ -1074,26 +1045,23 @@ class ConnectionPool(QObject): client.netRepErrorSig.conn(self._handle_async_http_net_error(idx)) client.responseErrorSig.conn(self._handle_async_http_resp_err(idx)) client.responseOkSig.conn(self._handle_async_http_new_msg(idx)) - self._wsMainReceivedMessages = deque(maxlen = 1000) # FIFO pour stocker les messages reçus par _wsMainClient - self._wsReceivedMessages = deque(maxlen = 1000) # FIFO pour stocker les messages reçus par _wsClients[i] - self._httpReceivedMessages = deque(maxlen = 1000) # FIFO pour stocker les messages reçus par _asyncHttpClients[i] + self._wsMainReceivedMessages = deque(maxlen = 1000) # FIFO to store messages received by _wsMainClient + self._wsReceivedMessages = deque(maxlen = 1000) # FIFO to store messages received by _wsClients[i] + self._httpReceivedMessages = deque(maxlen = 1000) # FIFO to store messages received by _asyncHttpClients[i] # Initialize request message queues self._wsMainQueue = Queue() self._wsQueue = Queue() self._asyncHttpQueue = Queue() - # Limites des compteurs + # Counters Limits self._maxRequestsPer10s = 100 self._maxRpcRequestsPer10s = 40 self._maxDataPer30s = 100 * 1024 * 1024 # 100 MB - # Compteurs + # Counters self._currentRequestCount = 0 self._currentDataSize = 0 - # Compteurs spécifiques pour chaque méthode RPC - self._currentRpcCounters = {method: 0 for method in (RPC_WS_REQ + RPC_HTTP_REQ)} - # Verrous pour synchronisation - self._lock = Lock() - # bool: indicateur de boucle en cours ou arretée - self._connected = False + self._currentRpcCounters = {method: 0 for method in (RPC_WS_REQ + RPC_HTTP_REQ)} # Specific counters for each RPC method + self._lock = Lock() # Lock for synchronization + self._connected = False # loop indicator in progress or stopped # Timers self._timer_reset_counters = QTimer() self._timer_reset_counters.timeout.connect(self._reset_counters) @@ -1101,7 +1069,7 @@ class ConnectionPool(QObject): self._timer_process_queues.timeout.connect(self._process_queues) def _reset_counters(self): - """Réinitialise les compteurs périodiquement.""" + """ Resets the counters periodically. """ with self._lock: self._currentRequestCount = 0 self._currentDataSize = 0 @@ -1109,7 +1077,7 @@ class ConnectionPool(QObject): self._currentRpcCounters[key] = 0 def _process_queues(self): - """Tente de traiter les files d'attente.""" + """ Attempts to process queues """ while not self._wsMainQueue.empty(): method, body = self._wsMainQueue.get() self.send_websocket_main_request(method, body) @@ -1121,7 +1089,7 @@ class ConnectionPool(QObject): self.send_async_http_post(method, body) def _check_limits(self, rpc_method: str, size: int = 0) -> bool: - """Vérifie si une nouvelle requête peut être envoyée pour une méthode spécifique.""" + """ Checks if a new/stored request can be sent using counters limits. """ with self._lock: if self._currentRequestCount >= self._maxRequestsPer10s: return False @@ -1150,36 +1118,32 @@ class ConnectionPool(QObject): if self._wsClients[index] in self._connectedWsClients: self._connectedWsClients.remove(self._wsClients[index]) self.wsConnexionStatusSigs[index].signal(value) - return handler def _handle_ws_main_error(self, ws_error: WsError): """ Handle self._wsMainClient.socketErrorSig """ - logger.error(f"[main] {int(ws_error.code.value)} - {ws_error.msg}") - self.logSig.signal(ws_error.msg) + logger.error(f"[main] {int(ws_error.code.value)} - {ws_error.body}") + self.logSig.signal(ws_error.body) def _handle_ws_error(self, index: int): """ Handle _wsClients[index].socketStateSig """ - def handler(ws_error: WsError): """ handler for Signal """ - logger.error(f"[{index}] {int(ws_error.code.value)} - {ws_error.msg}") - self.logSig.signal(ws_error.msg) + logger.error(f"[{index}] {int(ws_error.code.value)} - {ws_error.body}") + self.logSig.signal(ws_error.body) return handler - def _handle_ws_main_log(self, msg: str): + def _handle_ws_main_log(self, body: str): """ Handle self._wsMainClient.logSig """ - logger.debug(f"[main] {msg}") - self.logSig.signal(msg) + logger.debug(f"[main] {body}") + self.logSig.signal(body) def _handle_ws_log(self, index: int): """ Handle _wsClients[index].logSig """ - - def handler(msg: str): + def handler(body: str): """ handler for Signal """ - logger.debug(f"[{index}] {msg}") - self.logSig.signal(msg) - + logger.debug(f"[{index}] {body}") + self.logSig.signal(body) return handler def _handle_ws_main_new_msg(self, resp: str): @@ -1210,26 +1174,22 @@ class ConnectionPool(QObject): def _handle_async_http_net_error(self, index: int): """ Handle _asyncHttpClients[index].netRepErrorSig """ - def handler(reply: NetworkReplyError): """ handler for Signal """ - logger.error(f"[{index}] {int(reply.code.value)} - {reply.msg}") - self.logSig.signal(reply.msg) + logger.error(f"[{index}] {int(reply.code.value)} - {reply.body}") + self.logSig.signal(reply.body) return handler def _handle_async_http_resp_err(self, index: int): """ Handle _asyncHttpClients[index].responseErrorSig """ - def handler(reply: ResponseError): """ handler for Signal """ logger.error(f"[{index}] {reply.code} - {reply.body}") self.logSig.signal(reply.body) - return handler def _handle_async_http_new_msg(self, index: int): """ Handle _asyncHttpClients[index].logSig """ - def handler(reply: ResponseOk): """ handler for Signal """ timestamp = datetime.now() @@ -1240,14 +1200,17 @@ class ConnectionPool(QObject): return handler def _start(self): + """ Start the Timers """ self._timer_reset_counters.start(100) # reset counters a font every 100ms self._timer_process_queues.start(10) # process_queues every 10 ms def _stop(self): + """ Stop the Timers """ self._timer_reset_counters.stop() # reset counters a font every 10ms self._timer_process_queues.stop() # process_queues every 1 ms def open(self): + """ Open Ws Clients connexion """ if not self._connected: self._start() self._wsMainClient.open() @@ -1255,6 +1218,7 @@ class ConnectionPool(QObject): client.open() def close(self): + """ Close Ws Clients connexion """ if self._connected: self._connected = False self._stop() @@ -1263,13 +1227,14 @@ class ConnectionPool(QObject): client.close() def send_websocket_main_request(self, rpc_method: str, message: str): - """Envoie un message WebSocket avec une méthode RPC spécifique.""" + """ Sends a WebSocket message with a specific RPC method. """ if not rpc_method in RPC_WS_REQ: + # must never happen logger.debug(f"Wrong Rpc Request {rpc_method}.") raise RuntimeError(f"Wrong Rpc Request {rpc_method}.") size = len(message.encode("utf-8")) if not self._check_limits(rpc_method, size) or self._wsMainClient.get_socket_state() != QAbstractSocket.SocketState.ConnectedState: - self._wsMainQueue.put((rpc_method, message)) # Mise en file si limites atteintes + self._wsMainQueue.put((rpc_method, message)) # Queue if limits reached return with self._lock: self._currentRequestCount += 1 @@ -1278,25 +1243,27 @@ class ConnectionPool(QObject): self._wsMainClient.send_msg(message) def send_websocket_request(self, rpc_method: str, message: str): - """Envoie un message WebSocket avec une méthode RPC spécifique.""" + """ Sends a WebSocket message with a specific RPC method. """ if not rpc_method in RPC_WS_REQ: + # must never happen logger.debug(f"Wrong Rpc Request {rpc_method}.") raise RuntimeError(f"Wrong Rpc Request {rpc_method}.") size = len(message.encode("utf-8")) if not self._check_limits(rpc_method, size) or not self._connectedWsClients: - self._wsQueue.put((rpc_method, message)) # Mise en file si limites atteintes + self._wsQueue.put((rpc_method, message)) # Queue if limits reached return with self._lock: self._currentRequestCount += 1 self._currentRpcCounters[rpc_method] += 1 self._currentDataSize += size - client = self._connectedWsClients.pop(0) # Utiliser un client connecté + client = self._connectedWsClients.pop(0) # Pop and use a connected client client.send_msg(message) - self._connectedWsClients.append(client) # Réinsérer dans la liste + self._connectedWsClients.append(client) # Reinsert into the list of connected clients def send_async_http_post(self, rpc_method: str, body: Union[bytes, str]): - """Ajoute une requête HTTP dans la file d'attente.""" + """ Sends a HTTP Post message with a specific RPC method. """ if not rpc_method in RPC_HTTP_REQ: + # must never happen logger.debug(f"Wrong Rpc Request {rpc_method}.") raise RuntimeError(f"Wrong Rpc Request {rpc_method}.") size = len(body) if body else 0 @@ -1312,15 +1279,16 @@ class ConnectionPool(QObject): self._asyncHttpClients.append(client) def send_http_post(self, rpc_method: str, body: Union[bytes, str]) -> Union[NetworkReplyError, ResponseError, ResponseOk]: + """ Sends a HTTP Post message with a specific RPC method. """ if rpc_method not in RPC_HTTP_REQ: + # must never happen logger.debug(f"Wrong Rpc Request {rpc_method}.") raise RuntimeError(f"Wrong Rpc Request {rpc_method}.") size = len(body) if body else 0 while True: - with self._lock: - if self._check_limits(rpc_method, size) and self._syncHttpClients: - client = self._syncHttpClients.pop(0) - break + if self._check_limits(rpc_method, size) and self._syncHttpClients: + client = self._syncHttpClients.pop(0) + break response = client.send_post(body) with self._lock: self._currentRequestCount += 1 @@ -1337,87 +1305,84 @@ class ConnectionPool(QObject): return len(self._wsMainReceivedMessages) if self._wsMainReceivedMessages else 0 def read_first_ws_main_msg(self): - """Lire et supprimer le premier message.""" + """ Read and delete the first main WS message. """ return self._wsMainReceivedMessages.popleft() if self._wsMainReceivedMessages else None def read_last_ws_main_msg(self): - """Lire et supprimer le dernier message.""" + """ Read and delete the last main WS message. """ return self._wsMainReceivedMessages.pop() if self._wsMainReceivedMessages else None def get_count_ws_msgs(self): - """ Get count of the fifo list of messages from websocket """ + """ Get count of the fifo list of messages from websockets """ return len(self._wsReceivedMessages) if self._wsReceivedMessages else 0 def read_first_ws_msg(self): - """Lire et supprimer le premier message.""" + """ Read and delete the first WS message. """ return self._wsReceivedMessages.popleft() if self._wsReceivedMessages else None def read_last_ws_msg(self): - """Lire et supprimer le dernier message.""" + """ Read and delete the last WS message. """ return self._wsReceivedMessages.pop() if self._wsReceivedMessages else None def get_count_http_msgs(self): - """ Get count of the fifo list of messages from http client """ + """ Get count of the fifo list of messages from http clients """ return len(self._httpReceivedMessages) if self._httpReceivedMessages else 0 def read_first_http_msg(self): - """Lire et supprimer le premier message.""" + """ Read and delete the first Http Async message. """ return self._httpReceivedMessages.popleft() if self._httpReceivedMessages else None def read_last_http_msg(self): - """Lire et supprimer le dernier message.""" + """ Read and delete the last Http Async message. """ return self._httpReceivedMessages.pop() if self._httpReceivedMessages else None def clean_up(self): + """ Close connexions and stop timers """ self.close() self._stop() class WsReqGen: """ Generate requests for Solana Websocket client""" - @classmethod - def account_subscribe(cls, pubkey: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, encoding_to_use: Optional[UiAccountEncoding] = None) -> (int, str, AccountSubscribe): - """Subscribe to an account to receive notifications when the lamports or data change. - + def account_subscribe(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, encoding_to_use: Optional[UiAccountEncoding] = None) -> (int, str, AccountSubscribe): + """ Subscribe to an account to receive notifications when the lamports or data change. Args: - pubkey: Account pubkey. - commitment_to_use: Commitment level. - encoding_to_use: Encoding to use. + pubkey_to_use (Pubkey): Account pubkey. + commitment_to_use (Optional[CommitmentLevel]): Commitment level. + encoding_to_use (Optional[UiAccountEncoding]): Encoding to use. + Returns: + int, str, AccountSubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_account_info_config = None if commitment_to_use is None and encoding_to_use is None else RpcAccountInfoConfig( - encoding = encoding_to_use, commitment = commitment_to_use) - req = AccountSubscribe(pubkey, rpc_account_info_config, next_id) + rpc_account_info_config = None if commitment_to_use is None and encoding_to_use is None else RpcAccountInfoConfig(encoding = encoding_to_use, commitment = commitment_to_use) + req = AccountSubscribe(pubkey_to_use, rpc_account_info_config, next_id) return next_id, "accountSubscribe", req @classmethod def account_unsubscribe(cls, subscription_id: int) -> (int, str, AccountUnsubscribe): - """Unsubscribe from transaction logging. - + """ Unsubscribe from transaction logging. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, AccountUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = AccountUnsubscribe(subscription_id, next_id) return next_id, "accountUnsubscribe", req @classmethod - def block_subscribe(cls, rpc_filer: Union[ - RpcBlockSubscribeFilter, RpcBlockSubscribeFilterMentions] = RpcBlockSubscribeFilter.All, - commitment_to_use: Optional[CommitmentLevel] = None, - encoding_to_use: Optional[UiTransactionEncoding] = None, - transaction_details: Union[TransactionDetails, None] = None, show_rewards: Optional[bool] = None, - max_supported_transaction_version: Optional[int] = None) -> (int, str, BlockSubscribe): - """Subscribe to blocks. - + def block_subscribe(cls, rpc_filer: Union[RpcBlockSubscribeFilter, RpcBlockSubscribeFilterMentions] = RpcBlockSubscribeFilter.All, commitment_to_use: Optional[CommitmentLevel] = None, encoding_to_use: Optional[UiTransactionEncoding] = None, transaction_details: Optional[TransactionDetails] = None, show_rewards: Optional[bool] = None, max_supported_transaction_version: Optional[int] = None) -> (int, str, BlockSubscribe): + """ Subscribe to blocks. Args: - rpc_filer: filter criteria for the blocks. - commitment_to_use: The commitment level to use. - encoding_to_use: Encoding to use. - transaction_details: level of transaction detail to return. - show_rewards: whether to populate the rewards array. - max_supported_transaction_version: the max transaction version to return in responses. + rpc_filer (Union[RpcBlockSubscribeFilter, RpcBlockSubscribeFilterMentions]): filter criteria for the blocks. + commitment_to_use (Optional[CommitmentLevel]): The commitment level to use. + encoding_to_use (Optional[UiTransactionEncoding]): Encoding to use. + transaction_details (Optional[TransactionDetails]): level of transaction detail to return. + show_rewards (Optional[bool]): whether to populate the rewards array. + max_supported_transaction_version (Optional[int] ): the max transaction version to return in responses. + Returns: + int, str, BlockSubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_block_subscribe_config = None if commitment_to_use is None and encoding_to_use is None and transaction_details is None and show_rewards is None and max_supported_transaction_version is None else RpcBlockSubscribeConfig( @@ -1428,24 +1393,24 @@ class WsReqGen: @classmethod def block_unsubscribe(cls, subscription_id: int) -> (int, str, BlockUnsubscribe): - """Unsubscribe from blocks. - + """ Unsubscribe from blocks. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, BlockSubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = BlockUnsubscribe(subscription_id, next_id) return next_id, "blockUnsubscribe", req @classmethod - def logs_subscribe(cls, rpc_filer: Union[ - RpcTransactionLogsFilter, RpcTransactionLogsFilterMentions] = RpcTransactionLogsFilter.All, - commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, LogsSubscribe): - """Subscribe to transaction logging. - + def logs_subscribe(cls, rpc_filer: Union[RpcTransactionLogsFilter, RpcTransactionLogsFilterMentions] = RpcTransactionLogsFilter.All, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, LogsSubscribe): + """ Subscribe to transaction logging. Args: - rpc_filer: filter criteria for the logs. - commitment_to_use: The commitment level to use. + rpc_filer (Union[RpcTransactionLogsFilter, RpcTransactionLogsFilterMentions]): filter criteria for the logs. + commitment_to_use (Optional[CommitmentLevel]): The commitment level to use. + Returns: + int, str, LogsSubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_transaction_logs_config = None if commitment_to_use is None else RpcTransactionLogsConfig(commitment = commitment_to_use) @@ -1454,29 +1419,27 @@ class WsReqGen: @classmethod def logs_unsubscribe(cls, subscription_id: int) -> (int, str, LogsUnsubscribe): - """Unsubscribe from transaction logging. - + """ Unsubscribe from transaction logging. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, LogsUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = LogsUnsubscribe(subscription_id, next_id) return next_id, "logsUnsubscribe", req @classmethod - def program_subscribe(cls, program_id: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, - encoding_to_use: Optional[UiAccountEncoding] = None, data_slice: Optional[DataSliceOpts] = None, - filters: Optional[Sequence[Union[int, MemcmpOpts]]] = None) -> (int, str, ProgramSubscribe): - """Receive notifications when the lamports or data for a given account owned by the program changes. - + def program_subscribe(cls, program_id: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, encoding_to_use: Optional[UiAccountEncoding] = None, data_slice: Optional[DataSliceOpts] = None, filters: Optional[Sequence[Union[int, MemcmpOpts]]] = None) -> (int, str, ProgramSubscribe): + """ Receive notifications when the lamports or data for a given account owned by the program changes. Args: - program_id: The program ID. - commitment_to_use: Commitment level to use. - encoding_to_use: Encoding to use. - data_slice: (optional) Limit the returned account data using the provided `offset`: and - ` length`: fields; only available for "base58" or "base64" encoding. - filters: (optional) Options to compare a provided series of bytes with program account data at a particular offset. - Note: an int entry is converted to a `dataSize` filter. + program_id (Pubkey): The program ID. + commitment_to_use (Optional[CommitmentLevel]): Commitment level to use. + encoding_to_use (Optional[UiAccountEncoding]): Encoding to use. + data_slice (Optional[DataSliceOpts]): Limit the returned account data using the provided `offset`: and `length`: fields; only available for "base58" or "base64" encoding. + filters (Optional[Sequence[Union[int, MemcmpOpts]]]): Options to compare a provided series of bytes with program account data at a particular offset. Note: an int entry is converted to a `dataSize` filter. + Returns: + int, str, ProgramSubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() if commitment_to_use is None and encoding_to_use is None and data_slice is None and filters is None: @@ -1493,9 +1456,10 @@ class WsReqGen: @classmethod def program_unsubscribe(cls, subscription_id: int) -> (int, str, ProgramUnsubscribe): """Unsubscribe from program account notifications. - Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, ProgramUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = ProgramUnsubscribe(subscription_id, next_id) @@ -1503,42 +1467,47 @@ class WsReqGen: @classmethod def root_subscribe(cls) -> (int, str, RootSubscribe): - """Subscribe to receive notification anytime a new root is set by the validator.""" + """ Subscribe to receive notification anytime a new root is set by the validator. + Returns: + int, str, RootSubscribe: next_id, RPC method name, solders request object + """ next_id = SharedCounter.get_next_id() req = RootSubscribe(next_id) return next_id, "rootSubscribe", req @classmethod def root_unsubscribe(cls, subscription_id: int) -> (int, str, RootUnsubscribe): - """Unsubscribe from root notifications. - + """ Unsubscribe from root notifications. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, RootUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = RootUnsubscribe(subscription_id, next_id) return next_id, "rootUnsubscribe", req @classmethod - def signature_subscribe(cls, signature: Signature, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, SignatureSubscribe): - """Subscribe to a transaction signature to receive notification when the transaction is confirmed. - + def signature_subscribe(cls, signature_to_use: Signature, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, SignatureSubscribe): + """ Subscribe to a transaction signature to receive notification when the transaction is confirmed. Args: - signature: The transaction signature to subscribe to. - commitment_to_use: Commitment level. + signature_to_use (Signature): The transaction signature to subscribe to. + commitment_to_use (Optional[CommitmentLevel]): Commitment level. + Returns: + int, str, SignatureSubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_signature_subscribe_config = None if commitment_to_use is None else RpcSignatureSubscribeConfig(commitment = commitment_to_use) - req = SignatureSubscribe(signature, rpc_signature_subscribe_config, next_id) + req = SignatureSubscribe(signature_to_use, rpc_signature_subscribe_config, next_id) return next_id, "signatureSubscribe", req @classmethod def signature_unsubscribe(cls, subscription_id: int) -> (int, str, SignatureUnsubscribe): - """Unsubscribe from signature notifications. - + """ Unsubscribe from signature notifications. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, RootUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = SignatureUnsubscribe(subscription_id, next_id) @@ -1546,7 +1515,10 @@ class WsReqGen: @classmethod def slot_subscribe(cls) -> (int, str, SlotSubscribe): - """Subscribe to receive notification anytime a slot is processed by the validator.""" + """ Subscribe to receive notification anytime a slot is processed by the validator. + Returns: + int, str, SlotSubscribe: next_id, RPC method name, solders request object + """ next_id = SharedCounter.get_next_id() req = SlotSubscribe(next_id) return next_id, "slotSubscribe", req @@ -1554,9 +1526,10 @@ class WsReqGen: @classmethod def slot_unsubscribe(cls, subscription_id: int) -> (int, str, SlotUnsubscribe): """Unsubscribe from slot notifications. - Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, SlotUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = SlotUnsubscribe(subscription_id, next_id) @@ -1564,17 +1537,21 @@ class WsReqGen: @classmethod def slots_updates_subscribe(cls) -> (int, str, SlotsUpdatesSubscribe): - """Subscribe to receive a notification from the validator on a variety of updates on every slot.""" + """ Subscribe to receive a notification from the validator on a variety of updates on every slot. + Returns: + int, str, SlotsUpdatesSubscribe: next_id, RPC method name, solders request object + """ next_id = SharedCounter.get_next_id() req = SlotsUpdatesSubscribe(next_id) return next_id, "slotsUpdatesSubscribe", req @classmethod def slots_updates_unsubscribe(cls, subscription_id: int) -> (int, str, SlotsUpdatesUnsubscribe): - """Unsubscribe from slot update notifications. - + """ Unsubscribe from slot update notifications. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, SlotsUpdatesUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = SlotsUpdatesUnsubscribe(subscription_id, next_id) @@ -1582,17 +1559,21 @@ class WsReqGen: @classmethod def vote_subscribe(cls) -> (int, str, VoteSubscribe): - """Subscribe to receive notification anytime a new vote is observed in gossip.""" + """ Subscribe to receive notification anytime a new vote is observed in gossip. + Returns: + int, str, VoteSubscribe: next_id, RPC method name, solders request object + """ next_id = SharedCounter.get_next_id() req = VoteSubscribe(next_id) return next_id, "voteSubscribe", req @classmethod def vote_unsubscribe(cls, subscription_id: int) -> (int, str, VoteUnsubscribe): - """Unsubscribe from vote notifications. - + """ Unsubscribe from vote notifications. Args: - subscription_id: ID of subscription to cancel. + subscription_id (int): ID of subscription to cancel. + Returns: + int, str, VoteUnsubscribe: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = VoteUnsubscribe(subscription_id, next_id) @@ -1600,83 +1581,80 @@ class WsReqGen: class HttpReqGen: - """ Generate requests for Solana Http client""" - + """ Generate requests for Solana Http client. """ @classmethod - def get_account_info(cls, pubkey: Pubkey, commitment_to_use: Optional[CommitmentLevel] = CommitmentLevel.Finalized, encoding_to_use: Optional[UiAccountEncoding] = UiAccountEncoding.Base64, data_slice: Optional[DataSliceOpts] = None) -> (int, str, GetAccountInfo): - """Returns all the account info for the specified public key. - + def get_account_info(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = CommitmentLevel.Finalized, encoding_to_use: Optional[UiAccountEncoding] = UiAccountEncoding.Base64, data_slice: Optional[DataSliceOpts] = None) -> (int, str, GetAccountInfo): + """ Returns all the account info for the specified public key. Args: - pubkey: Pubkey of account to query - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - encoding_to_use: (optional) Encoding for Account data, either "base58" (slow), "base64", or - "jsonParsed". Default is "base64". - - - "base58" is limited to Account data of less than 128 bytes. - - "base64" will return base64 encoded data for Account data of any size. - - "jsonParsed" encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. - - If jsonParsed is requested but a parser cannot be found, the field falls back to base64 encoding, - detectable when the data field is type. (jsonParsed encoding is UNSTABLE). - data_slice: (optional) Option to limit the returned account data using the provided `offset`: and - `length`: fields; only available for "base58" or "base64" encoding. + pubkey_to_use (Pubkey): Address of account to query + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + encoding_to_use (Optional[UiAccountEncoding]): Encoding for Account data, either "base58" (slow), "base64", or "jsonParsed". + Default is "base64". + "base58" is limited to Account data of less than 128 bytes. + "base64" will return base64 encoded data for Account data of any size. + "jsonParsed" encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. + If jsonParsed is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the data field is type. (jsonParsed encoding is UNSTABLE). + data_slice (Optional[DataSliceOpts]): Option to limit the returned account data using the provided `offset`: and `length`: fields; only available for "base58" or "base64" encoding. + Returns: + int, str, GetAccountInfo: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() data_slice_to_use = ( None if data_slice is None else UiDataSliceConfig(offset = data_slice.offset, length = data_slice.length)) rpc_account_info_config = RpcAccountInfoConfig(encoding = encoding_to_use, data_slice = data_slice_to_use, commitment = commitment_to_use) - req = GetAccountInfo(pubkey, rpc_account_info_config, next_id) + req = GetAccountInfo(pubkey_to_use, rpc_account_info_config, next_id) return next_id, "getAccountInfo", req @classmethod - def get_balance(cls, pubkey: Pubkey, commitment_to_use: Optional[CommitmentLevel] = CommitmentLevel.Finalized) -> ( - int, str, GetBalance): - """Returns the balance of the account of provided Pubkey. - + def get_balance(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = CommitmentLevel.Finalized) -> (int, str, GetBalance): + """ Returns the balance of the account of provided Pubkey. Args: - pubkey: Pubkey of account to query - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of account to query + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetBalance: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = RpcContextConfig(commitment_to_use) - req = GetBalance(pubkey, rpc_context_config, next_id) + req = GetBalance(pubkey_to_use, rpc_context_config, next_id) return next_id, "getBalance", req @classmethod def get_block(cls, slot: int, encoding_to_use: Optional[UiTransactionEncoding] = UiTransactionEncoding.Json, max_supported_transaction_version: Optional[int] = None) -> (int, str, GetBlock): - """Returns identity and transaction information about a confirmed block in the ledger. - + """ Returns identity and transaction information about a confirmed block in the ledger. Args: slot: Slot, as u64 integer. - encoding_to_use: (optional) Encoding for the returned Transaction, either "json", "jsonParsed", + encoding_to_use (Optional[UiTransactionEncoding]): Encoding for the returned Transaction, either "json", "jsonParsed", "base58" (slow), or "base64". If parameter not provided, the default encoding is JSON. max_supported_transaction_version: (optional) The max transaction version to return in responses. If the requested transaction is a higher version, an error will be returned + Returns: + int, str, GetBlock: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_block_config = RpcBlockConfig(encoding = encoding_to_use, - max_supported_transaction_version = max_supported_transaction_version) + rpc_block_config = RpcBlockConfig(encoding = encoding_to_use, max_supported_transaction_version = max_supported_transaction_version) req = GetBlock(slot, rpc_block_config, next_id) return next_id, "getBlock", req @classmethod def get_block_commitment(cls, slot: int) -> (int, str, GetBlockCommitment): - """Fetch the commitment for particular block. - + """ Fetch the commitment for particular block. Args: - slot: Block, identified by Slot. + slot (int): Block, identified by Slot. + Returns: + int, str, GetBlockCommitment: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetBlockCommitment(slot, next_id) return next_id, "getBlockCommitment", req @classmethod - def get_block_height(cls, commitment_to_use: CommitmentLevel = CommitmentLevel.Finalized) -> ( - int, str, GetBlockHeight): - """Returns the current block height of the node. - + def get_block_height(cls, commitment_to_use: CommitmentLevel = CommitmentLevel.Finalized) -> (int, str, GetBlockHeight): + """ Returns the current block height of the node. Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + commitment_to_use (CommitmentLevel): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetBlockHeight: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment = commitment_to_use) @@ -1684,15 +1662,14 @@ class HttpReqGen: return next_id, "getBlockHeight", req @classmethod - def get_block_production(cls, identity_to_use: Optional[Pubkey] = None, - range_to_use: Optional[RpcBlockProductionConfigRange] = None, - commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetBlockProduction): - """Returns recent block production information from the current or previous epoch. - + def get_block_production(cls, identity_to_use: Optional[Pubkey] = None, range_to_use: Optional[RpcBlockProductionConfigRange] = None, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetBlockProduction): + """ Returns recent block production information from the current or previous epoch. Args: - identity_to_use: Only return results for this validator identity (base-58 encoded) - range_to_use: Slot range to return block production for. If parameter not provided, defaults to current epoch - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + identity_to_use (Optional[Pubkey]): Only return results for this validator identity (base-58 encoded) + range_to_use (Optional[RpcBlockProductionConfigRange]): Slot range to return block production for. If parameter not provided, defaults to current epoch + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetBlockProduction: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_block_production_config = None if identity_to_use is None and range_to_use is None and commitment_to_use is None else RpcBlockProductionConfig( @@ -1702,10 +1679,11 @@ class HttpReqGen: @classmethod def get_block_time(cls, slot: int) -> (int, str, GetBlockTime): - """Fetch the estimated production time of a block. - + """ Fetch the estimated production time of a block. Args: - slot: Block, identified by Slot. + slot (int): Block, identified by Slot. + Returns: + int, str, GetBlockTime: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetBlockTime(slot, next_id) @@ -1713,26 +1691,27 @@ class HttpReqGen: @classmethod def get_blocks(cls, start_slot: int, end_slot: Optional[int] = None, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetBlocks): - """Returns a list of confirmed blocks. - + """ Returns a list of confirmed blocks. Args: - start_slot: Start slot, as u64 integer. - end_slot: (optional) End slot, as u64 integer. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + start_slot (int): Start slot, as u64 integer. + end_slot (Optional[int]): End slot, as u64 integer. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetBlocks: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetBlocks(start_slot, end_slot, commitment_to_use, next_id) return next_id, "getBlocks", req @classmethod - def get_blocks_with_limit(cls, start_slot: int, limit_to_use: Optional[int] = None, - commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetBlocksWithLimit): - """Returns a list of confirmed blocks. - + def get_blocks_with_limit(cls, start_slot: int, limit_to_use: Optional[int] = None, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetBlocksWithLimit): + """ Returns a list of confirmed blocks. Args: - start_slot: Start slot, as u64 integer. - limit_to_use: (optional) limit, as u64 integer (must be no more than 500,000 blocks higher than the startSlot. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed".) + start_slot (int): Start slot, as u64 integer. + limit_to_use (Optional[int]): limit, as u64 integer (must be no more than 500,000 blocks higher than the startSlot. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed".) + Returns: + int, str, GetBlocksWithLimit: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetBlocksWithLimit(start_slot, limit_to_use, commitment_to_use, next_id) @@ -1740,7 +1719,9 @@ class HttpReqGen: @classmethod def get_cluster_nodes(cls) -> (int, str, GetClusterNodes): - """Returns information about all the nodes participating in the cluster. + """ Returns information about all the nodes participating in the cluster. + Returns: + int, str, GetClusterNodes: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetClusterNodes(next_id) @@ -1748,10 +1729,11 @@ class HttpReqGen: @classmethod def get_epoch_info(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetEpochInfo): - """Returns information about the current epoch. - + """ Returns information about the current epoch. Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetEpochInfo: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment_to_use) @@ -1760,20 +1742,22 @@ class HttpReqGen: @classmethod def get_epoch_schedule(cls) -> (int, str, GetEpochSchedule): - """Returns epoch schedule information from this cluster's genesis config. + """ Returns epoch schedule information from this cluster's genesis config. + Returns: + int, str, GetEpochSchedule: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetEpochSchedule(next_id) return next_id, "getEpochSchedule", req @classmethod - def get_fee_for_message(cls, message_to_use: VersionedMessage, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetFeeForMessage): - """Returns the fee for a message. - + def get_fee_for_message(cls, message_to_use: VersionedMessage, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetFeeForMessage): + """ Returns the fee for a message. Args: - message_to_use: Message that the fee is requested for. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + message_to_use (VersionedMessage): Message that the fee is requested for. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetFeeForMessage: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetFeeForMessage(message_to_use, commitment_to_use, next_id) @@ -1781,7 +1765,9 @@ class HttpReqGen: @classmethod def get_first_available_block(cls) -> (int, str, GetFirstAvailableBlock): - """Returns the slot of the lowest confirmed block that has not been purged from the ledger. + """ Returns the slot of the lowest confirmed block that has not been purged from the ledger. + Returns: + int, str, GetFirstAvailableBlock: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetFirstAvailableBlock(next_id) @@ -1789,7 +1775,9 @@ class HttpReqGen: @classmethod def get_genesis_hash(cls) -> (int, str, GetGenesisHash): - """Returns the genesis hash. + """ Returns the genesis hash. + Returns: + int, str, GetGenesisHash: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetGenesisHash(next_id) @@ -1797,8 +1785,10 @@ class HttpReqGen: @classmethod def get_health(cls) -> (int, str, GetHealth): - """Returns the current health of the node. + """ Returns the current health of the node. A healthy node is one that is within HEALTH_CHECK_SLOT_DISTANCE slots of the latest cluster confirmed slot. + Returns: + int, str, GetHealth: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetHealth(next_id) @@ -1806,7 +1796,9 @@ class HttpReqGen: @classmethod def get_highest_snapshot_slot(cls) -> (int, str, GetHighestSnapshotSlot): - """Returns the highest slot information that the node has snapshots for. + """ Returns the highest slot information that the node has snapshots for. + Returns: + int, str, GetHighestSnapshotSlot: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetHighestSnapshotSlot(next_id) @@ -1814,19 +1806,21 @@ class HttpReqGen: @classmethod def get_identity(cls) -> (int, str, GetIdentity): - """Returns the identity pubkey for the current node + """ Returns the identity pubkey for the current node + Returns: + int, str, GetIdentity: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetIdentity(next_id) return next_id, "getIdentity", req @classmethod - def get_inflation_governor(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetInflationGovernor): - """Returns the current inflation governor. - + def get_inflation_governor(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetInflationGovernor): + """ Returns the current inflation governor. Args: commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetInflationGovernor: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetInflationGovernor(commitment_to_use, next_id) @@ -1834,21 +1828,23 @@ class HttpReqGen: @classmethod def get_inflation_rate(cls) -> (int, str, GetInflationRate): - """Returns the specific inflation values for the current epoch. + """ Returns the specific inflation values for the current epoch. + Returns: + int, str, GetInflationRate: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetInflationRate(next_id) return next_id, "getInflationRate", req @classmethod - def get_inflation_reward(cls, pubkeys_to_use: Sequence[Pubkey], epoch_to_use: Optional[int] = None, - commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetInflationReward): - """Returns the inflation / staking reward for a list of addresses for an epoch. - + def get_inflation_reward(cls, pubkeys_to_use: Sequence[Pubkey], epoch_to_use: Optional[int] = None, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetInflationReward): + """ Returns the inflation / staking reward for a list of addresses for an epoch. Args: - pubkeys_to_use: An array of addresses to query, as base-58 encoded strings - epoch_to_use: (optional) An epoch for which the reward occurs. If omitted, the previous epoch will be used - commitment_to_use: Bank state to query. It can be either "finalized" or "confirmed". + pubkeys_to_use (Sequence[Pubkey]): An array of addresses to query, as base-58 encoded strings + epoch_to_use (Optional[int]): An epoch for which the reward occurs. If omitted, the previous epoch will be used + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized" or "confirmed". + Returns: + int, str, GetInflationReward: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_epoch_config = None if epoch_to_use is None and commitment_to_use is None else RpcEpochConfig(epoch_to_use, commitment_to_use) @@ -1856,13 +1852,13 @@ class HttpReqGen: return next_id, "getInflationReward", req @classmethod - def get_largest_accounts(cls, commitment_to_use: Optional[CommitmentLevel] = None, - filter_to_use: Optional[RpcLargestAccountsFilter] = None) -> (int, str, GetLargestAccounts): - """Returns the 20 largest accounts, by lamport balance. - + def get_largest_accounts(cls, commitment_to_use: Optional[CommitmentLevel] = None, filter_to_use: Optional[RpcLargestAccountsFilter] = None) -> (int, str, GetLargestAccounts): + """ Returns the 20 largest accounts, by lamport balance. Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - filter_to_use: Filter results by account type; currently supported: circulating|nonCirculating. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + filter_to_use (Optional[RpcLargestAccountsFilter]): Filter results by account type; currently supported: circulating|nonCirculating. + Returns: + int, str, GetLargestAccounts: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetLargestAccounts(commitment_to_use, filter_to_use, next_id) @@ -1870,12 +1866,11 @@ class HttpReqGen: @classmethod def get_latest_blockhash(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetLatestBlockhash): - """Returns the latest block hash from the ledger. - - Response also includes the last valid block height. - + """ Returns the latest block hash from the ledger. Response also includes the last valid block height. Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetLatestBlockhash: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment_to_use) @@ -1883,25 +1878,25 @@ class HttpReqGen: return next_id, "getLatestBlockhash", req @classmethod - def get_leader_schedule(cls, epoch_to_use: Optional[int] = None, identity_to_use: Optional[Pubkey] = None, - commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetLeaderSchedule): - """Returns the leader schedule for an epoch. - + def get_leader_schedule(cls, epoch_to_use: Optional[int] = None, identity_to_use: Optional[Pubkey] = None, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetLeaderSchedule): + """ Returns the leader schedule for an epoch. Args: - epoch_to_use: Fetch the leader schedule for the epoch that corresponds to the provided slot. - If unspecified, the leader schedule for the current epoch is fetched. - identity_to_use: Only return results for this validator identity (base-58 encoded) - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + epoch_to_use (Optional[int]): Fetch the leader schedule for the epoch that corresponds to the provided slot. If unspecified, the leader schedule for the current epoch is fetched. + identity_to_use (Optional[Pubkey]): Only return results for this validator identity (base-58 encoded) + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetLeaderSchedule: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_leader_schedule_config = None if identity_to_use is None and commitment_to_use is None else RpcLeaderScheduleConfig(identity_to_use, - commitment_to_use) + rpc_leader_schedule_config = None if identity_to_use is None and commitment_to_use is None else RpcLeaderScheduleConfig(identity_to_use, commitment_to_use) req = GetLeaderSchedule(epoch_to_use, rpc_leader_schedule_config, next_id) return next_id, "getLeaderSchedule", req @classmethod def get_max_retransmit_slot(cls) -> (int, str, GetMaxRetransmitSlot): - """Get the max slot seen from retransmit stage. + """ Get the max slot seen from retransmit stage. + Returns: + int, str, GetMaxRetransmitSlot: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetMaxRetransmitSlot(next_id) @@ -1909,82 +1904,73 @@ class HttpReqGen: @classmethod def get_max_shred_insert_slot(cls) -> (int, str, GetMaxShredInsertSlot): - """Get the max slot seen from after shred insert. + """ Get the max slot seen from after shred insert. + Returns: + int, str, GetAccountInfo: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetMaxShredInsertSlot(next_id) return next_id, "getMaxShredInsertSlot", req @classmethod - def get_minimum_balance_for_rent_exemption(cls, min_size: int, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetMinimumBalanceForRentExemption): - """Returns minimum balance required to make account rent exempt. - + def get_minimum_balance_for_rent_exemption(cls, min_size: int, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetMinimumBalanceForRentExemption): + """ Returns minimum balance required to make account rent exempt. Args: - min_size: Account data length. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + min_size (int): Account data length. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetMinimumBalanceForRentExemption: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetMinimumBalanceForRentExemption(min_size, commitment_to_use, next_id) return next_id, "getMinimumBalanceForRentExemption", req @classmethod - def get_multiple_accounts(cls, pubkeys_to_use: Sequence[Pubkey], - commitment_to_use: Optional[CommitmentLevel] = CommitmentLevel.Finalized, - encoding_to_use: Optional[UiAccountEncoding] = UiAccountEncoding.Base64, - data_slice: Optional[DataSliceOpts] = None) -> (int, str, GetMultipleAccounts): - """Returns all the account info for a list of public keys. - + def get_multiple_accounts(cls, pubkeys_to_use: Sequence[Pubkey], commitment_to_use: Optional[CommitmentLevel] = CommitmentLevel.Finalized, encoding_to_use: Optional[UiAccountEncoding] = UiAccountEncoding.Base64, data_slice: Optional[DataSliceOpts] = None) -> (int, str, GetMultipleAccounts): + """ Returns all the account info for a list of public keys. Args: - pubkeys_to_use: list of Pubkeys to query - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - encoding_to_use: (optional) Encoding for Account data, either "base58" (slow) or "base64". - - - "base58" is limited to Account data of less than 128 bytes. - - "base64" will return base64 encoded data for Account data of any size. - - data_slice: (optional) Option to limit the returned account data using the provided `offset`: and - `length`: fields; only available for "base58" or "base64" encoding. + pubkeys_to_use (Sequence[Pubkey]): list of Pubkeys to query + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". Default to finalized. + encoding_to_use (Optional[UiAccountEncoding]): Encoding for Account data, either "base58" (slow) or "base64". + "base58" is limited to Account data of less than 128 bytes. + "base64" will return base64 encoded data for Account data of any size. + data_slice (Optional[DataSliceOpts]): (optional) Option to limit the returned account data using the provided `offset`: and `length`: fields; only available for "base58" or "base64" encoding. + Returns: + int, str, GetMultipleAccounts: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - data_slice_to_use = ( - None if data_slice is None else UiDataSliceConfig(offset = data_slice.offset, length = data_slice.length)) + data_slice_to_use = (None if data_slice is None else UiDataSliceConfig(offset = data_slice.offset, length = data_slice.length)) rpc_account_info_config = RpcAccountInfoConfig(encoding = encoding_to_use, commitment = commitment_to_use, data_slice = data_slice_to_use) req = GetMultipleAccounts(pubkeys_to_use, rpc_account_info_config, next_id) return next_id, "getMultipleAccounts", req @classmethod - def get_program_accounts(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, - encoding_to_use: Optional[UiAccountEncoding] = None, data_slice: Optional[DataSliceOpts] = None, - filters: Optional[Sequence[Union[int, MemcmpOpts]]] = None) -> ( - int, str, GetProgramAccounts): - """Returns all accounts owned by the provided program Pubkey. - + def get_program_accounts(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, encoding_to_use: Optional[UiAccountEncoding] = None, data_slice: Optional[DataSliceOpts] = None, filters: Optional[Sequence[Union[int, MemcmpOpts]]] = None) -> (int, str, GetProgramAccounts): + """ Returns all accounts owned by the provided program Pubkey. Args: - pubkey_to_use: Pubkey of program - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - encoding_to_use: (optional) Encoding for the returned Transaction, either jsonParsed", - "base58" (slow), or "base64". - data_slice: (optional) Limit the returned account data using the provided `offset`: and - `length`: fields; only available for "base58" or "base64" encoding. - filters: (optional) Options to compare a provided series of bytes with program account data at a particular offset. - Note: an int entry is converted to a `dataSize` filter. + pubkey_to_use (Pubkey): Pubkey of program + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + encoding_to_use (Optional[UiAccountEncoding]): Encoding for the returned Transaction, either jsonParsed", "base58" (slow), or "base64". + data_slice (Optional[DataSliceOpts]): Limit the returned account data using the provided `offset`: and `length`: fields; only available for "base58" or "base64" encoding. + filters (Optional[Sequence[Union[int, MemcmpOpts]]]): Options to compare a provided series of bytes with program account data at a particular offset. Note: an int entry is converted to a `dataSize` filter. + Returns: + int, str, GetProgramAccounts: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() data_slice_to_use = (None if data_slice is None else UiDataSliceConfig(offset = data_slice.offset, length = data_slice.length)) rpc_account_info_config = RpcAccountInfoConfig(encoding = encoding_to_use, commitment = commitment_to_use, data_slice = data_slice_to_use) - filters_to_use: Optional[List[Union[int, Memcmp]]] = (None if filters is None else [x if isinstance(x, int) else Memcmp(offset = x.offset, bytes_ = x.bytes) for x in filters]) + filters_to_use = (None if filters is None else [x if isinstance(x, int) else Memcmp(offset = x.offset, bytes_ = x.bytes) for x in filters]) rpc_program_accounts_config = RpcProgramAccountsConfig(rpc_account_info_config, filters_to_use) req = GetProgramAccounts(pubkey_to_use, rpc_program_accounts_config, next_id) return next_id, "getProgramAccounts", req @classmethod def get_recent_performance_samples(cls, limit: int) -> (int, str, GetRecentPerformanceSamples): - """Returns a list of recent performance samples, in reverse slot order. - Performance samples are taken every 60 seconds and include the number of transactions and slots that occur in a given time window. - + """ Returns a list of recent performance samples, in reverse slot order. Performance samples are taken every 60 seconds and include the number of transactions and slots that occur in a given time window. Args: - limit: number of samples to return (maximum 720) + limit (int): number of samples to return (maximum 720) + Returns: + int, str, GetRecentPerformanceSamples: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetRecentPerformanceSamples(limit, next_id) @@ -1993,28 +1979,24 @@ class HttpReqGen: # TODO: where is GetRecentPrioritizationFees in solders # @classmethod # def getRecentPrioritizationFees(cls, pubkeys_to_use: Sequence[Pubkey]) -> (int, str, GetRecentPrioritizationFees): - # """Returns a list of prioritization fees from recent blocks. - # + # """ Returns a list of prioritization fees from recent blocks. # Args: - # pubkeys_to_use: An array of Account addresses (up to a maximum of 128 addresses), as base-58 encoded strings + # pubkeys_to_use (Sequence[Pubkey]): An array of Account addresses (up to a maximum of 128 addresses), as base-58 encoded strings + # Returns: + # int, str, GetRecentPrioritizationFees: next_id, RPC method name, solders request object # """ # next_id = SharedCounter.get_next_id() # req = GetRecentPrioritizationFees(pubkeys_to_use, id) # return next_id, "getRecentPrioritizationFees", req @classmethod - def get_signature_statuses(cls, signatures: Sequence[Signature], search_transaction_history: bool = False) -> ( - int, str, GetSignatureStatuses): - """Returns the statuses of a list of signatures. - - Unless the `searchTransactionHistory` configuration parameter is included, this method only - searches the recent status cache of signatures, which retains statuses for all active slots plus - `MAX_RECENT_BLOCKHASHES` rooted slots. - + def get_signature_statuses(cls, signatures: Sequence[Signature], search_transaction_history: bool = False) -> (int, str, GetSignatureStatuses): + """ Returns the statuses of a list of signatures. Unless the `searchTransactionHistory` configuration parameter is included, this method only searches the recent status cache of signatures, which retains statuses for all active slots plus `MAX_RECENT_BLOCKHASHES` rooted slots. Args: - signatures: An array of transaction signatures to confirm. - search_transaction_history: If true, a Solana node will search its ledger cache for - any signatures not found in the recent status cache. + signatures (Sequence[Signature]): An array of transaction signatures to confirm. + search_transaction_history (bool): If true, a Solana node will search its ledger cache for any signatures not found in the recent status cache. + Returns: + int, str, GetSignatureStatuses: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_signature_status_config = RpcSignatureStatusConfig(search_transaction_history) @@ -2022,31 +2004,29 @@ class HttpReqGen: return next_id, "getSignatureStatuses", req @classmethod - def get_signatures_for_address(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, - limit_to_use: Optional[int] = None, before_to_use: Optional[Signature] = None, - until_to_use: Optional[Signature] = None) -> (int, str, GetSignaturesForAddress): - """Returns signatures for confirmed transactions that include the given address in their accountKeys list. - Returns signatures backwards in time from the provided signature or most recent confirmed block - + def get_signatures_for_address(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None, limit_to_use: Optional[int] = None, before_to_use: Optional[Signature] = None, until_to_use: Optional[Signature] = None) -> (int, str, GetSignaturesForAddress): + """ Returns signatures for confirmed transactions that include the given address in their accountKeys list. Returns signatures backwards in time from the provided signature or most recent confirmed block Args: - pubkey_to_use: Account address as base-58 encoded string - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - limit_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - before_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - until_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Account address as base-58 encoded string + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + limit_to_use (Optional[int]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + before_to_use (Optional[Signature]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + until_to_use (Optional[Signature]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetSignaturesForAddress: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_signatures_for_address_config = None if commitment_to_use is None and limit_to_use is None and before_to_use is None and until_to_use is None else RpcSignaturesForAddressConfig( - before = before_to_use, until = until_to_use, limit = limit_to_use, commitment = commitment_to_use) + rpc_signatures_for_address_config = None if commitment_to_use is None and limit_to_use is None and before_to_use is None and until_to_use is None else RpcSignaturesForAddressConfig(before = before_to_use, until = until_to_use, limit = limit_to_use, commitment = commitment_to_use) req = GetSignaturesForAddress(pubkey_to_use, rpc_signatures_for_address_config, next_id) return next_id, "getSignaturesForAddress", req @classmethod def get_slot(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetSlot): - """Returns the current slot the node is processing. - + """ Returns the current slot the node is processing. Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetSlot: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment = commitment_to_use) @@ -2055,10 +2035,11 @@ class HttpReqGen: @classmethod def get_slot_leader(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetSlotLeader): - """Returns the current slot leader. - + """ Returns the current slot leader. Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetSlotLeader: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment = commitment_to_use) @@ -2067,11 +2048,12 @@ class HttpReqGen: @classmethod def get_slot_leaders(cls, start: int, limit: int) -> (int, str, GetSlotLeaders): - """Returns the slot leaders for a given slot range - + """ Returns the slot leaders for a given slot range. Args: - start: Start slot, as u64 integer - limit: Limit, as u64 integer (between 1 and 5,000) + start (int): Start slot, as u64 integer + limit (int): Limit, as u64 integer (between 1 and 5,000) + Returns: + int, str, GetSlotLeaders: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetSlotLeaders(start, limit, next_id) @@ -2080,10 +2062,11 @@ class HttpReqGen: # TODO: where is GetStakeMinimumDelegation in solders # @classmethod # def getStakeMinimumDelegation(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetStakeMinimumDelegation): - # """Returns the stake minimum delegation, in lamports. - # + # """ Returns the stake minimum delegation, in lamports. # Args: - # commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + # commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + # Returns: + # int, str, GetStakeMinimumDelegation: next_id, RPC method name, solders request object # """ # next_id = SharedCounter.get_next_id() # rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment = commitment_to_use) @@ -2091,43 +2074,41 @@ class HttpReqGen: # return next_id, "getStakeMinimumDelegation", req @classmethod - def get_supply(cls, exclude_non_circulating_accounts_list: bool = True, - commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetSupply): - """Returns information about the current supply. - + def get_supply(cls, exclude_non_circulating_accounts_list: bool = True, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetSupply): + """ Returns information about the current supply. Args: - exclude_non_circulating_accounts_list: exclude non circulating accounts list from response - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + exclude_non_circulating_accounts_list (bool): exclude non circulating accounts list from response + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetSupply: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_supply_config = RpcSupplyConfig(exclude_non_circulating_accounts_list = exclude_non_circulating_accounts_list, - commitment = commitment_to_use) + rpc_supply_config = RpcSupplyConfig(exclude_non_circulating_accounts_list = exclude_non_circulating_accounts_list, commitment = commitment_to_use) req = GetSupply(rpc_supply_config, next_id) return next_id, "getSupply", req @classmethod - def get_token_account_balance(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetTokenAccountBalance): - """Returns the token balance of an SPL Token account (UNSTABLE). - + def get_token_account_balance(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetTokenAccountBalance): + """ Returns the token balance of an SPL Token account (UNSTABLE). Args: - pubkey_to_use: Pubkey of Token account to query - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of Token account to query + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetTokenAccountBalance: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetTokenAccountBalance(pubkey_to_use, commitment_to_use, next_id) return next_id, "getTokenAccountBalance", req @classmethod - def get_token_accounts_by_delegate(cls, pubkey_to_use: Pubkey, opts: TokenAccountOpts, - commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetTokenAccountsByDelegate): - """Returns all SPL Token accounts by approved Delegate (UNSTABLE). - + def get_token_accounts_by_delegate(cls, pubkey_to_use: Pubkey, opts: TokenAccountOpts, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetTokenAccountsByDelegate): + """ Returns all SPL Token accounts by approved Delegate (UNSTABLE). Args: - pubkey_to_use: Pubkey of Token account to query - opts: Token account option specifying at least one of `mint` or `programId`. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of Token account to query + opts (TokenAccountOpts): Token account option specifying at least one of `mint` or `programId`. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetTokenAccountsByDelegate: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() encoding_to_use = opts.encoding @@ -2147,15 +2128,14 @@ class HttpReqGen: return next_id, "getTokenAccountsByDelegate", req @classmethod - def get_token_accounts_by_owner(cls, pubkey_to_use: Pubkey, opts: TokenAccountOpts, - commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetTokenAccountsByOwner): - """Returns all SPL Token accounts by token owner (UNSTABLE). - + def get_token_accounts_by_owner(cls, pubkey_to_use: Pubkey, opts: TokenAccountOpts, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetTokenAccountsByOwner): + """ Returns all SPL Token accounts by token owner (UNSTABLE). Args: - pubkey_to_use: Pubkey of Token account to query - opts: Token account option specifying at least one of `mint` or `programId`. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of Token account to query + opts (TokenAccountOpts): Token account option specifying at least one of `mint` or `programId`. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetTokenAccountsByOwner: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() encoding_to_use = opts.encoding @@ -2171,18 +2151,17 @@ class HttpReqGen: else: raise ValueError("Please provide one of mint or program_id") rpc_account_info_config = RpcAccountInfoConfig(encoding = encoding_to_use, data_slice = data_slice_to_use, commitment = commitment_to_use) - req = GetTokenAccountsByOwner(pubkey_to_use, filter_to_use, rpc_account_info_config, next_id) return next_id, "getTokenAccountsByOwner", req @classmethod - def get_token_largest_accounts(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, GetTokenLargestAccounts): - """Returns the 20 largest accounts of a particular SPL Token type. - + def get_token_largest_accounts(cls, pubkey_to_use: Pubkey, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetTokenLargestAccounts): + """ Returns the 20 largest accounts of a particular SPL Token type. Args: - pubkey_to_use: Pubkey of Token account to query - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of Token account to query + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetTokenLargestAccounts: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetTokenLargestAccounts(pubkey_to_use, commitment_to_use, next_id) @@ -2193,8 +2172,10 @@ class HttpReqGen: """Returns the total supply of an SPL Token type. Args: - pubkey_to_use: Pubkey of Token account to query - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of Token account to query + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetTokenSupply: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetTokenSupply(pubkey_to_use, commitment_to_use, next_id) @@ -2202,27 +2183,27 @@ class HttpReqGen: @classmethod def get_transaction(cls, signature_to_use: Signature, encoding_to_use: Optional[UiTransactionEncoding] = UiTransactionEncoding.Json, commitment_to_use: Optional[CommitmentLevel] = None, max_supported_transaction_version: Optional[int] = None) -> (int, str, GetTransaction): - """Returns transaction details for a confirmed transaction - + """ Returns transaction details for a confirmed transaction Args: - signature_to_use: Transaction signature, as base-58 encoded string - encoding_to_use: Encoding for the returned Transaction - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - max_supported_transaction_version: max supported transaction version + signature_to_use (Signature): Transaction signature, as base-58 encoded string + encoding_to_use (Optional[UiTransactionEncoding]): Encoding for the returned Transaction + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + max_supported_transaction_version (Optional[int]): max supported transaction version + Returns: + int, str, GetTransaction: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_transaction_config = None if encoding_to_use is None and commitment_to_use is None and max_supported_transaction_version is None else RpcTransactionConfig( - encoding = encoding_to_use, commitment = commitment_to_use, - max_supported_transaction_version = max_supported_transaction_version) + rpc_transaction_config = None if encoding_to_use is None and commitment_to_use is None and max_supported_transaction_version is None else RpcTransactionConfig(encoding = encoding_to_use, commitment = commitment_to_use, max_supported_transaction_version = max_supported_transaction_version) req = GetTransaction(signature_to_use, rpc_transaction_config, next_id) return next_id, "getTransaction", req @classmethod def get_transaction_count(cls, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, GetTransactionCount): - """Returns the current Transaction count from the ledger - + """ Returns the current Transaction count from the ledger Args: - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, GetTransactionCount: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment = commitment_to_use) @@ -2231,7 +2212,9 @@ class HttpReqGen: @classmethod def get_version(cls) -> (int, str, GetVersion): - """Returns the current Solana version running on the node + """ Returns the current Solana version running on the node + Returns: + int, str, GetVersion: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = GetVersion(next_id) @@ -2239,29 +2222,28 @@ class HttpReqGen: @classmethod def get_vote_accounts(cls, pubkey_to_use: Optional[Pubkey] = None, commitment_to_use: Optional[CommitmentLevel] = None, keep_unstaked_delinquents: Optional[bool] = None, delinquent_slot_distance: Optional[int] = None) -> (int, str, GetVoteAccounts): - """Returns the account info and associated stake for all the voting accounts in the current bank. - + """ Returns the account info and associated stake for all the voting accounts in the current bank. Args: - pubkey_to_use: Only return results for this validator vote address (base-58 encoded) - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". - keep_unstaked_delinquents: Do not filter out delinquent validators with no stake - delinquent_slot_distance: Specify the number of slots behind the tip that a validator must fall to be considered delinquent. NOTE: For the sake of consistency between ecosystem products, it is not recommended that this argument be specified. + pubkey_to_use (Optional[Pubkey]): Only return results for this validator vote address (base-58 encoded) + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + keep_unstaked_delinquents (Optional[bool]): Do not filter out delinquent validators with no stake + delinquent_slot_distance (Optional[int]): Specify the number of slots behind the tip that a validator must fall to be considered delinquent. NOTE: For the sake of consistency between ecosystem products, it is not recommended that this argument be specified. + Returns: + int, str, GetVoteAccounts: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() - rpc_get_vote_accounts_config = None if pubkey_to_use is None and commitment_to_use is None and keep_unstaked_delinquents is None and delinquent_slot_distance is None else RpcGetVoteAccountsConfig( - vote_pubkey = pubkey_to_use, commitment = commitment_to_use, keep_unstaked_delinquents = keep_unstaked_delinquents, - delinquent_slot_distance = delinquent_slot_distance) + rpc_get_vote_accounts_config = None if pubkey_to_use is None and commitment_to_use is None and keep_unstaked_delinquents is None and delinquent_slot_distance is None else RpcGetVoteAccountsConfig(vote_pubkey = pubkey_to_use, commitment = commitment_to_use, keep_unstaked_delinquents = keep_unstaked_delinquents, delinquent_slot_distance = delinquent_slot_distance) req = GetVoteAccounts(rpc_get_vote_accounts_config, next_id) return next_id, "getVoteAccounts", req @classmethod - def is_blockhash_valid(cls, blockhash_to_use: Hash, commitment_to_use: Optional[CommitmentLevel] = None) -> ( - int, str, IsBlockhashValid): - """Returns whether a blockhash is still valid or not - + def is_blockhash_valid(cls, blockhash_to_use: Hash, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, IsBlockhashValid): + """ Returns whether a blockhash is still valid or not Args: - blockhash_to_use: the blockhash of the block to evaluate, as base-58 encoded string - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + blockhash_to_use (Hash): the blockhash of the block to evaluate, as base-58 encoded string + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, IsBlockhashValid: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_context_config = None if commitment_to_use is None else RpcContextConfig(commitment_to_use) @@ -2270,7 +2252,9 @@ class HttpReqGen: @classmethod def minimum_ledger_slot(cls) -> (int, str, MinimumLedgerSlot): - """Returns the lowest slot that the node has information about in its ledger. + """ Returns the lowest slot that the node has information about in its ledger. + Returns: + int, str, MinimumLedgerSlot: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() req = MinimumLedgerSlot(next_id) @@ -2278,12 +2262,13 @@ class HttpReqGen: @classmethod def request_airdrop(cls, pubkey_to_use: Pubkey, lamports_to_use: int, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, RequestAirdrop): - """Requests an airdrop of lamports to a Pubkey. - + """ Requests an airdrop of lamports to a Pubkey. Args: - pubkey_to_use: Pubkey of account to receive lamports, as base-58 encoded string or public key object. - lamports_to_use: Amount of lamports. - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + pubkey_to_use (Pubkey): Pubkey of account to receive lamports, as base-58 encoded string or public key object. + lamports_to_use (int): Amount of lamports. + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, RequestAirdrop: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_request_airdrop_config = None if commitment_to_use is None else RpcRequestAirdropConfig(commitment_to_use) @@ -2291,33 +2276,31 @@ class HttpReqGen: return next_id, "requestAirdrop", req @classmethod - def send_transaction(cls, txn: Union[VersionedTransaction, Transaction], opts: Optional[TxOpts] = None) -> ( - int, str, SendRawTransaction): - """Send a transaction. - + def send_transaction(cls, txn: Union[VersionedTransaction, Transaction], opts: Optional[TxOpts] = None) -> (int, str, SendRawTransaction): + """ Send a transaction. Args: - txn: transaction object. - opts: (optional) Transaction options. + txn (Union[VersionedTransaction, Transaction]): transaction object. + opts (Optional[TxOpts]): Transaction options. + Returns: + int, str, SendRawTransaction: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() btxn = bytes(txn) opts_to_use = TxOpts(prefLightCommitment = CommitmentLevel.Finalized) if opts is None else opts pref_light_commitment_to_use = CommitmentLevel.Finalized if opts.prefLightCommitment is None else opts.prefLightCommitment - rpc_send_transaction_config = RpcSendTransactionConfig(skip_preflight = opts_to_use.skipPreflight, - preflight_commitment = pref_light_commitment_to_use, - encoding = opts_to_use.prefEncoding, max_retries = opts_to_use.maxRetries) + rpc_send_transaction_config = RpcSendTransactionConfig(skip_preflight = opts_to_use.skipPreflight, preflight_commitment = pref_light_commitment_to_use, encoding = opts_to_use.prefEncoding, max_retries = opts_to_use.maxRetries) req = SendRawTransaction(btxn, rpc_send_transaction_config, next_id) return next_id, "sendTransaction", req @classmethod def simulate_transaction(cls, txn: Union[VersionedTransaction, Transaction], verify_signature: bool, commitment_to_use: Optional[CommitmentLevel] = None) -> (int, str, Union[SimulateVersionedTransaction, SimulateLegacyTransaction]): - """Send a transaction. - + """ Send a transaction. Args: - txn: A Transaction object, a transaction in wire format, or a transaction as base-64 encoded string - The transaction must have a valid blockhash, but is not required to be signed. - verify_signature: If true the transaction signatures will be verified (default: false). - commitment_to_use: Bank state to query. It can be either "finalized", "confirmed" or "processed". + txn (Union[VersionedTransaction, Transaction]): A Transaction object, a transaction in wire format, or a transaction as base-64 encoded string. The transaction must have a valid blockhash, but is not required to be signed. + verify_signature (bool): If true the transaction signatures will be verified (default: false). + commitment_to_use (Optional[CommitmentLevel]): Bank state to query. It can be either "finalized", "confirmed" or "processed". + Returns: + int, str, Union[SimulateVersionedTransaction, SimulateLegacyTransaction]: next_id, RPC method name, solders request object """ next_id = SharedCounter.get_next_id() rpc_simulate_transaction_config = RpcSimulateTransactionConfig(sig_verify = verify_signature, commitment = commitment_to_use) @@ -2329,30 +2312,14 @@ class HttpReqGen: class Wallet: - """Class Wallet can raise ValueError """ - WrongWalletName = 1 - """'walletName' wrong type (must not be empty string)""" - WrongWalletId = 2 - """'walletId' wrong type (must not be empty string)""" - WrongWalletPass = 3 - """'walletPass' wrong type (must not be empty string)""" - WrongWalletType = 4 - """'walletType' must be equal string 'solana'""" - JSONDecodeError = 5 - """Cannot decode Json""" - - def __init__(self, - wallet_name: str, - wallet_pub_key: str, - wallet_priv_key: str, - dt_crea: datetime = None, - dt_update: datetime = None, - main_sol_balance: float = 0, - main_tokens: list = None, - dev_sol_balance: float = 0, - dev_tokens: list = None, - wallet_type: str = 'solana'): - # Vérification des paramètres obligatoires + """ Class Wallet can raise ValueError """ + WrongWalletName = 1 # 'walletName' wrong type (must not be empty string) + WrongWalletId = 2 # 'walletId' wrong type (must not be empty string) + WrongWalletPass = 3 # 'walletPass' wrong type (must not be empty string) + WrongWalletType = 4 # 'walletType' must be equal string 'solana' + JSONDecodeError = 5 # Cannot decode Json + def __init__(self, wallet_name: str, wallet_pub_key: str, wallet_priv_key: str, dt_crea: datetime = None, dt_update: datetime = None, main_sol_balance: float = 0, main_tokens: list = None, dev_sol_balance: float = 0, dev_tokens: list = None, wallet_type: str = 'solana'): + # Checking mandatory parameters if not isinstance(wallet_name, str) or not wallet_name.strip(): raise ValueError(Wallet.WrongWalletName) if not isinstance(wallet_pub_key, str) or not wallet_pub_key.strip(): @@ -2361,39 +2328,33 @@ class Wallet: raise ValueError(Wallet.WrongWalletPass) if wallet_type != "solana": raise ValueError(Wallet.WrongWalletType) - # Attribuer les valeurs obligatoires + # Assign required values self.walletType = wallet_type self.walletName = wallet_name self.walletPubKey = wallet_pub_key self.walletPrivKey = wallet_priv_key - # Gérer les valeurs par défaut des dates + # Manage default date values self.dtCrea = dt_crea if isinstance(dt_crea, datetime) else datetime.now(timezone.utc) self.dtUpdate = dt_update if isinstance(dt_update, datetime) else datetime.now(timezone.utc) - # Gérer les balances et les mettre à zéro si elles sont négatives + # Manage the balances and set them to zero if they are negative self.mainSolBalance = max(main_sol_balance, 0) self.devSolBalance = max(dev_sol_balance, 0) - # Initialiser les listes de tokens si elles ne sont pas fournies + # Initialize token lists if not provided self.mainTokens = main_tokens if main_tokens is not None else [] self.devTokens = dev_tokens if dev_tokens is not None else [] def to_json(self) -> str: - """Convertir l'instance en JSON""" - return json.dumps({ - "walletType": self.walletType, - "walletName": self.walletName, - "walletPubKey": self.walletPubKey, - "walletPrivKey": self.walletPrivKey, - "dtCrea": self.dtCrea.isoformat(), - "dtUpdate": self.dtUpdate.isoformat(), - "mainSolBalance": self.mainSolBalance, - "mainTokens": self.mainTokens, - "devSolBalance": self.devSolBalance, - "devTokens": self.devTokens - }) + """ Convert instance to JSON """ + return json.dumps({"walletType": self.walletType, "walletName": self.walletName, "walletPubKey": self.walletPubKey, "walletPrivKey": self.walletPrivKey, "dtCrea": self.dtCrea.isoformat(), "dtUpdate": self.dtUpdate.isoformat(), "mainSolBalance": self.mainSolBalance, "mainTokens": self.mainTokens, "devSolBalance": self.devSolBalance, "devTokens": self.devTokens}) @staticmethod def parse_datetime(dt_str): - """Parse une chaîne de date en datetime, retourne l'heure actuelle en UTC en cas d'erreur.""" + """ Parse a date string into a datetime, returning the current time in UTC on error. + Args: + dt_str: possible datetime string + Returns: + datetime: + """ if isinstance(dt_str, str): try: return datetime.fromisoformat(dt_str) @@ -2403,10 +2364,10 @@ class Wallet: @staticmethod def from_json(json_string: str): - """Créer une instance Wallet à partir d'une chaîne JSON""" + """ Create a Wallet instance from a JSON string """ try: data = json.loads(json_string) - # Vérification des paramètres obligatoires + # Checking mandatory parameters if 'walletName' not in data or not isinstance(data["walletName"], str) or not data["walletName"].strip(): raise ValueError(Wallet.WrongWalletName) if 'walletPubKey' not in data or not isinstance(data["walletPubKey"], str) or not data["walletPubKey"].strip(): @@ -2419,26 +2380,23 @@ class Wallet: wallet_pub_key = data["walletPubKey"] wallet_priv_key = data["walletPrivKey"] wallet_type = data["walletType"] - # Récupérer et valider les dates + # Retrieve and validate dates dt_crea = Wallet.parse_datetime(data.get("dtCrea")) dt_update = Wallet.parse_datetime(data.get("dtUpdate")) - # Récupérer et valider les balances, les réinitialiser à zéro si elles sont négatives + # Retrieve and validate the scales, reset them to zero if they are negative main_sol_balance = max(data.get("mainSolBalance", 0), 0) dev_sol_balance = max(data.get("devSolBalance", 0), 0) - # Récupérer les listes de tokens, initialiser à liste vide si absentes + # Retrieve token lists, initialize to empty list if absent main_tokens = data.get("mainTokens", []) dev_tokens = data.get("devTokens", []) - # Créer et retourner l'instance Wallet + # Create and return the Wallet instance return Wallet(wallet_name, wallet_pub_key, wallet_priv_key, dt_crea, dt_update, main_sol_balance, main_tokens, dev_sol_balance, dev_tokens, wallet_type) except json.JSONDecodeError: raise ValueError(Wallet.JSONDecodeError) class SimpleMsgBox(QMessageBox): - """ - Simple Class inherit from QMessageBox. - """ - + """ Simple Class inherit from QMessageBox. """ def __init__(self, window_title: str, window_text: str, ico: QMessageBox.Icon, parent = None): super().__init__(parent) self.setLayoutDirection(get_qt_dir()) @@ -2449,18 +2407,13 @@ class SimpleMsgBox(QMessageBox): self.setIcon(ico) def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) center_on_screen(self) class NewWalletNameAndPassDialog(QDialog): - """ - Dialog to ask for the name and the encryptPass for the new Wallet - """ - + """ Dialog to ask for the name and the encryptPass for the new Wallet """ def __init__(self, parent = None): super().__init__(parent) self.setLayoutDirection(get_qt_dir()) @@ -2472,41 +2425,28 @@ class NewWalletNameAndPassDialog(QDialog): self._walletEncyptPass_Qle = QLineEdit() self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password) main_qflo.addRow(self.tr("Wallet Name:"), self._walletName_Qle) - # main_qflo.addRow(self.tr("Wallet EncryptPass:"), self._walletEncyptPass_Qle) - # Password toggle button self._toggle_qtbtn = QToolButton() - self._toggle_qtbtn.setIcon(QIcon(":icoEyeShow")) # Replace with the actual eye icon path + self._toggle_qtbtn.setIcon(QIcon(":icoEyeShow")) self._toggle_qtbtn.setCheckable(True) self._toggle_qtbtn.setToolTip(self.tr("Show/Hide Password")) self._toggle_qtbtn.clicked.connect(self._toggle_password_visibility) - - # Add Wallet EncryptPass field and toggle button to layout - pass_layout = QHBoxLayout() pass_layout = QHBoxLayout() pass_layout.addWidget(self._walletEncyptPass_Qle) pass_layout.addWidget(self._toggle_qtbtn) main_qflo.addRow(self.tr("Wallet EncryptPass:"), pass_layout) - validate_qpbtn = QPushButton(self.tr("Validate")) cancel_qpbtn = QPushButton(self.tr("Cancel")) - button_qdbbox = QDialogButtonBox() button_qdbbox.addButton(validate_qpbtn, QDialogButtonBox.ButtonRole.AcceptRole) button_qdbbox.addButton(cancel_qpbtn, QDialogButtonBox.ButtonRole.RejectRole) - - # Connecter les signaux des boutons validate_qpbtn.clicked.connect(self._validate_and_accept) cancel_qpbtn.clicked.connect(self.reject) - main_qflo.addWidget(button_qdbbox) - self.walletName = None self.walletEncyptPass = None def _toggle_password_visibility(self): - """ - Toggle the password visibility in the QLineEdit - """ + """ Toggle the password visibility in the QLineEdit """ is_visible = self._walletEncyptPass_Qle.echoMode() == QLineEdit.EchoMode.Normal if is_visible: self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password) @@ -2516,16 +2456,14 @@ class NewWalletNameAndPassDialog(QDialog): self._toggle_qtbtn.setIcon(QIcon(":icoEyeHide")) def _validate_and_accept(self): - """ - validation of the QLineEdit contents - """ - # Validation du champ 1 + """ validation of the QLineEdit contents """ + # Validation of field 1 if len(self._walletName_Qle.text().strip()) < 2: msg_qmbox = SimpleMsgBox(self.tr("Error"), self.tr("The Wallet Name must have a minimum of 2 characters."), QMessageBox.Icon.Warning, self) msg_qmbox.addButton(self.tr("Ok"), QMessageBox.ButtonRole.ActionRole) msg_qmbox.exec() return - # Validation du champ 2 (mot de passe) + # Validation of field 2 (password) if not re.match(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$", self._walletEncyptPass_Qle.text()): msg_qmbox = SimpleMsgBox(self.tr("Error"), self.tr("The Wallet EncryptPass must have a minimum of 1 lower case character, 1 upper case character, 1 numeric character, 1 special character (@$!%*?&), and must have a minimum of 8 characters length."), QMessageBox.Icon.Warning, self) msg_qmbox.addButton(self.tr("Ok"), QMessageBox.ButtonRole.ActionRole) @@ -2533,13 +2471,11 @@ class NewWalletNameAndPassDialog(QDialog): return self.walletName = self._walletName_Qle.text().strip() self.walletEncyptPass = self._walletEncyptPass_Qle.text() - # Si les deux champs sont valides, on accepte la saisie + # If both fields are valid, the entry is accepted. self.accept() def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) size = self.size() self.setFixedSize(size) @@ -2547,17 +2483,14 @@ class NewWalletNameAndPassDialog(QDialog): class NewWalletFileDialog(QFileDialog): - """ - File Fialog to save a new wallet file - """ - + """ File Fialog to save a new wallet file """ def __init__(self, parent = None): super().__init__(parent) - self.setOption(QFileDialog.Option.DontUseNativeDialog, True) # Empêche d'écraser un fichier existant + self.setOption(QFileDialog.Option.DontUseNativeDialog, True) self.setWindowTitle(self.tr("Save Content to New Wallet File")) self.setWindowIcon(QIcon(":imgIcon")) - self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) # Mode "Enregistrer sous" - self.setFileMode(QFileDialog.FileMode.AnyFile) # Permet de saisir un nouveau fichier + self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + self.setFileMode(QFileDialog.FileMode.AnyFile) self.setOption(QFileDialog.Option.DontConfirmOverwrite, True) self.setLabelText(QFileDialog.DialogLabel.LookIn, self.tr("Wallets location:")) self.setLabelText(QFileDialog.DialogLabel.FileName, self.tr("File name:")) @@ -2569,9 +2502,7 @@ class NewWalletFileDialog(QFileDialog): self.setNameFilter(self.tr("{desc} (*.{ext})").format(desc = WALLET_FILE_DESC, ext = WALLET_FILE_EXT)) def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) size = self.size() self.setMinimumSize(size) @@ -2579,10 +2510,7 @@ class NewWalletFileDialog(QFileDialog): class LoadWalletPassDialog(QDialog): - """ - Dialog to ask for EncryptPass to open a wallet file - """ - + """ Dialog to ask for EncryptPass to open a wallet file """ def __init__(self, file_name: str, parent = None): super().__init__(parent) self.setLayoutDirection(get_qt_dir()) @@ -2592,35 +2520,27 @@ class LoadWalletPassDialog(QDialog): self.setLayout(main_qflo) self._walletEncyptPass_Qle = QLineEdit() self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password) - # main_qflo.addRow(self.tr("Wallet EncryptPass:"), self._walletEncyptPass_Qle) - # Password toggle button self._toggle_qtbtn = QToolButton() - self._toggle_qtbtn.setIcon(QIcon(":icoEyeShow")) # Replace with the actual eye icon path + self._toggle_qtbtn.setIcon(QIcon(":icoEyeShow")) self._toggle_qtbtn.setCheckable(True) self._toggle_qtbtn.setToolTip(self.tr("Show/Hide Password")) self._toggle_qtbtn.clicked.connect(self._toggle_password_visibility) - - # Add Wallet EncryptPass field and toggle button to layout pass_layout = QHBoxLayout() pass_layout.addWidget(self._walletEncyptPass_Qle) pass_layout.addWidget(self._toggle_qtbtn) main_qflo.addRow(self.tr("Wallet EncryptPass:"), pass_layout) - validate_qpbtn = QPushButton(self.tr("Validate")) cancel_qpbtn = QPushButton(self.tr("Cancel")) button_qdbbox = QDialogButtonBox() button_qdbbox.addButton(validate_qpbtn, QDialogButtonBox.ButtonRole.AcceptRole) button_qdbbox.addButton(cancel_qpbtn, QDialogButtonBox.ButtonRole.RejectRole) - # Connecter les signaux des boutons validate_qpbtn.clicked.connect(self._validate_and_accept) cancel_qpbtn.clicked.connect(self.reject) main_qflo.addWidget(button_qdbbox) self.walletEncyptPass = None def _toggle_password_visibility(self): - """ - Toggle the password visibility in the QLineEdit - """ + """ Toggle the password visibility in the QLineEdit """ is_visible = self._walletEncyptPass_Qle.echoMode() == QLineEdit.EchoMode.Normal if is_visible: self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password) @@ -2630,23 +2550,19 @@ class LoadWalletPassDialog(QDialog): self._toggle_qtbtn.setIcon(QIcon(":icoEyeHide")) def _validate_and_accept(self): - """ - validation of the QLineEdit content - """ - # Validation du champ 1 + """ validation of the QLineEdit content """ + # Validation of field 1 if len(self._walletEncyptPass_Qle.text().strip()) < 1: msg_qmbox = SimpleMsgBox(self.tr("Error"), self.tr("You must enter a valid Password."), QMessageBox.Icon.Warning, self) msg_qmbox.addButton(self.tr("Ok"), QMessageBox.ButtonRole.ActionRole) msg_qmbox.exec() return self.walletEncyptPass = self._walletEncyptPass_Qle.text() - # Si les deux champs sont valides, on accepte la saisie + # If field is valid, the entry is accepted self.accept() def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) size = self.size() self.setFixedSize(size) @@ -2654,17 +2570,14 @@ class LoadWalletPassDialog(QDialog): class LoadWalletFileDialog(QFileDialog): - """ - QFileDialog used to load wallets files - """ - + """ QFileDialog used to load wallets files """ def __init__(self, parent = None): super().__init__(parent) - self.setOption(QFileDialog.Option.DontUseNativeDialog, True) # Empêche d'écraser un fichier existant + self.setOption(QFileDialog.Option.DontUseNativeDialog, True) self.setWindowTitle(self.tr("Load Wallet File")) self.setWindowIcon(QIcon(":imgIcon")) - self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) # Mode "Ouvrir sous" - self.setFileMode(QFileDialog.FileMode.ExistingFile) # Permet de saisir un fichier existant + self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) + self.setFileMode(QFileDialog.FileMode.ExistingFile) self.setDirectory(WALLETS_FOLDER) self.setDefaultSuffix(WALLET_FILE_EXT) self.setLabelText(QFileDialog.DialogLabel.LookIn, self.tr("Wallets location:")) @@ -2676,9 +2589,7 @@ class LoadWalletFileDialog(QFileDialog): # self.setMinimumSize(dialog.width(), dialog.height()) def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) size = self.size() self.setMinimumSize(size) @@ -2686,14 +2597,13 @@ class LoadWalletFileDialog(QFileDialog): class SplashScreen(QSplashScreen): - """SplashScreen class.""" + """ SplashScreen class. """ def __init__(self, fade_in_duration = 1500, display_duration = 2000, fade_out_duration = 1500): super().__init__(QPixmap(":imgSplash")) - # logger.info(f"SplashScreen {Consts.APP_FULL_NAME} ({Consts.APP_VERSION})") - self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # Supprimer les boutons de fermeture + self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # Remove window border and buttons self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - # Appliquer l'effet d'opacité pour gérer les transitions + # Apply opacity effect to manage transitions self._opacityEffect = QGraphicsOpacityEffect() self.setGraphicsEffect(self._opacityEffect) # Animation Fade-in @@ -2708,29 +2618,24 @@ class SplashScreen(QSplashScreen): self.fadeOut.setStartValue(1) self.fadeOut.setEndValue(0) self.fadeOut.setEasingCurve(QEasingCurve.Type.InOutQuad) - # Planification de l'animation fade-out après la durée + # Scheduling the fade-out animation after duration QTimer.singleShot(display_duration, self.start_fade_out) - # Lancer l'animation fade-in + # Start fade-in animation self._fadeIn.start() def start_fade_out(self): - """Lancer l'animation fade-out et fermer le splash screen à la fin.""" + """ Start the fade-out animation and close the splash screen at the end. """ self.fadeOut.start() self.fadeOut.finished.connect(self.close) def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) center_on_screen(self) class AboutWindow(QWidget): - """ - About Window Class used to show Modal image about the app - """ - + """ About Window Class used to show Modal image about the app """ def __init__(self, bg_txt: str, parent = None): super().__init__(parent) self.setWindowTitle(self.tr("About {app}").format(app = APP_NAME_VERSION)) @@ -2739,14 +2644,14 @@ class AboutWindow(QWidget): self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint) self._bg_qpix = QPixmap(":imgAbout") - # Texte centré placé avant le bouton de fermeture pour ne pas empêcher le click de celui-là + # Centered text placed before the close button so as not to prevent it from being clicked self._about_Qlbl = QLabel() self._about_Qlbl.setText(bg_txt) self._about_Qlbl.setAlignment(Qt.AlignmentFlag.AlignCenter) # Centrer le texte self._about_Qlbl.setStyleSheet("font-size: 20px; color: white;") # Style du texte self._about_Qlbl.setWordWrap(True) # Activer le retour à la ligne self._about_Qlbl.setGeometry(0, 0, self.width(), self.height()) # Occuper toute la fenêtre - # Bouton de fermeture de la fenetre + # Window close button self._exit_Qpbtn = QPushButton(self) self._exit_Qpbtn.setIcon(QIcon(":icoQuit")) self._exit_Qpbtn.setFixedSize(24, 24) @@ -2755,23 +2660,21 @@ class AboutWindow(QWidget): self._exit_Qpbtn.move(self.width() - self._exit_Qpbtn.width() - 20, 20) def paintEvent(self, event): - """ - fired on paint event - """ + """ fired on paint event """ painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) - # Redimensionner l'image pour qu'elle corresponde à la taille de la fenêtre + # Resize the image to fit the window size scaled_image = self._bg_qpix.scaled(self.size(), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) painter.drawPixmap(0, 0, scaled_image) painter.setOpacity(0.7) - # Ajouter un effet de dégradé + # Add a gradient effect gradient = QLinearGradient(0, 0, 0, self.height()) gradient.setColorAt(0, QColor(0, 0, 0, 0)) # Transparent en haut gradient.setColorAt(1, QColor(0, 0, 0, 128)) # Sombre en bas painter.fillRect(self.rect(), gradient) def keyPressEvent(self, event: QKeyEvent): - """Gérer les raccourcis clavier pour fermer la fenêtre.""" + """ Manage keyboard shortcuts to close the window. """ if event.key() in {Qt.Key.Key_Escape, Qt.Key.Key_Space, Qt.Key.Key_Enter, Qt.Key.Key_Return}: print("Fenêtre fermée avec une touche simple.") self.close() @@ -2779,92 +2682,369 @@ class AboutWindow(QWidget): print("Fenêtre fermée avec Ctrl+F4.") self.close() else: - # Si une autre touche est pressée, laisser le comportement par défaut + # If another key is pressed, leave the default behavior super().keyPressEvent(event) def event(self, event): - """Surveiller les événements de la fenêtre.""" + """ Monitor window events. """ if event.type() == QEvent.Type.WindowDeactivate: self.close() return super().event(event) -class HelpWindow(QWidget): - """ - Class used to display help from pdf file (stored in qrc) - """ +class SolanaAdressCheckerWindow(QWidget): + """ Class used to check info of a solana address """ + def __init__(self, pool: ConnectionPool, parent = None): + super().__init__(parent) + self._parent = parent + self._pool = pool + self.setWindowIcon(QIcon(":imgIcon")) + self.setMinimumSize(1024, 600) + self.setFocus() + self.setWindowFlags(Qt.WindowType.Dialog) + layout = QVBoxLayout(self) + self.setLayout(layout) + self._solAddress_Qlbl = QLabel() + self._solAddress_Qled = QLineEdit() + h_layout1 = QHBoxLayout() + h_layout1.addWidget(self._solAddress_Qlbl) + h_layout1.addWidget(self._solAddress_Qled) + layout.addLayout(h_layout1) + self._get_account_info_Qpbtn = QPushButton() + self._get_account_info_Qpbtn.setEnabled(False) + self._get_account_info_Qpbtn.clicked.connect(self._get_account_info) + self._get_balance_Qpbtn_Qpbtn = QPushButton() + self._get_balance_Qpbtn_Qpbtn.setEnabled(False) + self._get_balance_Qpbtn_Qpbtn.clicked.connect(self._get_balance) + self._get_program_accounts_Qpbtn = QPushButton() + self._get_program_accounts_Qpbtn.setEnabled(False) + self._get_program_accounts_Qpbtn.clicked.connect(self._get_program_accounts) + self._get_signatures_for_address_Qpbtn = QPushButton() + self._get_signatures_for_address_Qpbtn.setEnabled(False) + self._get_signatures_for_address_Qpbtn.clicked.connect(self._get_signatures_for_address) + self._get_token_account_balance_Qpbtn = QPushButton() + self._get_token_account_balance_Qpbtn.setEnabled(False) + self._get_token_account_balance_Qpbtn.clicked.connect(self._get_token_account_balance) + self._get_token_accounts_by_delegate_Qpbtn = QPushButton() + self._get_token_accounts_by_delegate_Qpbtn.setEnabled(False) + self._get_token_accounts_by_delegate_Qpbtn.clicked.connect(self._get_token_accounts_by_delegate) + self._get_token_accounts_by_owner_Qpbtn = QPushButton() + self._get_token_accounts_by_owner_Qpbtn.setEnabled(False) + self._get_token_accounts_by_owner_Qpbtn.clicked.connect(self._get_token_accounts_by_owner) + self._get_token_supply_Qpbtn = QPushButton() + self._get_token_supply_Qpbtn.setEnabled(False) + self._get_token_supply_Qpbtn.clicked.connect(self._get_token_supply) + h_layout2 = QHBoxLayout() + h_layout2.addWidget(self._get_account_info_Qpbtn) + h_layout2.addWidget(self._get_balance_Qpbtn_Qpbtn) + h_layout2.addWidget(self._get_program_accounts_Qpbtn) + h_layout2.addWidget(self._get_signatures_for_address_Qpbtn) + h_layout2.addWidget(self._get_token_account_balance_Qpbtn) + h_layout2.addWidget(self._get_token_accounts_by_delegate_Qpbtn) + h_layout2.addWidget(self._get_token_accounts_by_owner_Qpbtn) + h_layout2.addWidget(self._get_token_supply_Qpbtn) + layout.addLayout(h_layout2) + self.result_text_edit = QTextEdit() + self.result_text_edit.setReadOnly(True) + layout.addWidget(self.result_text_edit) + self.update_display() + # Connects text modification to button activation/deactivation + self._solAddress_Qled.textChanged.connect(self._toggle_buttons_state) + + def _get_account_info(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_account_info(pk_adr) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = GetAccountInfoResp.from_json(response.body) + if isinstance(resp, GetAccountInfoResp): + result = f"response : {resp.value}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + def _get_balance(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_balance(pk_adr) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = GetBalanceResp.from_json(response.body) + if isinstance(resp, GetBalanceResp): + result = f"response : {resp.value}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _get_program_accounts(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_balance(pk_adr) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = response.body + if isinstance(resp, GetProgramAccountsResp): + result = f"response : {resp.to_json()}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _get_signatures_for_address(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_balance(pk_adr) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = response.body + if isinstance(resp, GetSignaturesForAddressResp): + result = f"response : {resp.to_json()}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _get_token_account_balance(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_balance(pk_adr) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = response.body + if isinstance(resp, GetTokenAccountBalanceResp): + result = f"response : {resp.to_json()}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _get_token_accounts_by_delegate(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_balance(pk_adr) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = response.body + if isinstance(resp, GetTokenAccountsByDelegateResp): + result = f"response : {resp.to_json()}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _get_token_accounts_by_owner(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + tokaaccopts = TokenAccountOpts(programId=PK_TOKEN_PROGRAM_ID) + req_id, rpc_method, req = HttpReqGen.get_token_accounts_by_owner(pk_adr, tokaaccopts, CommitmentLevel.Finalized) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = response.body + if isinstance(resp, GetTokenAccountsByOwnerResp): + result = f"response : {resp.to_json()}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _get_token_supply(self): + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + req_id, rpc_method, req = HttpReqGen.get_token_supply(pk_adr, CommitmentLevel.Finalized) + response = self._pool.send_http_post(rpc_method, req.to_json()) + if isinstance(response, ResponseOk): + resp = response.body + if isinstance(resp, GetTokenSupplyResp): + result = f"response : {resp.to_json()}\n" + self.result_text_edit.append(result) + else: + result = f"response err: {type(resp)} - {resp}\n" + self.result_text_edit.append(result) + elif isinstance(response, ResponseError): + result = f"response err: {response.code} - {response.body}\n" + self.result_text_edit.append(result) + elif isinstance(response, NetworkReplyError): + result = f"response net err: {response.code.value} - {response.body}\n" + self.result_text_edit.append(result) + else: + result = f"response unknown: {type(response)} - {response}\n" + self.result_text_edit.append(result) + + def _toggle_buttons_state(self): + """ Active ou désactive les boutons en fonction du contenu du QLineEdit """ + self._get_account_info_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_balance_Qpbtn_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_program_accounts_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_signatures_for_address_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_token_account_balance_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_token_accounts_by_delegate_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_token_accounts_by_owner_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + self._get_token_supply_Qpbtn.setEnabled(bool(self._solAddress_Qled.text().strip())) + if self._solAddress_Qled.text().strip() == "": + self._solAddress_Qled.setStyleSheet("background-color: white;") + else: + try: + adr = self._solAddress_Qled.text().strip() + pk_adr = Pubkey.from_string(adr) + if pk_adr.is_on_curve(): + self._solAddress_Qled.setStyleSheet("background-color: lightgreen;") + elif len(adr) != 44 or len(base58.b58decode(adr)) != 32: + self._solAddress_Qled.setStyleSheet("background-color: lighred;") + else: + self._solAddress_Qled.setStyleSheet("background-color: green;") + except ValueError: + self._solAddress_Qled.setStyleSheet("background-color: red;") + + def update_display(self): + """ update the display of the widget """ + global is_rtl + self.setWindowTitle(self.tr("Solana Address Checker")) + self.setLayoutDirection(get_qt_dir()) + self._solAddress_Qlbl.setText(self.tr("Address:")) + self._solAddress_Qlbl.setAlignment(get_qt_align_vchr(is_rtl)) + self._get_account_info_Qpbtn.setText(self.tr("Account Info")) + self._get_balance_Qpbtn_Qpbtn.setText(self.tr("Balance")) + self._get_program_accounts_Qpbtn.setText(self.tr("Program Accounts")) + self._get_signatures_for_address_Qpbtn.setText(self.tr("Signatures")) + self._get_token_account_balance_Qpbtn.setText(self.tr("Token Account Balance")) + self._get_token_accounts_by_delegate_Qpbtn.setText(self.tr("Token Account by Delegates")) + self._get_token_accounts_by_owner_Qpbtn.setText(self.tr("Token Account by Owner")) + self._get_token_supply_Qpbtn.setText(self.tr("Token Supply")) + + def showEvent(self, event) -> None: + """ fired on show event """ + super().showEvent(event) + center_on_screen(self) + + def closeEvent(self, event): + if isinstance(self._parent, MainWindow): + self._parent.solana_addr_check = False + return super().closeEvent(event) + + +class HelpWindow(QWidget): + """ Class used to display help from pdf file (stored in qrc) """ def __init__(self, pdf_doc: str, parent = None): super().__init__(parent) self.setWindowTitle(self.tr("Help - {app}").format(app = APP_NAME_VERSION)) - # self.setWindowIcon(QIcon(":/res/images/favicon.ico")) self.setWindowIcon(QIcon(":imgIcon")) self.setFixedSize(900, 600) self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.MSWindowsFixedSizeDialogHint) - # Créer le document PDF et le visualiseur PDF + # Create PDF document loader and PDF viewer self._pdfDoc = QPdfDocument() self._pdfView = QPdfView() self._pdfView.setDocument(self._pdfDoc) - # Configurer le widget central pour afficher le PDF + # Configure the central widget to display the PDF self._main_Qvlo = QVBoxLayout() self.setLayout(self._main_Qvlo) self._main_Qvlo.setSpacing(0) self._main_Qvlo.setContentsMargins(QMargins(0, 0, 0, 0)) self._main_Qvlo.addWidget(self._pdfView) - # self._pdfDoc.load(":/res/help.pdf") self._pdfDoc.load(pdf_doc) if self._pdfDoc.status() != QPdfDocument.Status.Ready: self.close() else: - # Afficher la première page du PDF self._pdfView.setPageMode(QPdfView.PageMode.MultiPage) self._pdfView.setZoomMode(QPdfView.ZoomMode.FitToWidth) self._pdfView.setPageSpacing(0) self._pdfView.setDocumentMargins(QMargins(0, 0, 0, 0)) def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ super().showEvent(event) center_on_screen(self) class WalletTokensTableModel(QAbstractTableModel): - """ - Table Model for the tokens in a wallet - """ - + """ Table Model for the tokens in a wallet """ def __init__(self, parent = None): super().__init__(parent) self._headers = [self.tr("Token Id"), self.tr("Buy Price"), self.tr("Buy Time"), self.tr("Action")] self._dataList = [] def rowCount(self, parent = QModelIndex()): - """ - return the number of rows in the table - """ + """ return the number of rows in the table """ return len(self._dataList) def columnCount(self, parent = QModelIndex()): - """ - return the number of columns in the table - """ + """ return the number of columns in the table """ return len(self._headers) def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): - """ - Définit les en-têtes des colonnes. - """ + """ Définit les en-têtes des colonnes. """ if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: return self._headers[section] return None def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: - """ - return the data to display in the QTableView - """ + """ return the data to display in the QTableView """ if not index.isValid(): return None elif role == Qt.ItemDataRole.DisplayRole: @@ -2891,26 +3071,19 @@ class WalletTokensTableModel(QAbstractTableModel): return None def set_headers(self, new_headers: list): - """ - used to update the header column content (on lang change) - """ + """ used to update the header column content (on lang change) """ self._headers = new_headers self.headerDataChanged.emit(Qt.Orientation.Horizontal, 0, 3) self.layoutChanged.emit() def set_data(self, data: list): - """ - change the data of the table model - """ + """ change the data of the table model """ self._dataList = data self.layoutChanged.emit() class WalletWidget(QWidget): - """ - Widget to display the content of a wallet - """ - + """ Widget to display the content of a wallet """ def __init__(self, parent = None): super().__init__(parent) self.setWindowIcon(QIcon(":icoWallet")) @@ -2919,26 +3092,26 @@ class WalletWidget(QWidget): self.filePath = None self._main_Qglo = QGridLayout() self.setLayout(self._main_Qglo) - # Line 1: Wallet Name + # Line 1 self._walletName_Qlbl = QLabel() self.walletName_Qled = QLineEdit() self._saveWallet_Qpbtn = QPushButton() self._init_line1() - # Line 2: Wallet PubKey + # Line 2 self._walletPubKey_Qlbl = QLabel() self.walletPubKey_Qled = QLineEdit() self._copyToClipboard_Qpbtn = QPushButton() self._init_line2() - # Line 3: Balances + # Line 3 self._realBalance_Qlbl = QLabel() self.realBalance_Qled = QLineEdit() self._demoBalance_Qlbl = QLabel() self.demoBalance_Qled = QLineEdit() self._init_line3() - # Line 4: Tokens Label + # Line 4 self._tokens_Qlbl = QLabel() self._init_line4() - # # Line 5: TableView + # # Line 5 self._tokens_qtbvmb = QTableView() self._tokens_wtQtm = WalletTokensTableModel() self._init_tokens_qtbvmb() @@ -2946,9 +3119,7 @@ class WalletWidget(QWidget): self.populate_table() def set_wallet(self, wallet: Wallet, file_pass: str, file_path: str): - """ - changing the wallet - """ + """ changing the wallet """ self.save_wallet() self.fileKey = file_pass self.filePath = file_path @@ -2956,9 +3127,7 @@ class WalletWidget(QWidget): self.update_wallet_display() def update_wallet_display(self): - """ - refresh the display of the wallet data - """ + """ refresh the display of the wallet data """ if self.wallet is not None: self.walletPubKey_Qled.setText(self.wallet.walletPubKey) self.walletName_Qled.setText(self.wallet.walletName) @@ -2971,9 +3140,7 @@ class WalletWidget(QWidget): self.demoBalance_Qled.setText(f"{0:.9f}") def save_wallet(self): - """ - saving the wallet content to the wallet file - """ + """ saving the wallet content to the wallet file """ if self.wallet is not None and self.fileKey is not None and self.filePath is not None: encrypted_data = encrypt_string(self.fileKey, APP_SALT.encode("utf-8"), self.wallet.to_json()) with LockedFile(self.filePath, "w+b") as file: @@ -3004,11 +3171,9 @@ class WalletWidget(QWidget): def _init_line3(self): self.realBalance_Qled.setAlignment(get_qt_align_vchr()) self.realBalance_Qled.setText("10.000000000") - # self.realBalance_Qled.setMaximumWidth(300) self.realBalance_Qled.setReadOnly(True) self.demoBalance_Qled.setAlignment(get_qt_align_vchr()) self.demoBalance_Qled.setText("10.000000000") - # self.demoBalance_Qled.setMaximumWidth(300) self.demoBalance_Qled.setReadOnly(True) self._main_Qglo.addWidget(self._realBalance_Qlbl, 2, 0, 1, 2) self._main_Qglo.addWidget(self.realBalance_Qled, 2, 2, 1, 3) @@ -3021,18 +3186,17 @@ class WalletWidget(QWidget): def _init_tokens_qtbvmb(self): self._tokens_qtbvmb.setModel(self._tokens_wtQtm) - # selection de ligne entiere + # entire line selection self._tokens_qtbvmb.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) - # desactiver tentative d'edition - self._tokens_qtbvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Lecture seule - # alterner les couleurs + # disable editing attempt + self._tokens_qtbvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + # alternate colors self._tokens_qtbvmb.setAlternatingRowColors(True) - # remplir le background + # fill the background self._tokens_qtbvmb.setAutoFillBackground(True) - # activer le trie + # enable sorting self._tokens_qtbvmb.setSortingEnabled(True) - - # Configuration des tailles des colonnes + # configuring column sizes self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Première colonne self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Deuxième colonne self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive) # Troisième colonne @@ -3042,9 +3206,7 @@ class WalletWidget(QWidget): self._main_Qglo.addWidget(self._tokens_qtbvmb, 4, 0, 1, 12) def update_display(self): - """ - update the display of the widget - """ + """ update the display of the widget """ global is_rtl self.setLayoutDirection(get_qt_dir()) self._walletName_Qlbl.setText(self.tr("Wallet Name:")) @@ -3059,8 +3221,7 @@ class WalletWidget(QWidget): self._tokens_wtQtm.set_headers([self.tr("Token Id"), self.tr("Buy Price"), self.tr("Buy Time"), self.tr("Action")]) def populate_table(self): - """ - populate the tableview... + """ populate the tableview... TODO: muste change the model etc... """ data = [] @@ -3069,46 +3230,36 @@ class WalletWidget(QWidget): buy_price = f"{random.uniform(0.000000001, 1.000000000):.9f}" buy_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] action = self.tr("Sell") # Placeholder for button-like behavior - token = { "token_id": token_id, # token_id "buy_price": buy_price, # Niveau du log "buy_time": buy_time, # Message "action": action # Message } - data.append(token) - self._tokens_wtQtm.set_data(data) def copy_wallet_priv_key_to_clipboard(self): - """Copier le texte de QLineEdit dans le presse-papiers.""" + """ Copier le texte de QLineEdit dans le presse-papiers. """ clipboard = QApplication.clipboard() clipboard.setText(self.walletPubKey_Qled.text()) logger.info(self.tr("walletId copied to clipboard")) class LogHandler: - """ - Gestionnaire personnalisé pour diriger les logs de loguru vers un QTableView. - """ - + """ Gestionnaire personnalisé pour diriger les logs de loguru vers un QTableView. """ def __init__(self, emitter: DictSignal): self._logEmitter = emitter def __call__(self, message: str): - """ - Appelé par loguru pour chaque message de log. - """ + """ Appelé par loguru pour chaque message de log. """ parsed_log = self._parse_log_message(message) if parsed_log: self._logEmitter.signal(parsed_log) @staticmethod def _parse_log_message(message: str): - """ - Parse un message de log loguru au format attendu (timestamp, LEVEL, message). - """ + """ Parse un message de log loguru au format attendu (timestamp, LEVEL, message). """ parts = message.split(" -|- ", 2) # On attend le format "{time} -|- {level} -|- {message}" if len(parts) == 3: return { @@ -3120,42 +3271,31 @@ class LogHandler: class LogsTableModel(QAbstractTableModel): - """ - Modèle pour gérer l'affichage des logs dans un QTableView. - """ - + """ Model to manage the display of logs in a QTableView. """ def __init__(self, display_lines: int = 100): super().__init__() - self._dataList = [] # Stocke les logs - self._maxLines = display_lines # Nombre maximum de lignes + self._dataList = [] # logs storage + self._maxLines = display_lines # max lines display self._headers = [self.tr("Timestamp"), self.tr("Level"), self.tr("Message")] self._sortColumn = 0 self._sortOrder = Qt.SortOrder.AscendingOrder self.sort(self._sortColumn, self._sortOrder) def rowCount(self, parent = QModelIndex()): - """ - return the number of rows in the table - """ + """ return the number of rows in the table """ return len(self._dataList) def columnCount(self, parent = QModelIndex()): - """ - return the number of columns in the table - """ + """ return the number of columns in the table """ return len(self._headers) def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole = Qt.ItemDataRole.DisplayRole) -> Any: - """ - Définit les en-têtes des colonnes. - """ + """ Définit les en-têtes des colonnes. """ if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: return self._headers[section] def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: - """ - return the data to display in the QTableView - """ + """ return the data to display in the QTableView """ if not index.isValid(): return None elif role == Qt.ItemDataRole.DisplayRole: @@ -3212,9 +3352,7 @@ class LogsTableModel(QAbstractTableModel): return None def sort(self, column: int, order: Qt.SortOrder = Qt.SortOrder.DescendingOrder): - """ - sort the datas - """ + """ sort the datas """ self._sortColumn = column self._sortOrder = order # Sorting logic @@ -3228,17 +3366,13 @@ class LogsTableModel(QAbstractTableModel): self.layoutChanged.emit() def set_headers(self, new_headers: list): - """ - used to update the header column content (on lang change) - """ + """ used to update the header column content (on lang change) """ self._headers = new_headers self.headerDataChanged.emit(Qt.Orientation.Horizontal, 0, len(self._headers) - 1) self.layoutChanged.emit() def add_data(self, log: dict): - """ - Ajoute un log au modèle tout en respectant la limite maximale des lignes. - """ + """ Adds a log to the model while respecting the maximum row limit. """ self._dataList.append(log) while len(self._dataList) > self._maxLines: self._dataList.pop(0) # Supprime les anciennes lignes @@ -3246,79 +3380,56 @@ class LogsTableModel(QAbstractTableModel): self.layoutChanged.emit() def set_data(self, data: list): - """ - change the data of the table model - """ + """ change the data of the table model """ self._dataList = data self.sort(self._sortColumn, self._sortOrder) self.layoutChanged.emit() class LogViewerWidget(QWidget): - """ - Widget principal contenant un QTableView pour afficher les logs. - """ - + """ Main widget containing a QTableView to display logs. """ def __init__(self, max_display_lines: int = 100, parent = None): super().__init__(parent) - # Layout principal self._main_Qvlo = QVBoxLayout() self.setLayout(self._main_Qvlo) self._main_Qvlo.setContentsMargins(0, 0, 0, 0) - # TableView self._logs_Qtvmb = QTableView(self) self._main_Qvlo.addWidget(self._logs_Qtvmb) - # Model self._logs_lQtm = LogsTableModel(max_display_lines) self._logs_Qtvmb.setModel(self._logs_lQtm) - # selection de ligne entiere self._logs_Qtvmb.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) - # désactiver tentative d'edition self._logs_Qtvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Lecture seule - # alterner les couleurs self._logs_Qtvmb.setAlternatingRowColors(True) - # remplir le background self._logs_Qtvmb.setAutoFillBackground(True) - # activer le trie self._logs_Qtvmb.setSortingEnabled(True) - # Configuration des tailles des colonnes self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Première colonne self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Deuxième colonne self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Troisième colonne self._logs_Qtvmb.setTextElideMode(Qt.TextElideMode.ElideNone) self._logs_Qtvmb.setWordWrap(False) - # Émetteur de signaux pour les logs self._logEmitter = DictSignal() - self._logEmitter.conn(self._add_log) # Connexion du signal - # Ajouter un gestionnaire personnalisé pour loguru + self._logEmitter.conn(self._add_log) log_handler = LogHandler(self._logEmitter) - # logger.remove() # Supprime les sorties par défaut logger.add(log_handler, format = "{time:YYYY-MM-DD HH:mm:ss.SSS} -|- {level} -|- {message}", level = "INFO") - # update display self.update_display() @Slot(dict) def _add_log(self, log: dict): - """ - Ajoute un log au modèle et effectue le défilement automatique. - """ + """ Ajoute un log au modèle et effectue le défilement automatique. """ self._logs_lQtm.add_data(log) def update_display(self): - """ - call on lang chang - """ + """ call on lang change """ self._logs_lQtm.set_headers([self.tr("Timestamp"), self.tr("Level"), self.tr("Message")]) self.setLayoutDirection(get_qt_dir()) class MainWindow(QMainWindow): """ Main Window class """ - - def __init__(self, appli: QApplication, trans: QTranslator, cfg: dict = DEFAULT_YAML_CONFIG): + def __init__(self, appli: QApplication, trans: QTranslator, cfg: dict = None): super().__init__() self.app = appli - self.config = cfg + self.config = cfg if cfg is not None else DEFAULT_YAML_CONFIG self.translator = trans self.currentLang = self.config['defaultLang'] self.setWindowIcon(QIcon(":imgIcon")) @@ -3350,6 +3461,13 @@ class MainWindow(QMainWindow): self.forceCloseApp_Qact.setIcon(QIcon(":icoForceQuit")) # finalize initialisation of File Menu self._init_file_qmnu() + # Tools Menu + self.tools_Qmnu = QMenu() + self.checkAddress_Qact = QAction() + self.checkAddress_Qact.setIcon(QIcon(":icoCheckAddress")) + self.solana_addr_check = False + # finalize initialisation of File Menu + self._init_tools_qmnu() # Langs Menu self.langs_Qmnu = QMenu() self.changeLangAr_Qact = QAction(QIcon(":icoLang"), "العربية") @@ -3417,22 +3535,19 @@ class MainWindow(QMainWindow): self.setStatusBar(self.main_Qstb) self.wsCnxImgs = [] self.httpMsgImgs = [] - # self.main_Qstb.addPermanentWidget(self._get_vertical_separator()) - # self.netReachabilityImg = QLabel() - # self.netReachabilityImg.setPixmap(self.reachabilityUnknown_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - # if self.connPool.check_reachability() == QNetworkInformation.Reachability.Online: - # self.netReachabilityImg.setPixmap(self.reachabilityOnline_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - # elif self.connPool.check_reachability() == QNetworkInformation.Reachability.Site: - # self.netReachabilityImg.setPixmap(self.reachabilitySite_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - # elif self.connPool.check_reachability() == QNetworkInformation.Reachability.Local: - # self.netReachabilityImg.setPixmap(self.reachabilityLocal_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - # elif self.connPool.check_reachability() == QNetworkInformation.Reachability.Disconnected: - # self.netReachabilityImg.setPixmap(self.reachabilityDisconnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - # elif self.connPool.check_reachability() == QNetworkInformation.Reachability.Unknown: - # self.netReachabilityImg.setPixmap(self.reachabilityUnknown_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - # self.main_Qstb.addWidget(self.netReachabilityImg) - # self.connPool.reachabilitySig.conn(self._update_reachability_status) - # self.main_Qstb.addWidget(self._get_vertical_separator()) + self.netReachabilityImg = QLabel() + try: + QNetworkInformation.loadBackendByFeatures(QNetworkInformation.Feature.Reachability) + self._network_info = QNetworkInformation.instance() + if self._network_info: + self._network_info.reachabilityChanged.connect(self.on_reachability_changed) + self.check_connectivity() + except Exception as e: + self.netReachabilityImg.setPixmap(self.reachabilityUnknown_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Unknown")) + logger.error(self.tr(f"Error while loading QNetworkInformation: {str(e)}")) + self.main_Qstb.addWidget(self.netReachabilityImg) + self.main_Qstb.addWidget(self._get_vertical_separator()) self.wsMainCnxImg = QLabel() self.wsMainCnxImg.setPixmap(self.wsDisconnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) self.main_Qstb.addWidget(self.wsMainCnxImg) @@ -3461,24 +3576,50 @@ class MainWindow(QMainWindow): self.statusMsg_Qled.setReadOnly(True) self.statusMsg_Qled.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) # self.statusMsg_Qled.setFrame(False) - self.statusMsg_Qled.setStyleSheet(""" - QLineEdit { - border: none; - background: transparent; - } - """) + self.statusMsg_Qled.setStyleSheet("QLineEdit {border: none; background: transparent;}") self.statusMsg_Qled.setText(self.tr("Ready")) self.main_Qstb.addPermanentWidget(self.statusMsg_Qled) self.connPool.wsMainReplyReceivedSig.conn(self.read_ws_main_sol_msg) self.connPool.wsReplyReceivedSig.conn(self.read_ws_sol_msg) self.connPool.httpReplyReceivedSig.conn(self.read_http_sol_msg) + self.connPool.logSig.conn(self.show_pool_logs) self.update_display() logger.success(self.tr("Init of {appName}").format(appName = APP_ABOUT_NAME)) + def on_reachability_changed(self, reachability): + self.check_connectivity() + + def check_connectivity(self): + if self._network_info: + reachability = self._network_info.reachability() + if reachability == QNetworkInformation.Reachability.Online: + self.netReachabilityImg.setPixmap(self.reachabilityOnline_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Online")) + elif reachability == QNetworkInformation.Reachability.Local: + self.netReachabilityImg.setPixmap(self.reachabilityLocal_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Local")) + elif reachability == QNetworkInformation.Reachability.Site: + self.netReachabilityImg.setPixmap(self.reachabilitySite_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Site")) + elif reachability == QNetworkInformation.Reachability.Disconnected: + self.netReachabilityImg.setPixmap(self.reachabilityDisconnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Disconnected")) + elif reachability == QNetworkInformation.Reachability.Unknown: + self.netReachabilityImg.setPixmap(self.reachabilityUnknown_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Unknown")) + else: + self.netReachabilityImg.setPixmap(self.reachabilityUnknown_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) + self.netReachabilityImg.setToolTip(self.tr("Unknown")) + + def show_pool_logs(self, msg: str): + self.statusMsg_Qled.setText(self.tr(msg)) + if self.isHidden() or self.isMinimized(): + self.tray_Qsti.showMessage(APP_NAME, self.tr(msg), QIcon(":imgFavicon"), 2500) + def read_ws_main_sol_msg(self): - """Lis le dernier message""" + """ Lis le dernier message """ (ts, msg) = self.connPool.read_first_ws_main_msg() parsed = parse_websocket_message(msg) for item in parsed: @@ -3499,7 +3640,7 @@ class MainWindow(QMainWindow): break def read_ws_sol_msg(self): - """Lis le dernier message""" + """ Lis le dernier message """ (ts, msg) = self.connPool.read_first_ws_msg() parsed = parse_websocket_message(msg) for item in parsed: @@ -3520,7 +3661,7 @@ class MainWindow(QMainWindow): break def read_http_sol_msg(self): - """Lis le dernier message""" + """ Lis le dernier message """ (ts, msg) = self.connPool.read_first_http_msg() parsed = parse_websocket_message(msg) for item in parsed: @@ -3540,21 +3681,8 @@ class MainWindow(QMainWindow): logger.info(f"See, https://solscan.io/tx/{signature}") break - def _update_reachability_status(self, value: QNetworkInformation.Reachability): - """Met à jour l'image d'un QLabel Net Reachability en fonction de son état.""" - if value == QNetworkInformation.Reachability.Online: - self.netReachabilityImg.setPixmap(self.reachabilityOnline_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - elif value == QNetworkInformation.Reachability.Site: - self.netReachabilityImg.setPixmap(self.reachabilitySite_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - elif value == QNetworkInformation.Reachability.Local: - self.netReachabilityImg.setPixmap(self.reachabilityLocal_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - elif value == QNetworkInformation.Reachability.Disconnected: - self.netReachabilityImg.setPixmap(self.reachabilityDisconnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - elif value == QNetworkInformation.Reachability.Unknown: - self.netReachabilityImg.setPixmap(self.reachabilityUnknown_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) - def _update_ws_main_connection_status(self, state: QAbstractSocket.SocketState): - """Met à jour l'image d'un QLabel WebSocket en fonction de son état.""" + """ Met à jour l'image d'un QLabel WebSocket en fonction de son état. """ if state == QAbstractSocket.SocketState.ConnectedState: self.wsMainCnxImg.setPixmap(self.wsConnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) self._connected = True @@ -3573,34 +3701,31 @@ class MainWindow(QMainWindow): SharedCounter.reset_id() def _update_ws_connection_status(self, index: int, state: QAbstractSocket.SocketState): - """Met à jour l'image d'un QLabel WebSocket en fonction de son état.""" + """ Met à jour l'image d'un QLabel WebSocket en fonction de son état. """ if state == QAbstractSocket.SocketState.ConnectedState: self.wsCnxImgs[index].setPixmap(self.wsConnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) elif state == QAbstractSocket.SocketState.UnconnectedState: self.wsCnxImgs[index].setPixmap(self.wsDisconnected_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) def update_http_pxm_sent(self, index: int): - """Met à jour l'image d'un QLabel HTTP en état 'Envoyé'.""" + """ Met à jour l'image d'un QLabel HTTP en état 'Envoyé'. """ self.httpMsgImgs[index].setPixmap(self.httpSend_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) QTimer.singleShot(200, lambda: self.httpMsgImgs[index].setPixmap(self.httpWait_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio))) def update_http_pxm_received(self, index: int): - """Met à jour l'image d'un QLabel HTTP en état 'Reçu'.""" + """ Met à jour l'image d'un QLabel HTTP en état 'Reçu'. """ self.httpMsgImgs[index].setPixmap(self.httpReceive_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)) QTimer.singleShot(200, lambda: self.httpMsgImgs[index].setPixmap(self.httpNone_Qpxm.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio))) def _get_vertical_separator(self) -> QFrame: - """Créer un QFrame configuré comme séparateur vertical.""" + """ Create and return vertical separator.""" sep = QFrame(self) sep.setFrameShape(QFrame.Shape.VLine) sep.setFrameShadow(QFrame.Shadow.Sunken) return sep def first_load(self): - """ - called the first time from main to load or create wallet - """ - # logger.debug(type(config)) + """ called the first time from main to load or create wallet """ last_open_is_maximized = self.config.get('lastMaximized', True) if last_open_is_maximized: self.showMaximized() @@ -3617,44 +3742,36 @@ class MainWindow(QMainWindow): else: self._ask_load_wallet_pass(last_file, True) - def _quit_app_func(self): - msg_qmbox = SimpleMsgBox(self.tr("Do you really want to Quit?"), self.tr("If you accept, all the files will be saved\nand the application will be closed."), QMessageBox.Icon.Question, self) + def _quit_app_func(self, force: bool = False): + """ called to Quit the application """ + if force: + msg_qmbox = SimpleMsgBox(self.tr("Do you really want to Quit?"), self.tr("If you accept, nothing will be saved\nand the application will be closed."), QMessageBox.Icon.Critical, self) + else: + msg_qmbox = SimpleMsgBox(self.tr("Do you really want to Quit?"), self.tr("If you accept, all the files will be saved\nand the application will be closed."), QMessageBox.Icon.Question, self) yes_qpbtn = msg_qmbox.addButton(self.tr("Yes"), QMessageBox.ButtonRole.ActionRole) msg_qmbox.addButton(self.tr("No"), QMessageBox.ButtonRole.ActionRole) # no_qpbtn = msg_qmbox.addButton(self.tr("No"), QMessageBox.ButtonRole.ActionRole) msg_qmbox.exec() if msg_qmbox.clickedButton() == yes_qpbtn: - self.config['lastMaximized'] = self.isMaximized() - # if self.isMaximized(): - # self.showNormal() - # TODO: save size of non maximized window - self.config['lastHeight'] = self.height() - self.config['lastWidth'] = self.width() - # saving self.currentLang in config memory - self.config['defaultLang'] = self.currentLang - # save memory config to file - save_yaml_app_config(self.config) - + if not force: + self.config['lastMaximized'] = self.isMaximized() + # if self.isMaximized(): + # self.showNormal() + # TODO: save size of non maximized window + self.config['lastHeight'] = self.height() + self.config['lastWidth'] = self.width() + # saving self.currentLang in config memory + self.config['defaultLang'] = self.currentLang + # save memory config to file + save_yaml_app_config(self.config) self.wallet_Qw.save_wallet() self.logViewer_Qw.close() - self.connPool.clean_up() - self.connPool = None - self.tray_Qsti.deleteLater() - logger.debug(self.tr("Closing App")) # close instance self.app.quit() - def _force_quit_app_func(self): - msg_qmbox = SimpleMsgBox(self.tr("Do you really want to Quit?"), self.tr("If you accept, nothing will be saved\nand the application will be closed."), QMessageBox.Icon.Critical, self) - yes_qpbtn = msg_qmbox.addButton(self.tr("Yes"), QMessageBox.ButtonRole.ActionRole) - msg_qmbox.addButton(self.tr("No"), QMessageBox.ButtonRole.ActionRole) # no_qpbtn = msg_qmbox.addButton(self.tr("No"), QMessageBox.ButtonRole.ActionRole) - msg_qmbox.exec() - if msg_qmbox.clickedButton() == yes_qpbtn: - QApplication.instance().quit() - def _change_lang(self, lang_code, lang_name): if self.translator.load(f":trans_{lang_code}"): self.app.installTranslator(self.translator) @@ -3675,18 +3792,18 @@ class MainWindow(QMainWindow): self.tray_Qsti.show() def _show_about_window(self): - """Affiche une boîte de dialogue 'À propos'.""" + """ Show About Dialog. """ bg_txt = f"

{APP_FULL_NAME}: {APP_VERSION}
PYTHON: {platform.python_version()}
Qt: {pyside6version}
OS: {platform.system()}

" about_window = AboutWindow(bg_txt, self) about_window.show() def _show_help_window(self): - """Affiche une boîte de dialogue 'Aide'.""" + """ Show Help Dialog. """ help_window = HelpWindow(":pdfHelpFr", self) help_window.show() def _init_main_toolbar(self): - # Création de la barre d'outils + """ Initialize the main ToolBar """ self.addToolBar(self.main_Qtb) self.main_Qtb.setMovable(False) favicon_toolbar_qlbl = QLabel() @@ -3872,7 +3989,7 @@ class MainWindow(QMainWindow): self._ask_load_wallet_filepath(return_to_prev) def _init_file_qmnu(self): - """Initialize File Menu.""" + """ Initialize File Menu. """ # add File Menu to Main MenuBar self.main_Qmnub.addMenu(self.file_Qmnu) # add action "New Wallet" to File Menu @@ -3899,11 +4016,28 @@ class MainWindow(QMainWindow): # add action "Close application" to File Menu self.file_Qmnu.addAction(self.closeApp_Qact) # set action target - self.closeApp_Qact.triggered.connect(self._quit_app_func) + self.closeApp_Qact.triggered.connect(lambda: self._quit_app_func(False)) # add action "Close without saving" to File Menu self.file_Qmnu.addAction(self.forceCloseApp_Qact) # set action target - self.forceCloseApp_Qact.triggered.connect(self._force_quit_app_func) + self.forceCloseApp_Qact.triggered.connect(lambda: self._quit_app_func(True)) + + def _init_tools_qmnu(self): + """ Initialize Tools Menu. """ + # add File Menu to Main MenuBar + self.main_Qmnub.addMenu(self.tools_Qmnu) + # add action "New Wallet" to File Menu + self.tools_Qmnu.addAction(self.checkAddress_Qact) + # set action target + self.checkAddress_Qact.triggered.connect(self._check_address) + # add action "Open Wallet" to File Menu + self.tools_Qmnu.addAction(self.checkAddress_Qact) + + def _check_address(self): + if not self.solana_addr_check: + self.solana_addr_check = True + ch_adr_window = SolanaAdressCheckerWindow(self.connPool, self) + ch_adr_window.show() def _conn_disc(self): self.conn_disc_Qact.setDisabled(True) @@ -3926,7 +4060,7 @@ class MainWindow(QMainWindow): self.filesHistory_Qmnu.addAction(self.noFileHistory_Qact) def _init_langs_qmnu(self): - """Initialize Langs Menu.""" + """ Initialize Langs Menu. """ self.main_Qmnub.addMenu(self.langs_Qmnu) # Ar self.langs_Qmnu.addAction(self.changeLangAr_Qact) @@ -3982,7 +4116,7 @@ class MainWindow(QMainWindow): self.changeLangZh_Qact.setDisabled(True) def _init_help_qmnu(self): - """Initialize Help menu.""" + """ Initialize Help menu. """ # add Help Menu to Main MenuBar self.main_Qmnub.addMenu(self.help_Qmnu) # add action "Help Content" to Help Menu @@ -3995,7 +4129,7 @@ class MainWindow(QMainWindow): self.about_Qact.triggered.connect(self._show_about_window) def update_display(self): - """refresh translations of texts in main menubar when language change.""" + """ refresh translations of texts in main menubar when language change. """ self.file_Qmnu.setTitle(self.tr("File")) self.newWallet_Qact.setText(self.tr("New Wallet")) self.openWallet_Qact.setText(self.tr("Open Wallet")) @@ -4008,6 +4142,8 @@ class MainWindow(QMainWindow): self.hideApp_Qact.setText(self.tr("Hide application")) self.closeApp_Qact.setText(self.tr("Close application")) self.forceCloseApp_Qact.setText(self.tr("Close without saving")) + self.tools_Qmnu.setTitle(self.tr("Tools")) + self.checkAddress_Qact.setText(self.tr("Check Address")) self.langs_Qmnu.setTitle(self.tr("Languages")) self.help_Qmnu.setTitle(self.tr("Help")) self.help_Qact.setText(self.tr("Help Content")) @@ -4024,14 +4160,12 @@ class MainWindow(QMainWindow): self.setLayoutDirection(get_qt_dir()) def closeEvent(self, event: QEvent): - """Intercepter la fermeture pour masquer la fenêtre au lieu de quitter.""" + """ Intercept closing to hide the window instead of exiting. """ self.hide() event.ignore() def showEvent(self, event) -> None: - """ - fired on show event - """ + """ fired on show event """ self.showApp_Qact.setDisabled(True) self.hideApp_Qact.setDisabled(False) super().showEvent(event) @@ -4040,9 +4174,7 @@ class MainWindow(QMainWindow): center_on_screen(self) def hideEvent(self, event) -> None: - """ - fired on hide event - """ + """ fired on hide event """ self.showApp_Qact.setDisabled(False) self.hideApp_Qact.setDisabled(True) self._mustCenter = True @@ -4084,5 +4216,4 @@ if __name__ == "__main__": # Planifier l'affichage de la fenêtre principale après le splash screen main_window = MainWindow(app, translator, config) splash_screen.fadeOut.finished.connect(main_window.first_load) - sys.exit(app.exec()) diff --git a/icons.qrc b/icons.qrc index 716dfdc..be37580 100644 --- a/icons.qrc +++ b/icons.qrc @@ -4,6 +4,7 @@ res/icons/about.png res/icons/burn.png res/icons/buy.png + res/icons/check_address.png res/icons/connect.png res/icons/connected.png res/icons/copy.png diff --git a/rc_icons.py b/rc_icons.py index 54088fd..2788a65 100644 --- a/rc_icons.py +++ b/rc_icons.py @@ -2315,62 +2315,146 @@ b1\xbe\xe9=\xed&~\x80\xa7\x1dX\xc9\xfc&\x9c\ h\x89\xbcg\x05\xc6rJB\x8a\xc4\x04FFb\xd8\ \x1c\xc3\xc9\xb4\xc6\xb9G\xfe\x0fy\xf8Xh\xfa\xec\x83\ \x0d\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x03T\ +\x00\x00\x08\x9b\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x000\x00\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\ -\x00\x00\x03\x1bIDATx\x01\xed\xc1A\x8b\x94\xf7\ -\x1d\x00\xe0\xe7\xf7\xf7-.d\xa4\x03\x9dC\xa1\x8b\xfb\ -~\x00!.d\x89b\xbeI\x05=\x14\xda\x83\x10\xc1\ -\x8b\x90B\x08\xb9X\xc8\xc1\x82\x87\xf6\x10j\xa1\x1f%\ -\xa0\xc1\xc0n\xc1c\x0f\xb3\xb2\x85\x1eF\x18\xd89\xac\ -\xe4\xdd\xf7\xd7\x19\xf6}3b\xc0u\x13[g\xc0\xe7\ -\xf1\xde{?O8\xc5\x93:G\xdf3(\xadQ2\ -0\x17\x8c2\x0d\xf4B%m:\x8bp 5:\x11\ -f\xc9\xc4\x5c0k\x8b\xc9/\x98\xed\x8cc\xe25\xc2\ -+\xbe\xdb\xca\xcb\xc7\xe9\xf7m\xfa\x04\x97\xac\x86\xbd\x12\ -\x1e\x9f\x0b\x7f\xfdh?\xf6\xbc$tv\xeb\xac^\xa4\ -?f\xeb3TVS\x13\xc5\x17\xe7\xc3\xbd\xedq4\ -\xe6*\x9d\xa3\xd6=\xe9\x8e\x1f{\x8e\x19\xa6\xc2\xd4\x89\ -\x890\xd3\x09\x9al\x1d8\x83(6\x93J/\x0d0\ -\xb2\x90\x86\x18b\x80_Y\xaa\xb2\xf5\xe5Q\x18\xe0\xae\ -\xb90\xf7h+\xafh=\xd2)\xe1\xeb6<\xd8\x08\ -O\xb7\xc7\xd1x\x87v\xeb\xac\x8e\xd2\xa5\x92n\xb7\xe9\ -\x86^\xb1su?\xbe\xab,\xa4[:%\xfc\xfd\xe3\ -g\xf1;+b{\x1c\x0d\xf6p\xf3\xdb\x8b\xa9M7\ -,\xa4\xdb\xb8^,\xa4k:m\xb8oEex\xa0\ -\x97\xae\x99+N\xd4:\x1b\xe1\xa9\x15u>\xecY\xaa\ -\xcd\x95Gu\xfe\xda\xd2\xf3\xedq4V\xd4\xf68\x1a\ -<\xd7yR\xe7\xa8`\xc3\xd2\xcc\xea\x9b\xe94\x0c\x8a\ -V\xad\x17\xc6V]8\xd0k\xd5\xc5\xfai\xbc\xa4`\ -dib\xf5M-\x0d\x8b4\xd0\x0b3\xab.L\xf5\ -\xd2\xb0Xs\xc5\x9a+\xa8\xf5Zc+\xae\xa4\x03\xbd\ -\xb0Y\xac\x9965z\xa9*\xd6\x5c\xb1\xe6\x8a5W\ -\xac\xb9b\xcd\x15k\xaeXs\xc5\x9a)\xa1\xd2\x0bM\ -\xc1X\xaf\xa8\xad\xb86l\xea\xa5\x83b\xcd\x15k\xae\ -\x083\xbd4\xb0\xea\xd2P/L\x0b&\x96FV\xdf\ -\xd0\xd2\xb4X?\x95\x97T\x8a\xb1c'R\xedM\x0c\ -\xb3r\x9c\xd7\xa5\xda\xdb\x10\xc6\x0e\xcbCo\x22m\xea\ -\x15\xe3\x0aG\x96>\xf0&\x8e\xf3\xba\xf47oK\xe2\ -B\xcbay\xe8t\xbf\xd4\xa9\x98\x95\xab\xe3\xf8\x8f\xa5\ -\xd1n\x9d\x95w\xa3q\x8a\xdd:+\x0cuv\xc61\ -\xa9\x9c\xd8\xc7\x96\xb9\xa3t\x09{^\xe7\xb0Gei\x88\xa1\x85\xb4\x94~\ -\x90\xce.[?E\x13\xc5\x17\xe7\xc3=\x9d\xf0\x8a'\ -[y9\xf9\xc3q\xeb\x0a>\xb4\x1a\xfey\xaex\x1c\ -\xfceg?\xf6\xbc$\x9c\xe2I\x9d\xa3\x86\x814\x94\ -\x86N\x8c\xa4\x81\xa5\x0a\x9b\xce\xe6\x00\x8d^\x98ab\ -!L\x85i\xc5lg\x1c\x13\xef\xbd\xf7\xbf\xf3_@\ -\xeb\x13+\x80\x08@\xe1\x00\x00\x00\x00IEND\xae\ -B`\x82\ +\x00\x00\x08bIDATx\x01\xed\xc1\x7fh\x9c\x85\ +\x1d\xc7\xf1\xf7\xf7\xb9G\x8dr\xe9\xdd\xa4\x83\xe2Br\ +\xb8\x0e\xf2G\xd9\xd2-\x9d=\x160\xd9\xdaQfe\ +\xed\x8c\xae\xa2\xb2\x0auVp\xd8\x8e\xc2Z\xacxg\ +\x1d\x83\xadb\xdd\x0f\xdct\xc5\xb8v\xc4\xd1J\xd4\xea\ +J\xa9b\x86\xd5\xb46%\x91\x15\x96?\xca\xb8K+\ +\xdc \xc2]\xee\x18i\xbc\xe7\xf9\xecn\xcf=\xe4\xec\ +\xf2\xe3j\xb3!\xac\xaf\x17W]\xf5\x7f\xce\xf8\xacj\ +\xd62\xa4{\x81(f\xc3\x14\xedufa|\x16-\ +\xd1&|\x1d\x00\x5cB\xc6\x9b`wS\xb4\x09\xea\x18\ +\x8bd$!\xf7\xa2O\xa7D;\xc6RD\x14\x876\ +*\x5c\xf8\xb0\xecS\xc6\xc8`\x8c6\x19gWf\xac\ +\xcclbZ\x8f\xa7\x01\xc0\xe5?\x8da\xd6C\xd1r\ +\xd4\x18W`\xb8M\x89\x8fa3\xe2\x1b\x88\xd5@\x94\ +\xc6La\x9c@\xdc\x97\x85\xa1V\xad{ \x96z\xda\x13\xbfD\ +|\x13\xb8\x19\xb8\x96\xc6\xb9\xc0\xcd\x18\x1f\xed/\xa4O\ +P\x15U\x1c8\x0e\xdcD \x83Y\x0fE;\xcc\xf5\ +\xa9\xbf!\xbe\x078\x88\x04\xd7\xa5>`:=F\x85\ +\xcbe8\xd9\xa6N\xc4\xb3\x12\x9db\x16F\x168\xe9\ +\x18\x17|\x9f\x12\x90\xa1\xcahA\xb8\x18+\x10\x1d\xc0\ +\x17\x81)\xc78AU\x5c.\x9e\xfa\x11\xed\x04JD\ +\xb8\x9d\x82MPU\xb0\xc34k\x0d\xd2\x83\xfc\x9bV\ +\x03\xafP\xe1\xd2\x80\xf7\x13\x8a\xfa>\x8f\xcbg\x1b\xe0\ +R\xc7\x8c7\x04\x07\xafq\x18\xec\xccX\x8e\x06\x0c%\ +\x14\xa7\xe2\x96\x8c\xe5\xa9\xf2\x94B\xac#dv?\x05\ +;K(\xae(\x9en%$r\xd4\xb8,`\xa8U\ +\xed\x9e\xc7Q \xc1\x8c\x12\xb0\xdfu\xf8\xf5\xaa\xac\x9d\ +\xe32%3\x96'\xd4\xac^\xa4G\x09\x19?\xa5h\ +\x87\xa9\xe7\xe9W\x88v\x02y\xcc\x0eR\xe32\x8f3\ +\x09uM{\x1c\x01\xe2\x84\x8cc\xc0\xd6\xe4\xb8e\xb8\ +R1\x7f\x05\x9e^ d\xbcA\xd1\xd9M\xbd%\xda\ +\x8a\xaf\xcd\x84\x1c\xdb\xce\xa4MP\xe32\x87S\x09\xf5\ +N{\xf4\x03.\x81\x92\xeb\xf0\xd0\xaa\xac\x1dd1D\ +\x15\xc7\xd7\x00\x10%0F\xc46Q/\xa6\x0e<=\ +M\xc8x\x91I\xeb\xa3\x8e\xcb,\xdeOh\x8d\xe7\xd1\ +\x0f\xb8\x04r\xae\xc3\xdaUY;\xcbb\x88\xcb\xc5S\ +?b9\x81\x12\xa6\x8d\xe4\x9d\x12\xa1\xa8\xe2x\x1a\x00\ +\x9a\xa82> b[\xb9\x84\xcb%\xdek\xd5r\xcf\ +\xe3\x10\xe0\x12\x18\x03zVe-\xc7b\xf1\x94B\xac\ +#\x14\xb1\xbb)8c\xd43\x1d@$\x08\x94\x90z\ +\xc9;S\x5c\xc2\xa1\xce\xfb\x09EM\x1c\x01\xe2\x04.\ +\x00=\xc9\xf3\x96c\xb14k\x03\xe2QBF\x9a\x82\ +\xbdN\xbdf?\x85XO\xc8\xec>J\x91s\xcc\xc2\ +\xa1\x8e\xe7\xb1\x1bh'Pr\x1dnO\x9e\xb7\x1c\x8b\ +\xa5\xd9kG:@\xc88F\xc4\x9e\xa4^L\xeb\x11\ +\x8f\x132\x9e\xa2h\xaf0\x07\x97\x9a\xd3mZ^\xf6\ +y\x84\x1a3~\xb4*k\xa3\xcc'\xae&<\xadF\ +,\xc5l\x90\xa2M0\x97\xb8\xa2\x945\x00D\xa92\ +\xce!\xdbD\xde\xca\x84\xa2\xder<\xf5\x132\xde\x22\ +b;\xa9\x18J\xa8\xc9\xf1y\xd6\x878\xb0=9n\ +\x19*\x5cj\xca\xe2\x17@\x13U\xc6\xbb\xab\xc7\xad\x8f\ +\xf9\xc4\xb4\x9e\xb2\x9e\x05Z\xa8\x92\xca4k\x1f\x11v\ +\x91\xb72\x97\xf2\xf4\x02\xd0N\xa0\x84cwR\xb0<\ +\xa1\xb8\xa2x\x1a@D\x09d\xc06\x91\xb72U>\ +\x1b|\xb1\x99*\xc3\x80\x0dT8T\x0c\xb5\xa9\x1d\xb1\ +\x81\x90\xb1\x8d\xf9\xc4\xd4\x8b\xa7\x01\xa0\x85\x19.\xd2\x0e\ +<\xf5\x13\x97K\xbd\xa8\xbf\x13\xd1K\xc8\xb1\x87(\xd8\ +(\xf5<\x1d@\xac P\x22b\x1b)\xda\x04!\xe3\ +,!\xf1\xdd\xd3mZN\x85C\x95\xd8L\xc8\xf8S\ +2k\xc3\xcc%\xe6w\xe0\xa9\x1fp\x09d\x80\xb7\x08\ +\x89^|\x1d'\xae(U1\xad\x01\xf60\xe3\x19&\ +\xed \xf5\x9a\xfd\x14b\x03!\xc7\x1e\xa2`\xa3\xd4I\ +f\xed,\xc6\xab\xd4\xf8b\x07\x15\xceHB.p\x0f\ +!\xd1\xc7|<\x9e\x04\x5c\x02c\x98%)9k@\ +O\x11\xf2\xe9\xc6\xd7Qbj\xc7\xd3\x01\xc0\xa5\xcax\ +\x17\xd7vP\xafY\xbd\x88\xc7\x09\x19\xcf0i\x07\x99\ +\x85\x19{\xa9\xf1\xc5\x1d#\x09\xb9\xce\x94G7\xa2\x85\ +*\xe3\xc3\xa6\x08o2\xbf\x15\x84\x1c\xee\xa4h9\xaa\ +J\x91\x1d\x98m'\xe4\xd3\x85\xa7\xbf\x02\xcb\x08\xe4\xc0\ +z\xc9[\x99PL\x1dH/\x102\x8e\x11\xb1\x1d\xcc\ +au\xd6N`|H`\xe9\x94G\xb7\x83\xd1\xc1\x8c\ +\xc3+3Vf~yB\xa2\x9bzE\xdb\x87\xd9v\ +f\xb8\x04\xcaD\xecn\x8a\x96#\xd4\xacex:\x02\ +D\x09\x8c!\xdbD\xde\xca\xcc\xc3\xe0\x15BF\x87\x83\ +(Sc\xe0\xb2\x10\x87\x97\x08\x89\xa7\x89\xa9\x97zE\ +\xdb\x87\xd9v>i\x17\x05\x1b$\x14\x97\x8b\xe9\x10\xd0\ +B\xa0\x84i#%\xcb\xb3\x00\x09\x97\x90(;\xe60\ +J\x8d\xc4\xf7G\x12r\x99\x8fc{\x813\x04\x5c<\ +\xf5\x13S\x17\xf5\x8a\xb6\x0fc\x17\xf0\x11\xc6o(9\ +{\xa9\xe7\xe9y|\xba\x08\x94\x89\xd8\x9d\x14#c,\ +`(\xa1&\xe0\x0ej\xcca4\xf2\xfbB:\xb3%\ +\x9e\xba\x1f\x88\x037\x94ad\x7f!=\xc6\x5c\xa6\xd2\ +>\xd7\xa5^\x03\xd6\x03K\x01\x07q\x17M\xa9\xb7\xb9\ +\x98\xbe@h:}\x82\xe9\xf4\xcf\x99N\xff\x99zQ\ +\x7f7\xf0cf\xfc\x84\xa2\xf3\x07\x1a\xf0\xc0\x92\xd4\x06\ +`3UF69n\xdb\x1c*\x0c\xfa\xa81\xd8\xc2\ +B\x8a\x96\xc3\xb1\xb5@\x8e@\x14_\x03\xc4\xd4\xc1|\ +b\xea\x05\xf60c?%g/\x8d\xdbL\x8dA\x1f\ +\x15\x0e\x15\xae\xd1G\x8d\xc4m\xa7\xda\xd4\xc5B&\xed\ +\x02\xa6\x1e G`\x19\x9e\xde!\xa6.f\xb3D\x9d\ +xz\x81\x90\xc3_pm+\x0d\x1ajS\xb7\xc4m\ +\xd4\xb8F\x1f\x15\x11*\x9e+\xa4\xf3?\x8c\xa7\xbe$\ +\xf82\x15\x12_}\xf8\xc6\xd4\xfe\xdf\xe5\xd3>\xf3\x99\ +~b\x82\xa6\xd4\xbb\x88\xf5@\x14\xb8\x16q\x17\xd7\xa7\ +\xde\xe1bz\x9c\xd0\x12\xb5\xe0\xebm\xe0F\x02c\xc8\ +\xd62i\xff\xa4\x01#\x09\xb9e\x9f\x97\x81eT8\ +\xc6\x8b_\x1f\xb7>*\x1cj\x04;\x81\x12\x81\x8e\x8b\ +>[i\xc4\xa4\x9d\xc4\xd4\x03\xe4\x08D\xf1t\x94\x98\ +6Q\xb5\xc4\xef\xc0\xd7q`\x19\x81\x09L\x1b)Y\ +\x9e\x06M\xf9<\x0ct\x10(\xf9b'5F\x9d\xa1\ +V\xedF\xec!Pv\x22\xac\xbd%c\x834\xa2\xd9\ +kG\xf66\xb0\x8c\xb9\x95\x89\xd8Z\x0a6H\x83N\ +%\xd4\xed{\x1c\x07\x5c\xaa\x8c\xc7\x92\xe3\xf6$5\x0e\ +\xf5\x1c\xf6b\x9c!\xe0\xfa\x1e\x87\x86Z\x95\xa0\x11\xc5\ +\xc8\x18\xa6\x1e\x8cs\xcc\xaeD\xc46R\xb0A\x1a\xf4\ +^\xab\x96\xfb\x1e\x87\x00\x97*\xe3\x0c\x0e{\xa9\x13\xa1\ +\xce\xfe|\xba\xbceI\xea5\xe0^ \x0a\xdc\x00l\ +\xd8\x12O\x1d\xdb_HO\xb0\x90\xe9'&\xb8!\xf5\ +<\xb2k\x80\xcf\x03K1\xfe\x81\xd9\xcb\x98~\xc0\xa4\ +\xf3\x1e\x0d\x1ajU\xbb\x89c\xc0M\x04r\xc0\xad\xc9\ +q\xcbS\xc7\x98\xc5\xe96u\x96}\xde\x01\x9a\x08\xe4\ +\xaf\x8dp\xfb\xd72v\x82\xff\x813\x09uM{\x1c\ +\x01\xe2\x04J\xaeC\xcf\xaa\xac\x0ds\x09c\x0e\xc3m\ +Z\xf3\xb1\xcf\x00\x10%P\xc6aW\x93\xb1oe\xc6\ +\xca\xfc\x17\x8c$\xe4N\x89m\xf8\xfc\x0cp\x09\x94\xae\ +q\xd8\xd8\x99\xb57\x99E\x849\xecY\xaa\ +\xcd\x95Gu\xfe\xda\xd2\xf3\xedq4V\xd4\xf68\x1a\ +<\xd7yR\xe7\xa8`\xc3\xd2\xcc\xea\x9b\xe94\x0c\x8a\ +V\xad\x17\xc6V]8\xd0k\xd5\xc5\xfai\xbc\xa4`\ +dib\xf5M-\x0d\x8b4\xd0\x0b3\xab.L\xf5\ +\xd2\xb0Xs\xc5\x9a+\xa8\xf5Zc+\xae\xa4\x03\xbd\ +\xb0Y\xac\x9965z\xa9*\xd6\x5c\xb1\xe6\x8a5W\ +\xac\xb9b\xcd\x15k\xaeXs\xc5\x9a)\xa1\xd2\x0bM\ +\xc1X\xaf\xa8\xad\xb86l\xea\xa5\x83b\xcd\x15k\xae\ +\x083\xbd4\xb0\xea\xd2P/L\x0b&\x96FV\xdf\ +\xd0\xd2\xb4X?\x95\x97T\x8a\xb1c'R\xedM\x0c\ +\xb3r\x9c\xd7\xa5\xda\xdb\x10\xc6\x0e\xcbCo\x22m\xea\ +\x15\xe3\x0aG\x96>\xf0&\x8e\xf3\xba\xf47oK\xe2\ +B\xcbay\xe8t\xbf\xd4\xa9\x98\x95\xab\xe3\xf8\x8f\xa5\ +\xd1n\x9d\x95w\xa3q\x8a\xdd:+\x0cuv\xc61\ +\xa9\x9c\xd8\xc7\x96\xb9\xa3t\x09{^\xe7\xb0Gei\x88\xa1\x85\xb4\x94~\ +\x90\xce.[?E\x13\xc5\x17\xe7\xc3=\x9d\xf0\x8a'\ +[y9\xf9\xc3q\xeb\x0a>\xb4\x1a\xfey\xaex\x1c\ +\xfceg?\xf6\xbc$\x9c\xe2I\x9d\xa3\x86\x814\x94\ +\x86N\x8c\xa4\x81\xa5\x0a\x9b\xce\xe6\x00\x8d^\x98ab\ +!L\x85i\xc5lg\x1c\x13\xef\xbd\xf7\xbf\xf3_@\ +\xeb\x13+\x80\x08@\xe1\x00\x00\x00\x00IEND\xae\ +B`\x82\ \x00\x00\x07\xe5\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -4904,14 +5044,18 @@ qt_resource_name = b"\ \x0f\xa3\xec\xf0\ \x00i\ \x00c\x00o\x00H\x00e\x00l\x00p\ -\x00\x0b\ -\x08R\xa6\xc5\ +\x00\x0f\ +\x00\x97A\xc3\ \x00i\ -\x00c\x00o\x00T\x00r\x00a\x00y\x00H\x00i\x00d\x00e\ +\x00c\x00o\x00C\x00h\x00e\x00c\x00k\x00A\x00d\x00d\x00r\x00e\x00s\x00s\ \x00\x0c\ \x06\xb8\xe6\x02\ \x00i\ \x00c\x00o\x00H\x00t\x00t\x00p\x00E\x00r\x00r\x00o\x00r\ +\x00\x0b\ +\x08R\xa6\xc5\ +\x00i\ +\x00c\x00o\x00T\x00r\x00a\x00y\x00H\x00i\x00d\x00e\ \x00\x0c\ \x05n\xf2\xc4\ \x00i\ @@ -4992,71 +5136,73 @@ qt_resource_name = b"\ " qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00)\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00*\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xa5\ +\x00\x00\x01\x93\xe4\x06\xdb\xac\ \x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x00fl\ \x00\x00\x01\x93n\x9f\x15\xb0\ -\x00\x00\x04\x86\x00\x00\x00\x00\x00\x01\x00\x01\x1eX\ +\x00\x00\x04\xaa\x00\x00\x00\x00\x00\x01\x00\x01&\xf7\ \x00\x00\x01\x93\x07$\xf6\x9d\ \x00\x00\x00\xb4\x00\x00\x00\x00\x00\x01\x00\x003\xd2\ \x00\x00\x01\x93q\xa6\x97\x90\ \x00\x00\x01`\x00\x00\x00\x00\x00\x01\x00\x00`m\ \x00\x00\x01\x93\xd6|\xaf`\ -\x00\x00\x03\xf2\x00\x00\x00\x00\x00\x01\x00\x01\x01\x84\ +\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x01\x0a#\ \x00\x00\x01\x93\xd6y\xd8\x9c\ \x00\x00\x01\xde\x00\x00\x00\x00\x00\x01\x00\x00p\xb4\ \x00\x00\x01\x92\xeeIk\x1d\ \x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00JI\ \x00\x00\x01\x92\xf0_\xf0N\ -\x00\x00\x02\xe2\x00\x00\x00\x00\x00\x01\x00\x00\xac\xa2\ +\x00\x00\x03\x06\x00\x00\x00\x00\x00\x01\x00\x00\xb5A\ \x00\x00\x01\x93\x018\x94\xcb\ -\x00\x00\x02\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x98`\ +\x00\x00\x02\xb0\x00\x00\x00\x00\x00\x01\x00\x00\xa0\xff\ \x00\x00\x01\x93h\xc9K\xaa\ -\x00\x00\x02n\x00\x00\x00\x00\x00\x01\x00\x00\x90\xfd\ +\x00\x00\x02v\x00\x00\x00\x00\x00\x01\x00\x00\x96D\ \x00\x00\x01\x93\xb0\x98\xb7\x13\ -\x00\x00\x03\x14\x00\x00\x00\x00\x00\x01\x00\x00\xbdp\ +\x00\x00\x038\x00\x00\x00\x00\x00\x01\x00\x00\xc6\x0f\ \x00\x00\x01\x92\xeeF\xbc;\ -\x00\x00\x02\xc4\x00\x00\x00\x00\x00\x01\x00\x00\xa6\xaa\ +\x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\xafI\ \x00\x00\x01\x93n\xa3-\xc3\ \x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00~\x83\ \x00\x00\x01\x93\x06\xe1b\x8d\ -\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xa5\ +\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x00\x9d\xa7\ \x00\x00\x01\x93\x06\xe0?\xd4\ \x00\x00\x01&\x00\x00\x00\x00\x00\x01\x00\x00O\xec\ \x00\x00\x01\x93\x07$\x13\xfc\ \x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00Ar\ \x00\x00\x01\x92\xeeL\x7f\xc3\ -\x00\x00\x04p\x00\x00\x00\x00\x00\x01\x00\x01\x17\xfe\ +\x00\x00\x04\x94\x00\x00\x00\x00\x00\x01\x00\x01 \x9d\ \x00\x00\x01\x92\xeeCi\xe8\ -\x00\x00\x02\xaa\x00\x00\x00\x00\x00\x01\x00\x00\xa0I\ +\x00\x00\x02\xce\x00\x00\x00\x00\x00\x01\x00\x00\xa8\xe8\ \x00\x00\x01\x93\x13Mf\xc5\ \x00\x00\x01\xac\x00\x00\x00\x00\x00\x01\x00\x00j\xc3\ \x00\x00\x01\x93\xd6x\xe55\ -\x00\x00\x04.\x00\x00\x00\x00\x00\x01\x00\x01\x08\xc5\ +\x00\x00\x04R\x00\x00\x00\x00\x00\x01\x00\x01\x11d\ \x00\x00\x01\x93\xb0\x94\xb3G\ \x00\x00\x00\xd2\x00\x00\x00\x00\x00\x01\x00\x00:\x07\ \x00\x00\x01\x93\xb0\x96G\x83\ -\x00\x00\x03&\x00\x00\x00\x00\x00\x01\x00\x00\xc3\xe5\ +\x00\x00\x03J\x00\x00\x00\x00\x00\x01\x00\x00\xcc\x84\ \x00\x00\x01\x93\xb0\x95\x91I\ \x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00Wo\ \x00\x00\x01\x93\x014\x0f\xe7\ \x00\x00\x00V\x00\x00\x00\x00\x00\x01\x00\x00\x17\x14\ \x00\x00\x01\x93\xb0\x97\xb2\xf5\ -\x00\x00\x03v\x00\x00\x00\x00\x00\x01\x00\x00\xd8u\ +\x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\xe1\x14\ \x00\x00\x01\x93n\xa2\x8d\xa7\ -\x00\x00\x03\xd8\x00\x00\x00\x00\x00\x01\x00\x00\xf8\x5c\ +\x00\x00\x03\xfc\x00\x00\x00\x00\x00\x01\x00\x01\x00\xfb\ \x00\x00\x01\x93\x02\x96zR\ -\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\xb4\xe5\ +\x00\x00\x03\x1e\x00\x00\x00\x00\x00\x01\x00\x00\xbd\x84\ \x00\x00\x01\x93\x02\x95\xd0\xb1\ -\x00\x00\x03V\x00\x00\x00\x00\x00\x01\x00\x00\xd1\xb7\ +\x00\x00\x03z\x00\x00\x00\x00\x00\x01\x00\x00\xdaV\ \x00\x00\x01\x93\x017\x852\ \x00\x00\x01\xf6\x00\x00\x00\x00\x00\x01\x00\x00v\xa4\ \x00\x00\x01\x93\xd6{\xeb\xb6\ \x00\x00\x00\x14\x00\x00\x00\x00\x00\x01\x00\x00\x06\xca\ \x00\x00\x01\x93\xd6{S\x11\ -\x00\x00\x04J\x00\x00\x00\x00\x00\x01\x00\x01\x0d\xe7\ +\x00\x00\x04n\x00\x00\x00\x00\x00\x01\x00\x01\x16\x86\ \x00\x00\x01\x92\xf0`\xe3/\ -\x00\x00\x03\xa6\x00\x00\x00\x00\x00\x01\x00\x00\xe7(\ +\x00\x00\x03\xca\x00\x00\x00\x00\x00\x01\x00\x00\xef\xc7\ \x00\x00\x01\x93\x018\x07\xf7\ \x00\x00\x00B\x00\x00\x00\x00\x00\x01\x00\x00\x0eg\ \x00\x00\x01\x92\xeeH50\ @@ -5068,11 +5214,11 @@ qt_resource_struct = b"\ \x00\x00\x01\x92\xeeD(Y\ \x00\x00\x00x\x00\x00\x00\x00\x00\x01\x00\x00\x1ed\ \x00\x00\x01\x93n\xf8\x8bH\ -\x00\x00\x03B\x00\x00\x00\x00\x00\x01\x00\x00\xcb\xbf\ +\x00\x00\x03f\x00\x00\x00\x00\x00\x01\x00\x00\xd4^\ \x00\x00\x01\x93rh\x9d!\ -\x00\x00\x03\xc4\x00\x00\x00\x00\x00\x01\x00\x00\xee\x02\ +\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x01\x00\x00\xf6\xa1\ \x00\x00\x01\x92\xeeD\xf4)\ -\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\xdf\x8c\ +\x00\x00\x03\xb6\x00\x00\x00\x00\x00\x01\x00\x00\xe8+\ \x00\x00\x01\x93\x02\x93'\xa3\ \x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x00'\xbc\ \x00\x00\x01\x92\xeeG{\x87\ diff --git a/res/icons/check_address.png b/res/icons/check_address.png new file mode 100644 index 0000000000000000000000000000000000000000..ac801eb619a4097024c56e7c40f955da8315b891 GIT binary patch literal 2203 zcmV;M2xRw(P){G_bV2m~yXIEyd%xq#>kEa2^b`!g`rlhdmxs2+7rJ- zd=t1CxR%Dbz|9yR?JuNHP!&}iI1b!IAcI*CMLW#(slOq<2RMiX*QGvSIz2{(%Vjv( zrcT7O5}jfeMiw~f7#Dn>5|E+FXhFCbD-a#P7lA3qI6+lhE}r@k?F33hxSfJcP*t$Q zG;0mggZ!IT1Fulw(t!69iuyi}%V!(lQpb!p1P8Bi80^egjs^@R6sO{rjOD5FltQc{ zWOhFVJ4-08D4uR5By!ZaEMTZr?ViU`Ai<0hu-|p~^{T9$<1!^PmM7G#UZh6}89$|H zhpFf3SBQ23dCU${<1&vk33W|-90B5ljAOvo?701^A5G621#5PecrVczkO~`nWN$aDI z%PihcXpQI554Z$!uEW|@Sv$*Oh|9%OKVmt75~hvhof4s_;&>VWny4~9AYxm&gv%w~ zYt?ek;}GBy7z27%Wi7{I@svWWBNVZ_DL7-)GaW!A2$Ya|$vabA!lBOl2}MmD0pbKM z06od`mSeFrYXjm@LPnZ`y-vMmKZRIN$m~8!k#o$#p^oRV8)zp`avg3@p0^x}rP*so zIzY%cM#P?^UQFu|j}tPUqG+!$2bY@DZCYOTd4w7>ezYoUqs%9+COoZ1EX8b)qD?TD zxT2Ib07R)W&LP`G-pYic-f}$k6EfZ+VmqlAN~p>4dDe$Gusu0$87f?A%HkJ56II5$ ztH-U6`NXuIvYrK6sFKPwYabDIk+(9J^7I1GPN0NicT%uCp}3-W8bCw|WWY9&w=z{e z8+@^uD#L&cR5(-&wW%GkfsnC{`NY(Vrvada87|TeJoOWoOTDPS6VXe^v4<$wDD^xY zK&&88B4QgU+5{ovEFtmT(`{N#g+rCtOvw0CGHb)kCFXbFsUL6&WPrB_iC%miC$1<} zF|7giFyk2}?I87<{S;yYA;%t~V58JYX6-D-tJL^n2NljpLN0I*A<@TtJe~wx0%MpZ zDLHFEPZ9Fz#@A6|YRA(7z@^G~7uR-BuUWl_#|atFFlh&wg&BLP@x>h@3AxUcM>nE{ zD&xbZ2yZgK6@(n_q-Z&+N|7CyZ71Z@g{Qv)%LwGaddXXvP)r?2FAy@05wRW2VRhC{ zV0=hm>6AyegVDi^!z(jZW`21>qMLd}+lSe9X1D~(MC@*gmLt@xWq3LQLdmWPc$&#$2Y-KG&sQ1J(me0S9pxO{urJn7%-0 zI(>ra^v{`2|CxGGe>d=BX8eSr^9LI6z>?GkU^7)lHaTwF94ZX@s4>{WLW-8dv9&;s zKonmC#MHrjV(JC~NyY6Lrh5`m73K3i$Tq@Bdn1LO!q@pP1!Hc_?2@4(k_U>Si4OdBa#o(4_%JPdRY zDEVS1HAbDGR_#XYAy9TAti2^;gEXSqYe)7ru#CBtiP%~SHb{d`CDiSE90y#gjK`AW z_9Qh9^&IwMoFU-)9*2fnm7o#D$QY&`jC0JcM8q~yut6I1UaOY-9)|#zD&q_ed#Mwl z&i;J5yk$x1HQ-(#3fur}*b+}2+h0geu#o8|Cb%K>JhE$m&jU*_{(;%cn0~;d^BxTv zYE`?5W57)Wa=@+0am!Q3n8oQfb(VeJ0h$QpuZ?i~*D`j3|A8|JbxnI50pbKou0zkN ztPOJsV-CY@TJy2b01zcm#$i{Jv2SI@%KR6MB;-zsZTS2Qa0!&IaoE_IwKthXggHD{ zNdJB7=Tb*~u>ptz4&vJ$d}nJsb^5tNI>#a!Zd1p{;#I_Ez#))x9d2EfwUf-jSis3v zwE(Y6e1#chjKdhuB(rvsD=^fi)rdzBy?{%N86QZjOP;qJ^Dr7Pl8`$kw&Ak}Xri7m zjQArCgUPIo(tx2hb>P#DaSx)CPnyEvu_j~R%8Zp+fN>dyT9q*Py~MYeUEYjQ85tmt zPk}0j7!Ws+kmvy7%)*Sf1Y45hmZ3q$r!dr}4o5ufi+g}3uFRBUyyzRhO=fMB%Vb=E zQwc5geR>VLCAxqw&