diff --git a/beam b/beam index 9c4366aae..618747baa 160000 --- a/beam +++ b/beam @@ -1 +1 @@ -Subproject commit 9c4366aae08e7fbde7bc8d65f828a0deace68bfc +Subproject commit 618747baa41a5e89465a40e1dba2da6b122e9e28 diff --git a/ui/i18n/be_BY.ts b/ui/i18n/be_BY.ts index acb34a117..ad74246f6 100644 --- a/ui/i18n/be_BY.ts +++ b/ui/i18n/be_BY.ts @@ -918,14 +918,6 @@ Password Пароль - - Receive amount - Сума атрымання - - - Send amount - Сума адпраўкі - Exchange rate Абменны курс @@ -2074,8 +2066,9 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node - Вузел Ethereum + Ethereum + Ethereum node + Вузел Ethereum Ethereum seed phrase @@ -2223,10 +2216,6 @@ Please try again later or create an offer yourself. На дадзены момант няма актыўных прапаноў. Паўтарыце спробу пазней альбо стварыце прапанову самастойна. - - Send - Адправіць - All Усе @@ -3595,10 +3584,6 @@ Please, restart the wallet and try again. updating абнаўленне - - Receive - Атрымаць - Rate Курс @@ -3965,5 +3950,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Не ўдалося распакаваць файлы DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Токен + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/cs_CZ.ts b/ui/i18n/cs_CZ.ts index efafb9ec9..17a375556 100644 --- a/ui/i18n/cs_CZ.ts +++ b/ui/i18n/cs_CZ.ts @@ -918,14 +918,6 @@ Password Heslo - - Receive amount - Přijmout částku - - - Send amount - Odeslat částku - Exchange rate Směnný kurz @@ -2073,8 +2065,9 @@ Vaše verze je: %2. Zkontrolujte prosím aktualizace. Ethereum - Ethereum node - Ethereum node + Ethereum + Ethereum node + Ethereum node Ethereum seed phrase @@ -2222,10 +2215,6 @@ Please try again later or create an offer yourself. Momentálně nejsou žádné aktivní nabídky. Zkuste to prosím později nebo vytvořte svou nabídku. - - Send - Odeslat - All Všechny @@ -3595,10 +3584,6 @@ Please, restart the wallet and try again. updating probíhá aktualizace - - Receive - Přijmout - Rate Sazba @@ -3965,5 +3950,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Soubory DApp se nepodařilo rozbalit. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/de_DE.ts b/ui/i18n/de_DE.ts index 924a591eb..0dc06ebbc 100644 --- a/ui/i18n/de_DE.ts +++ b/ui/i18n/de_DE.ts @@ -915,14 +915,6 @@ Password Passwort - - Receive amount - Betrag erhalten - - - Send amount - Betrag senden - Exchange rate Wechselkurs @@ -2063,8 +2055,9 @@ erhalten (öffentlich offline) Ethereum - Ethereum node - Ethereum Node + Ethereum + Ethereum node + Ethereum Node Ethereum seed phrase @@ -2212,10 +2205,6 @@ Please try again later or create an offer yourself. Momentan gibt es keine aktiven Angebote. Bitte versuchen Sie es später noch einmal oder erstellen Sie selbst ein Angebot. - - Send - Senden - All Alle @@ -3580,10 +3569,6 @@ Bitte starten Sie das Wallet neu und versuchen Sie es erneut. updating aktualisiere - - Receive - Erhalten - Rate Rate @@ -3950,5 +3935,142 @@ Verbinden Sie Ihre Hardware-Wallet um die Transaktion abzuschließen.Couldn't extract the DApp files. Die DApp-Dateien konnten nicht entpackt werden. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/en_US.ts b/ui/i18n/en_US.ts index 9e06cdd82..902dee855 100644 --- a/ui/i18n/en_US.ts +++ b/ui/i18n/en_US.ts @@ -916,14 +916,6 @@ Password Password - - Receive amount - Receive amount - - - Send amount - Send amount - Exchange rate Exchange rate @@ -2066,8 +2058,9 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node - Ethereum node + Ethereum + Ethereum node + Ethereum Ethereum seed phrase @@ -2215,10 +2208,6 @@ Please try again later or create an offer yourself. There are no active offers at the moment. Please try again later or create an offer yourself. - - Send - Send - All All @@ -3612,10 +3601,6 @@ Please, restart the wallet and try again. updating updating - - Receive - Receive - Rate Rate @@ -3954,5 +3939,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Couldn't extract the DApp files. + + I send + I send + + + I receive + I receive + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + Infura + Infura + + + Custom RPC + Custom RPC + + + Ethereum RPC endpoint + Ethereum RPC endpoint + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + + + Check connection + Check connection + + + Unable to connect + Unable to connect + + + Ethereum Mainnet + Ethereum Mainnet + + + Sepolia Testnet + Sepolia Testnet + + + chain %1 + chain %1 + + + Connected to %1. Latest block: %2 + Connected to %1. Latest block: %2 + + + Connected to %1, but Ethereum Mainnet is required + Connected to %1, but Ethereum Mainnet is required + + + Custom ERC-20 tokens + Custom ERC-20 tokens + + + remove + remove + + + 0x contract address + 0x contract address + + + Look up + Look up + + + Add + Add + + + %1, %2 decimals + %1, %2 decimals + + + Token + Token + + + Contract address + Contract address + + + Symbol / decimals + Symbol / decimals + + + Verify this token contract address carefully. Anyone can create a token with any name. + Verify this token contract address carefully. Anyone can create a token with any name. + + + Confidential Asset + Confidential Asset + + + Asset id %1, unit %2 + Asset id %1, unit %2 + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + Invalid contract address + Invalid contract address + + + built-in + built-in + + + + Add token + + Add token + + + cancel + cancel + + + This token is already supported + This token is already supported + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + Not enough ETH to pay the redeem transaction fee (%1 needed) + diff --git a/ui/i18n/es_ES.ts b/ui/i18n/es_ES.ts index 23fc93600..c2d427b21 100644 --- a/ui/i18n/es_ES.ts +++ b/ui/i18n/es_ES.ts @@ -916,14 +916,6 @@ Password Contraseña - - Receive amount - Recibir cantidad - - - Send amount - Enviar cantidad - Exchange rate Tipo de cambio @@ -2068,8 +2060,9 @@ Su versión es: %2. Por favor, compruebe si hay actualizaciones. Ethereum - Ethereum node - Nodo de Ethereum + Ethereum + Ethereum node + Nodo de Ethereum Ethereum seed phrase @@ -2216,10 +2209,6 @@ Please try again later or create an offer yourself. No hay ofertas activas en este momento. Por favor, inténtalo de nuevo más tarde o crea una oferta usted mismo. - - Send - Enviar - All Todas @@ -3584,10 +3573,6 @@ Por favor, reinicie el wallet e inténtelo de nuevo. updating actualizando - - Receive - Recibe - Rate Valorar @@ -3954,5 +3939,142 @@ Conecte el Wallet Físico para finalizar la transacción. Couldn't extract the DApp files. No se pudieron extraer los archivos de la DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/fi_FI.ts b/ui/i18n/fi_FI.ts index 16f044289..4ae564bd6 100644 --- a/ui/i18n/fi_FI.ts +++ b/ui/i18n/fi_FI.ts @@ -916,14 +916,6 @@ Password Salasana - - Receive amount - Vastaanotettava summa - - - Send amount - Summa - Exchange rate Vaihtokurssi @@ -2068,7 +2060,8 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2217,10 +2210,6 @@ Please try again later or create an offer yourself. Ei yhtään voimassa olevaa tarjousta. Tee oma tarjous tai yritä myöhemmin uudelleen. - - Send - Lähetä - All Kaikki @@ -3586,10 +3575,6 @@ Please, restart the wallet and try again. updating päivittää - - Receive - Vastaanota - Rate Vaihtomaksu @@ -3956,5 +3941,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. DApp-tiedostoja ei voitu purkaa. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/fr_FR.ts b/ui/i18n/fr_FR.ts index 04091222b..cb261e5f2 100644 --- a/ui/i18n/fr_FR.ts +++ b/ui/i18n/fr_FR.ts @@ -916,14 +916,6 @@ Password Mot de passe - - Receive amount - Recevoir montant - - - Send amount - Envoyer montant - Exchange rate Taux de change @@ -2065,8 +2057,9 @@ Votre version est : %2. Veuillez vérifier les mises à jour. Ethereum - Ethereum node - Nœud Ethereum + Ethereum + Ethereum node + Nœud Ethereum Ethereum seed phrase @@ -2214,10 +2207,6 @@ Please try again later or create an offer yourself. Il n'y a pas d'offre active pour le moment. Veuillez réessayer plus tard ou créer une offre vous-même. - - Send - Envoyer - All Toutes @@ -3582,10 +3571,6 @@ Veuillez redémarrer le portefeuille et réessayer. updating mise à jour en cours - - Receive - Recevoir - Rate Taux @@ -3926,7 +3911,7 @@ Connectez votre portefeuille physique pour finaliser la transaction. Couldn't open the DApp file. It may be corrupted. - Impossible d'ouvrir le fichier DApp. Il est peut-être corrompu. + Impossible d'ouvrir le fichier DApp. Il est peut-être corrompu. Couldn't read the DApp manifest. @@ -3934,7 +3919,7 @@ Connectez votre portefeuille physique pour finaliser la transaction. This DApp is not supported by your wallet version. - Cette DApp n'est pas prise en charge par votre version du portefeuille. + Cette DApp n'est pas prise en charge par votre version du portefeuille. Invalid DApp file: the manifest is missing or incomplete. @@ -3946,11 +3931,148 @@ Connectez votre portefeuille physique pour finaliser la transaction. Couldn't prepare the installation folder. - Impossible de préparer le dossier d'installation. + Impossible de préparer le dossier d'installation. Couldn't extract the DApp files. - Impossible d'extraire les fichiers de la DApp. + Impossible d'extraire les fichiers de la DApp. + + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Jeton + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + diff --git a/ui/i18n/id_ID.ts b/ui/i18n/id_ID.ts index efa09357a..1cbbdd56a 100644 --- a/ui/i18n/id_ID.ts +++ b/ui/i18n/id_ID.ts @@ -915,14 +915,6 @@ Password Kata sandi - - Receive amount - Jumlah yang diterima - - - Send amount - Jumlah kirim - Exchange rate Kurs @@ -2063,8 +2055,9 @@ Versi Anda adalah: %2. Silakan periksa pembaruan. Ethereum - Ethereum node - Ethereum node + Ethereum + Ethereum node + Ethereum node Ethereum seed phrase @@ -2212,10 +2205,6 @@ Please try again later or create an offer yourself. Tidak ada penawaran aktif saat ini. Silakan coba lagi nanti atau buat penawaran sendiri. - - Send - Kirim - All Semua @@ -3577,10 +3566,6 @@ Silakan mulai ulang dompet dan coba lagi. updating memperbarui - - Receive - Terima - Rate Rate @@ -3946,5 +3931,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Tidak dapat mengekstrak berkas DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/it_IT.ts b/ui/i18n/it_IT.ts index 271d58efe..ab4a9a744 100644 --- a/ui/i18n/it_IT.ts +++ b/ui/i18n/it_IT.ts @@ -916,14 +916,6 @@ Password Password - - Receive amount - Ricevi importo - - - Send amount - Inviare importo - Exchange rate Tasso di cambio @@ -2068,8 +2060,9 @@ fallito Ethereum - Ethereum node - Nodo Ethereum + Ethereum + Ethereum node + Nodo Ethereum Ethereum seed phrase @@ -2217,10 +2210,6 @@ Please try again later or create an offer yourself. Al momento non ci sono offerte attive. Riprova più tardi o crea un'offerta. - - Send - Invia - All Tutti @@ -3584,10 +3573,6 @@ Riavviare il wallet e riprovare. updating in aggiornamento - - Receive - Ricevere - Rate Vota @@ -3954,5 +3939,142 @@ Collega il tuo wallet Hardware per finalizzare la transazione. Couldn't extract the DApp files. Impossibile estrarre i file della DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/ja_JP.ts b/ui/i18n/ja_JP.ts index 56e5289f9..fe4c014e9 100644 --- a/ui/i18n/ja_JP.ts +++ b/ui/i18n/ja_JP.ts @@ -913,14 +913,6 @@ Password パスワード - - Receive amount - 受取り金額 - - - Send amount - 送金額 - Exchange rate 交換レート @@ -2048,8 +2040,9 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node - Ethereumノード + Ethereum + Ethereum node + Ethereumノード Ethereum seed phrase @@ -2195,10 +2188,6 @@ Please try again later or create an offer yourself. 現在アクティブなオファーはありません。 後でもう一度試すか、自分でオファーを作成してください。 - - Send - 送信 - All すべて @@ -3553,10 +3542,6 @@ Please, restart the wallet and try again. updating 更新中 - - Receive - 受信 - Rate レート @@ -3923,5 +3908,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. DAppファイルを展開できませんでした。 + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + トークン + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/ko_KR.ts b/ui/i18n/ko_KR.ts index 9569ee1b5..9d7a6d0a5 100644 --- a/ui/i18n/ko_KR.ts +++ b/ui/i18n/ko_KR.ts @@ -914,14 +914,6 @@ Password 비밀번호 - - Receive amount - 송금 받기 - - - Send amount - 송금하기 - Exchange rate 환율 @@ -2062,7 +2054,8 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2211,10 +2204,6 @@ Please try again later or create an offer yourself. 활성화된 오퍼가 현재 없습니다. 잠시 후 재시도하거나 오퍼를 만드십시오. - - Send - 보내기 - All 전부 @@ -3576,10 +3565,6 @@ Please, restart the wallet and try again. updating 업데이트 중 - - Receive - 받기 - Rate Rate @@ -3946,5 +3931,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. DApp 파일의 압축을 풀 수 없습니다. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/nl_NL.ts b/ui/i18n/nl_NL.ts index 582c60ed7..4df888361 100644 --- a/ui/i18n/nl_NL.ts +++ b/ui/i18n/nl_NL.ts @@ -916,14 +916,6 @@ Password Wachtwoord - - Receive amount - Ontvang bedrag - - - Send amount - Verzend bedrag - Exchange rate Wisselkoers @@ -2066,8 +2058,9 @@ Uw versie is: %2. Controleer op updates. Ethereum - Ethereum node - Ethereum node + Ethereum + Ethereum node + Ethereum node Ethereum seed phrase @@ -2214,10 +2207,6 @@ Please try again later or create an offer yourself. Er zijn momenteel geen actieve aanbiedingen. Probeer het later opnieuw of maak zelf een aanbieding. - - Send - Verzend - All Alle @@ -3581,10 +3570,6 @@ Herstart de wallet en probeer het opnieuw. updating bijwerken - - Receive - Ontvang - Rate Rate @@ -3951,5 +3936,142 @@ Verbind uw Hardware Wallet om de transactie af te ronden. Couldn't extract the DApp files. Kan de DApp-bestanden niet uitpakken. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/pt_BR.ts b/ui/i18n/pt_BR.ts index fe2bda7bf..24087b306 100644 --- a/ui/i18n/pt_BR.ts +++ b/ui/i18n/pt_BR.ts @@ -916,14 +916,6 @@ Password Senha - - Receive amount - Receber valor - - - Send amount - Enviar valor - Exchange rate Taxa de Câmbio @@ -2067,8 +2059,9 @@ Sua versão é: %2. Por favor, verifique se há atualizações. Ethereum - Ethereum node - Nó Ethereum + Ethereum + Ethereum node + Nó Ethereum Ethereum seed phrase @@ -2215,10 +2208,6 @@ Please try again later or create an offer yourself. Não há ofertas ativas no momento. Por favor, tente novamente mais tarde ou crie uma oferta. - - Send - Mandar - All Tudo @@ -3583,10 +3572,6 @@ Por favor, reinicie a carteira e tente novamente. updating atualizando - - Receive - Receber - Rate Taxa @@ -3953,5 +3938,142 @@ Conecte a sua Carteira Física para finalizar a transação. Couldn't extract the DApp files. Não foi possível extrair os arquivos da DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/ru_RU.ts b/ui/i18n/ru_RU.ts index 9ca0e607a..7c85d0ffa 100644 --- a/ui/i18n/ru_RU.ts +++ b/ui/i18n/ru_RU.ts @@ -918,14 +918,6 @@ Password Пароль - - Receive amount - Сумма к получению - - - Send amount - Отправляемая сумма - Exchange rate Обменный курс @@ -2075,8 +2067,9 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node - Узел Ethereum + Ethereum + Ethereum node + Узел Ethereum Ethereum seed phrase @@ -2224,10 +2217,6 @@ Please try again later or create an offer yourself. В данный момент нет активных предложений. Пожалуйста, повторите попытку позже или создайте предложение самостоятельно. - - Send - Отправить - All Все @@ -3597,10 +3586,6 @@ Please, restart the wallet and try again. updating обновление - - Receive - Получить - Rate Курс @@ -3967,5 +3952,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Не удалось распаковать файлы DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Токен + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/sr_SR.ts b/ui/i18n/sr_SR.ts index f67cee1b8..3e88b04dd 100644 --- a/ui/i18n/sr_SR.ts +++ b/ui/i18n/sr_SR.ts @@ -924,14 +924,6 @@ Password Password - - Receive amount - Износ који примате - - - Send amount - Износ који шаљете - Exchange rate Стопа Мењачнице @@ -2081,7 +2073,8 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2231,10 +2224,6 @@ Please try again later or create an offer yourself. Тренутно нема активних понуда. Молим проверите поново касније или сами креирајте понуду. - - Send - Send - All All @@ -3601,10 +3590,6 @@ Please, restart the wallet and try again. updating ажурирање - - Receive - Receive - Rate Курс @@ -3978,5 +3963,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Nije moguće raspakovati DApp datoteke. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/sv_SE.ts b/ui/i18n/sv_SE.ts index ed16eee7f..2709ca3be 100644 --- a/ui/i18n/sv_SE.ts +++ b/ui/i18n/sv_SE.ts @@ -915,14 +915,6 @@ Password Lösenord - - Receive amount - Ta emot belopp - - - Send amount - Skicka belopp - Exchange rate Växelkurs @@ -2064,7 +2056,8 @@ Din version är: %2. Sök efter uppdateringar. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2213,10 +2206,6 @@ Please try again later or create an offer yourself. Det finns inga aktiva erbjudanden för tillfället. Vänligen testa igen senare eller skapa ett erbjudande själv. - - Send - Skicka - All Alla @@ -3582,10 +3571,6 @@ Please, restart the wallet and try again. updating uppdaterar - - Receive - Ta emot - Rate Kurs @@ -3952,5 +3937,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Det gick inte att extrahera DApp-filerna. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/th_TH.ts b/ui/i18n/th_TH.ts index f83846e20..d52b480d4 100644 --- a/ui/i18n/th_TH.ts +++ b/ui/i18n/th_TH.ts @@ -914,14 +914,6 @@ Password รหัสผ่าน - - Receive amount - จำนวนที่ได้รับ - - - Send amount - จำนวนที่โอน - Exchange rate อัตราแลกเปลี่ยน @@ -2063,7 +2055,8 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2212,10 +2205,6 @@ Please try again later or create an offer yourself. ไม่มีข้อเสนอการแลกเปลี่ยนใดๆในตอนนี้ กรุณาลองอีกครั้งหรือสร้างข้อเสนอของตัวเองเลย - - Send - โอน - All ทั้งหมด @@ -3576,10 +3565,6 @@ Please, restart the wallet and try again. updating กำลังอัพเดท - - Receive - รับ - Rate เรท @@ -3946,5 +3931,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. ไม่สามารถแตกไฟล์ DApp ได้ + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + โทเค็น + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/tr_TR.ts b/ui/i18n/tr_TR.ts index 844c594c1..4f0bf1915 100644 --- a/ui/i18n/tr_TR.ts +++ b/ui/i18n/tr_TR.ts @@ -914,14 +914,6 @@ Password Şifre - - Receive amount - Receive amount - - - Send amount - Send amount - Exchange rate Exchange rate @@ -2062,7 +2054,8 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2211,10 +2204,6 @@ Please try again later or create an offer yourself. There are no active offers at the moment. Please try again later or create an offer yourself. - - Send - Gönder - All Tümü @@ -3576,10 +3565,6 @@ Please, restart the wallet and try again. updating güncelleniyor - - Receive - Al - Rate Rate @@ -3946,5 +3931,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. DApp dosyaları çıkarılamadı. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/uk_UA.ts b/ui/i18n/uk_UA.ts index 765407a68..37ae962d1 100644 --- a/ui/i18n/uk_UA.ts +++ b/ui/i18n/uk_UA.ts @@ -918,14 +918,6 @@ Password Пароль - - Receive amount - Отримати суму - - - Send amount - Відправити суму - Exchange rate Обмінний курс @@ -2072,8 +2064,9 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node - Ethereum вузол + Ethereum + Ethereum node + Ethereum вузол Ethereum seed phrase @@ -2221,10 +2214,6 @@ Please try again later or create an offer yourself. На цей час немає активних пропозицій. Спробуйте пізніше або створіть пропозицію самостійно. - - Send - Надіслати - All Всі @@ -3592,10 +3581,6 @@ Please, restart the wallet and try again. updating оновлення - - Receive - Отримати - Rate Оцінити @@ -3962,5 +3947,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Не вдалося розпакувати файли DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Токен + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/vi_VI.ts b/ui/i18n/vi_VI.ts index b264d366b..454cf185d 100644 --- a/ui/i18n/vi_VI.ts +++ b/ui/i18n/vi_VI.ts @@ -920,14 +920,6 @@ Password Mật khẩu - - Receive amount - Nhận số tiền - - - Send amount - Send amount - Exchange rate Exchange rate @@ -2070,7 +2062,8 @@ Your version is: %2. Please, check for updates. Ethereum - Ethereum node + Ethereum + Ethereum node Ethereum node @@ -2220,10 +2213,6 @@ Please try again later or create an offer yourself. There are no active offers at the moment. Please try again later or create an offer yourself. - - Send - Gửi - All Tất cả @@ -3582,10 +3571,6 @@ Please, restart the wallet and try again. updating đang cập nhật - - Receive - Nhận - Rate Đánh giá @@ -3959,5 +3944,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. Không thể giải nén các tệp DApp. + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + Token + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/i18n/zh_CN.ts b/ui/i18n/zh_CN.ts index c949a8311..ad3082112 100644 --- a/ui/i18n/zh_CN.ts +++ b/ui/i18n/zh_CN.ts @@ -922,14 +922,6 @@ Password 密码 - - Receive amount - 收款金额 - - - Send amount - 发送金额 - Exchange rate 汇率 @@ -2078,8 +2070,9 @@ Your version is: %2. Please, check for updates. 以太坊 - Ethereum node - 以太坊节点 + Ethereum + Ethereum node + 以太坊节点 Ethereum seed phrase @@ -2227,10 +2220,6 @@ Please try again later or create an offer yourself. 目前没有活动的报价。 请稍后再试或自己创建报价。 - - Send - 付款 - All 所有 @@ -3592,10 +3581,6 @@ Please, restart the wallet and try again. updating 正在更新 - - Receive - 收款 - Rate 评价 @@ -3962,5 +3947,142 @@ Connect your Hardware Wallet to finalize the transaction. Couldn't extract the DApp files. 无法解压 DApp 文件。 + + I send + + + + I receive + + + + While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most. + + + + Infura + + + + Custom RPC + + + + Ethereum RPC endpoint + + + + Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node. + Supports Infura, Alchemy, QuickNode, Chainstack, Ankr, or your own Ethereum node. + + + + Check connection + + + + Unable to connect + + + + Ethereum Mainnet + + + + Sepolia Testnet + + + + chain %1 + + + + Connected to %1. Latest block: %2 + + + + Connected to %1, but Ethereum Mainnet is required + + + + Custom ERC-20 tokens + + + + remove + + + + 0x contract address + + + + Look up + + + + Add + + + + %1, %2 decimals + + + + Token + 令牌 + + + Contract address + + + + Symbol / decimals + + + + Verify this token contract address carefully. Anyone can create a token with any name. + + + + Confidential Asset + + + + Asset id %1, unit %2 + + + + You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee. + + + + %1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name. + + + + Invalid contract address + + + + built-in + + + + + Add token + + + + cancel + + + + This token is already supported + + + + Not enough ETH to pay the redeem transaction fee (%1 needed) + + diff --git a/ui/model/app_model.cpp b/ui/model/app_model.cpp index 970f3fc71..3decc6614 100644 --- a/ui/model/app_model.cpp +++ b/ui/model/app_model.cpp @@ -557,6 +557,9 @@ void AppModel::startWallet() swapTransactionCreator->RegisterFactory(AtomicSwapCoin::Dai, secondSideFactory); swapTransactionCreator->RegisterFactory(AtomicSwapCoin::Usdt, secondSideFactory); swapTransactionCreator->RegisterFactory(AtomicSwapCoin::WBTC, secondSideFactory); + // per-offer custom ERC-20 tokens: EthereumSide resolves the contract and + // decimals from the tx params (AtomicSwapTokenContract/Decimals) + swapTransactionCreator->RegisterFactory(AtomicSwapCoin::Erc20Token, secondSideFactory); } additionalTxCreators->emplace(TxType::AtomicSwap, swapTransactionCreator); diff --git a/ui/model/settings.cpp b/ui/model/settings.cpp index 206de155a..9a0eba49d 100644 --- a/ui/model/settings.cpp +++ b/ui/model/settings.cpp @@ -44,6 +44,7 @@ namespace const char* kIsAlowedBeamMWLink = "beam_mw_links_allowed"; const char* kRateUnit = "rateUnit"; const char* kLastAssetSelection = "lastAssetSelection"; + const char* kEthCustomTokens = "eth/customTokens"; const char* kHideSeedValidationPromo = "hideSeedValidationPromo"; const char* kDevMode ="dev_mode"; @@ -1179,6 +1180,43 @@ void WalletSettings::setLastAssetSelection(QVector selection) m_accountSettings.m_data.setValue(kLastAssetSelection, QVariant::fromValue(ser)); } +QList> WalletSettings::getEthCustomTokens() const +{ + Lock lock(m_mutex); + + auto ser = m_accountSettings.m_data.value(kEthCustomTokens).value(); + QDataStream in(&ser, QIODevice::ReadOnly); + QVariantList res; + in >> res; + + QList> result; + for (const auto& entry : res) + { + result.push_back(entry.toMap()); + } + return result; +} + +void WalletSettings::setEthCustomTokens(const QList>& tokens) +{ + { + Lock lock(m_mutex); + + QVariantList toStore; + for (const auto& token : tokens) + { + toStore.push_back(token); + } + + QByteArray ser; + QDataStream out(&ser, QIODevice::WriteOnly); + out << toStore; + + m_accountSettings.m_data.setValue(kEthCustomTokens, QVariant::fromValue(ser)); + } + emit ethCustomTokensChanged(); +} + bool WalletSettings::getShowInProgress() const { Lock lock(m_mutex); diff --git a/ui/model/settings.h b/ui/model/settings.h index 850b3e4ad..00b3869de 100644 --- a/ui/model/settings.h +++ b/ui/model/settings.h @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include "model/wallet_model.h" @@ -134,6 +136,11 @@ class WalletSettings : public QObject void removeAllowedAsset(beam::Asset::ID asset); #endif // BEAM_ASSET_SWAP_SUPPORT + // per-wallet custom ERC-20 tokens added for atomic-swap offers + // (beam::ethereum::Settings has no free-form storage for this) + QList> getEthCustomTokens() const; + void setEthCustomTokens(const QList>& tokens); + uint8_t getMaxPrivacyAnonymitySet() const; void setMaxPrivacyAnonymitySet(uint8_t anonymitySet); @@ -207,6 +214,7 @@ class WalletSettings : public QObject void secondCurrencyChanged(); void IPFSSettingsChanged(); void generalMouseEvent(); + void ethCustomTokensChanged(); private: struct AccountSettings diff --git a/ui/model/swap_eth_client_model.cpp b/ui/model/swap_eth_client_model.cpp index fc2689141..84871c3ec 100644 --- a/ui/model/swap_eth_client_model.cpp +++ b/ui/model/swap_eth_client_model.cpp @@ -49,6 +49,7 @@ SwapEthClientModel::SwapEthClientModel(beam::ethereum::IBridgeHolder::Ptr bridge connect(this, SIGNAL(gotStatus(beam::ethereum::Client::Status)), this, SLOT(setStatus(beam::ethereum::Client::Status))); connect(this, SIGNAL(gotCanModifySettings(bool)), this, SLOT(setCanModifySettings(bool))); connect(this, SIGNAL(gotConnectionError(beam::ethereum::IBridge::ErrorType)), this, SLOT(setConnectionError(beam::ethereum::IBridge::ErrorType))); + connect(this, &SwapEthClientModel::gotTokenBalance, this, &SwapEthClientModel::setTokenBalance); requestBalance(); requestEstimatedFeeRate(); @@ -94,6 +95,11 @@ beam::ethereum::IBridge::ErrorType SwapEthClientModel::getConnectionError() cons return m_connectionError; } +void SwapEthClientModel::validateEndpoint() +{ + GetAsync()->ValidateEndpoint(); +} + void SwapEthClientModel::OnBalance(wallet::AtomicSwapCoin swapCoin, Amount balance) { emit gotBalance(swapCoin, balance); @@ -120,6 +126,14 @@ void SwapEthClientModel::OnConnectionError(beam::ethereum::IBridge::ErrorType er emit gotConnectionError(error); } +void SwapEthClientModel::OnEndpointValidated(uint64_t chainID, uint64_t blockNumber, const beam::ethereum::IBridge::Error& error) +{ + emit endpointValidated(chainID, blockNumber, + error.m_type == beam::ethereum::IBridge::None, + error.m_type == beam::ethereum::IBridge::InvalidNetwork, + QString::fromStdString(error.m_message)); +} + void SwapEthClientModel::requestBalance() { if (GetSettings().IsActivated()) @@ -131,6 +145,17 @@ void SwapEthClientModel::requestBalance() { GetAsync()->GetBalance(token); } + + // same cadence, for user-stored custom ERC-20 tokens + for (const auto& token : AppModel::getInstance().getSettings().getEthCustomTokens()) + { + const auto contract = token.value("contract").toString().toStdString(); + const auto decimals = static_cast(token.value("decimals").toUInt()); + if (!contract.empty()) + { + GetAsync()->GetTokenBalance(contract, decimals); + } + } } } @@ -186,6 +211,28 @@ void SwapEthClientModel::setCanModifySettings(bool canModify) } } +void SwapEthClientModel::requestTokenInfo(const std::string& contractAddress) +{ + GetAsync()->GetTokenInfo(contractAddress); +} + +void SwapEthClientModel::OnTokenInfo(const std::string& tokenContract, const std::string& symbol, uint8_t decimals, const beam::ethereum::IBridge::Error& error) +{ + QString errorStr; + if (error.m_type != beam::ethereum::IBridge::None) + { + errorStr = error.m_message.empty() + //% "Cannot connect to node. Please check your network connection." + ? qtTrId("swap-connection-error") + : QString::fromStdString(error.m_message); + } + + emit gotTokenInfo(QString::fromStdString(tokenContract), + QString::fromStdString(symbol), + decimals, + errorStr); +} + void SwapEthClientModel::setConnectionError(beam::ethereum::IBridge::ErrorType error) { if (m_connectionError != error) @@ -194,3 +241,30 @@ void SwapEthClientModel::setConnectionError(beam::ethereum::IBridge::ErrorType e emit connectionErrorChanged(); } } + +void SwapEthClientModel::OnTokenBalance(const std::string& tokenContract, beam::Amount balance) +{ + emit gotTokenBalance(QString::fromStdString(tokenContract), balance); +} + +beam::Amount SwapEthClientModel::getTokenBalance(const QString& contract) const +{ + auto it = m_tokenBalances.find(contract.toLower()); + return it != m_tokenBalances.end() ? it->second : 0; +} + +void SwapEthClientModel::setTokenBalance(const QString& contract, beam::Amount balance) +{ + auto key = contract.toLower(); + auto it = m_tokenBalances.find(key); + if (it == m_tokenBalances.end()) + { + m_tokenBalances.emplace(key, balance); + emit tokenBalancesChanged(); + } + else if (it->second != balance) + { + it->second = balance; + emit tokenBalancesChanged(); + } +} diff --git a/ui/model/swap_eth_client_model.h b/ui/model/swap_eth_client_model.h index e19b7a108..eb1cd7c66 100644 --- a/ui/model/swap_eth_client_model.h +++ b/ui/model/swap_eth_client_model.h @@ -35,6 +35,16 @@ class SwapEthClientModel beam::ethereum::Client::Status getStatus() const; bool canModifySettings() const; beam::ethereum::IBridge::ErrorType getConnectionError() const; + void validateEndpoint(); + + // symbol()/decimals() of an arbitrary ERC-20 contract (per-offer custom + // tokens have no static settings entry, unlike kEthTokens); reports back + // through gotTokenInfo + void requestTokenInfo(const std::string& contractAddress); + + // wallet-units balance (already normalized to min(decimals,9) by Client::GetTokenBalance) + // of a stored custom ERC-20 token, or 0 if not fetched yet + beam::Amount getTokenBalance(const QString& contract) const; signals: void gotStatus(beam::ethereum::Client::Status status); @@ -42,12 +52,16 @@ class SwapEthClientModel void gotEstimatedGasPrice(beam::Amount estimatedFeeRate); void gotCanModifySettings(bool canModify); void gotConnectionError(const beam::ethereum::IBridge::ErrorType& error); + void gotTokenInfo(const QString& contract, const QString& symbol, uint decimals, const QString& error); + void gotTokenBalance(const QString& contract, beam::Amount balance); void canModifySettingsChanged(); void balanceChanged(); + void tokenBalancesChanged(); void estimatedFeeRateChanged(); void statusChanged(); void connectionErrorChanged(); + void endpointValidated(quint64 chainID, quint64 blockNumber, bool ok, bool wrongNetwork, const QString& errorMsg); private: void OnStatus(Status status) override; @@ -56,6 +70,9 @@ class SwapEthClientModel void OnCanModifySettingsChanged(bool canModify) override; void OnChangedSettings() override; void OnConnectionError(beam::ethereum::IBridge::ErrorType error) override; + void OnEndpointValidated(uint64_t chainID, uint64_t blockNumber, const beam::ethereum::IBridge::Error& error) override; + void OnTokenBalance(const std::string& tokenContract, beam::Amount balance) override; + void OnTokenInfo(const std::string& tokenContract, const std::string& symbol, uint8_t decimals, const beam::ethereum::IBridge::Error& error) override; private slots: void requestBalance(); @@ -65,13 +82,16 @@ private slots: void setStatus(beam::ethereum::Client::Status status); void setCanModifySettings(bool canModify); void setConnectionError(beam::ethereum::IBridge::ErrorType error); + void setTokenBalance(const QString& contract, beam::Amount balance); private: QTimer m_balanceTimer; QTimer m_feeRateTimer; std::map m_balances; + std::map m_tokenBalances; // key: lowercased contract address beam::Amount m_gasPrice = 0; Status m_status = Status::Unknown; bool m_canModifySettings = true; beam::ethereum::IBridge::ErrorType m_connectionError = beam::ethereum::IBridge::ErrorType::None; + }; \ No newline at end of file diff --git a/ui/view/Settings.qml b/ui/view/Settings.qml index 0aa009e70..5421a5534 100644 --- a/ui/view/Settings.qml +++ b/ui/view/Settings.qml @@ -227,7 +227,7 @@ ColumnLayout { connectionError: modelData.connectionError getAddressesElectrum: modelData.getAddressesElectrum folded: creating ? modelData.folded : - (unfoldSection == modelData.coinID ? false : (unfoldSection == "ALL_COINS" ? modelData.isConnected : true)) + (unfoldSection == modelData.coinID ? false : true) mainSettingsViewModel: viewModel @@ -376,6 +376,7 @@ ColumnLayout { SwapEthSettings { id: swapEthSettings + ethSettings: viewModel.ethSettings title: viewModel.ethSettings.title generalTitle: viewModel.ethSettings.generalTitle showSeedDialogTitle: viewModel.ethSettings.showSeedDialogTitle @@ -386,13 +387,15 @@ ColumnLayout { mainSettingsViewModel: viewModel showStatus: true getEthereumAddresses: viewModel.ethSettings.getEthereumAddresses - folded: creating ? (unfoldSection == viewModel.ethSettings.coinID ? false : (unfoldSection == "ALL_COINS" ? viewModel.ethSettings.isConnected : true)) : viewModel.ethSettings.folded + folded: creating ? (unfoldSection == viewModel.ethSettings.coinID ? false : true) : viewModel.ethSettings.folded canChangeConnection: viewModel.ethSettings.canChangeConnection isConnected: viewModel.ethSettings.isConnected connectionStatus: viewModel.ethSettings.connectionStatus connectionError: viewModel.ethSettings.connectionError infuraProjectID: viewModel.ethSettings.infuraProjectID accountIndex: viewModel.ethSettings.accountIndex + useCustomRpc: viewModel.ethSettings.useCustomRpc + customRpcUrl: viewModel.ethSettings.customRpcUrl Connections { target: viewModel.ethSettings @@ -422,6 +425,14 @@ ColumnLayout { swapEthSettings.accountIndex = viewModel.ethSettings.accountIndex } + function onUseCustomRpcChanged () { + swapEthSettings.useCustomRpc = viewModel.ethSettings.useCustomRpc + } + + function onCustomRpcUrlChanged () { + swapEthSettings.customRpcUrl = viewModel.ethSettings.customRpcUrl + } + function onSeedPhrasesChanged () { swapEthSettings.seedPhrases = viewModel.ethSettings.seedPhrases } @@ -439,6 +450,7 @@ ColumnLayout { onRestoreSeedPhrases: viewModel.ethSettings.restoreSeedPhrases() onCopySeedPhrases: viewModel.ethSettings.copySeedPhrases() onValidateCurrentSeedPhrase: viewModel.ethSettings.validateCurrentSeedPhrase() + onValidateEndpoint: viewModel.ethSettings.validateEndpoint() Binding { target: viewModel.ethSettings @@ -457,6 +469,18 @@ ColumnLayout { property: "accountIndex" value: swapEthSettings.accountIndex } + + Binding { + target: viewModel.ethSettings + property: "useCustomRpc" + value: swapEthSettings.useCustomRpc + } + + Binding { + target: viewModel.ethSettings + property: "customRpcUrl" + value: swapEthSettings.customRpcUrl + } } } } diff --git a/ui/view/accept_asset_swap.qml b/ui/view/accept_asset_swap.qml index ab3b668a4..2a0efc6f8 100644 --- a/ui/view/accept_asset_swap.qml +++ b/ui/view/accept_asset_swap.qml @@ -58,8 +58,8 @@ ColumnLayout { // Send amount // Panel { - //% "Send amount" - title: qsTrId("sent-amount-label") + //% "I send" + title: qsTrId("atomic-swap-i-send") Layout.fillWidth: true content: @@ -108,8 +108,8 @@ ColumnLayout { // Receive amount // Panel { - //% "Receive amount" - title: qsTrId("receive-amount-swap-label") + //% "I receive" + title: qsTrId("atomic-swap-i-receive") Layout.fillWidth: true content: diff --git a/ui/view/assets_swap.qml b/ui/view/assets_swap.qml index 3214d739c..f95c23635 100644 --- a/ui/view/assets_swap.qml +++ b/ui/view/assets_swap.qml @@ -256,8 +256,8 @@ ColumnLayout { TableViewColumn { role: "send" - //% "Send" - title: qsTrId("general-send") + //% "I send" + title: qsTrId("atomic-swap-i-send") width: 150 * ordersTable.columnResizeRatio movable: false resizable: false @@ -283,8 +283,8 @@ ColumnLayout { TableViewColumn { role: "receive" - //% "Receive" - title: qsTrId("general-receive") + //% "I receive" + title: qsTrId("atomic-swap-i-receive") width: 150 * ordersTable.columnResizeRatio movable: false resizable: false diff --git a/ui/view/atomic_swap.qml b/ui/view/atomic_swap.qml index 9afea26d6..8b4bad9f4 100644 --- a/ui/view/atomic_swap.qml +++ b/ui/view/atomic_swap.qml @@ -223,6 +223,30 @@ ColumnLayout { } } + // one card per stored custom ERC-20 token, live balance + // shown only while the eth client is connected - matches the DAI/USDT/WBTC rule + Repeater { + model: viewModel.customTokenCards + + SwapCurrencyAmountPane { + gradLeft: modelData.color + amount: modelData.available + unitName: modelData.symbol + isOk: modelData.isConnected + swapSettingsPane: "ETH" + //% "Connecting..." + textConnecting: qsTrId("swap-connecting") + //% "Cannot connect to peer. Please check the address in Settings and try again." + textConnectionError: qsTrId("swap-beta-connection-error") + iconDelegate: Component { + TokenIcon { + tokenColor: modelData.color + symbol: modelData.symbol + } + } + } + } + Rectangle { id: swapOptions Layout.fillWidth: true @@ -413,22 +437,26 @@ ColumnLayout { fontLetterSpacing: 0.47 color: Style.content_main textRole: 'text' - model: [ - { - //% "(all)" - text: qsTrId("atomic-swap-all-coins"), - pair: "" - }, - {text: "BTC", pair: "^(btc-)|(-btc)$"}, // We need a separator '-' to distinguish 'btc' and 'wbtc' - {text: "DAI", pair: "^(dai-)|(-dai)$"}, - {text: "DASH", pair: "^(dash-)|(-dash)$"}, - {text: "DOGE", pair: "^(doge-)|(-doge)$"}, - {text: "ETH", pair: "^(eth-)|(-eth)$"}, - {text: "LTC", pair: "^(ltc-)|(-ltc)$"}, - {text: "QTUM", pair: "^(qtum-)|(-qtum)$"}, - {text: "USDT", pair: "^(usdt-)|(-usdt)$"}, - {text: "WBTC", pair: "^(wbtc-)|(-wbtc)$"} - ] + // fixed classic coins + custom ERC-20 tokens + held CA's, + // built on the C++ side (viewModel.coinFilterList) so the + // combo stays in sync with settings/wallet changes + model: viewModel.coinFilterList + + // regenerating the model resets currentIndex: restore the + // selection by value (its filter pair), or fall back to "(all)" + property string selectedPair: "" + onActivated: selectedPair = model[currentIndex] ? model[currentIndex].pair : "" + onModelChanged: { + if (!model) return + for (var i = 0; i < model.length; ++i) { + if (model[i].pair === selectedPair) { + if (currentIndex !== i) currentIndex = i + return + } + } + currentIndex = 0 + selectedPair = "" + } } } // RowLayout @@ -530,7 +558,7 @@ Please try again later or create an offer yourself." ? viewModel.allOffersFitBalance : viewModel.allOffers filterRole: "pair" - filterString: coinSelector.model[coinSelector.currentIndex].pair + filterString: coinSelector.model[coinSelector.currentIndex] ? coinSelector.model[coinSelector.currentIndex].pair : "" filterCaseSensitivity: Qt.CaseInsensitive } @@ -610,8 +638,8 @@ Please try again later or create an offer yourself." TableViewColumn { role: "amountSend" - //% "Send" - title: qsTrId("atomic-swap-amount-send") + //% "I send" + title: qsTrId("atomic-swap-i-send") width: offersTable.columnWidth movable: false resizable: false @@ -625,8 +653,8 @@ Please try again later or create an offer yourself." TableViewColumn { role: "amountReceive" - //% "Receive" - title: qsTrId("general-receive") + //% "I receive" + title: qsTrId("atomic-swap-i-receive") width: offersTable.columnWidth movable: false resizable: false @@ -881,6 +909,7 @@ Please try again later or create an offer yourself." beamRedeemTxKernelId: txRolesMap && txRolesMap.beamRedeemTxKernelId ? txRolesMap.beamRedeemTxKernelId : "" beamRefundTxKernelId: txRolesMap && txRolesMap.beamRefundTxKernelId ? txRolesMap.beamRefundTxKernelId : "" stateDetails: txRolesMap && txRolesMap.stateDetails ? txRolesMap.stateDetails : "" + coinsUnlockNote: txRolesMap && txRolesMap.coinsUnlockNote ? txRolesMap.coinsUnlockNote : "" failureReason: txRolesMap && txRolesMap.failureReason ? txRolesMap.failureReason : "" onTextCopied: function (text) { diff --git a/ui/view/controls/AmountInput.qml b/ui/view/controls/AmountInput.qml index 5bb4bc884..0ac5b061e 100644 --- a/ui/view/controls/AmountInput.qml +++ b/ui/view/controls/AmountInput.qml @@ -97,8 +97,10 @@ ColumnLayout { // Allow integers up to 12 digits (max safe for uint64 with 8 decimals). // Exact overflow protection is handled by the C++ backend (decode_base10) // and the isEnoughAmount / balance validation. + var decimals = currencies[currencyIdx].decimals + if (decimals < 1) return "^([1-9][0-9]{0,11}|0)$"; // e.g. 0-decimals ERC-20 tokens var pattern = "^([1-9][0-9]{0,11}|0)(\\.[0-9]{0,%1}[1-9])?$"; - return pattern.arg(currencies[currencyIdx].decimals - 1); + return pattern.arg(decimals - 1); } Connections { diff --git a/ui/view/controls/FeeInput.qml b/ui/view/controls/FeeInput.qml index a068939c9..b65c784cc 100644 --- a/ui/view/controls/FeeInput.qml +++ b/ui/view/controls/FeeInput.qml @@ -27,6 +27,14 @@ ColumnLayout { property string rateUnit: "" property string minimumFeeNotificationText: "" + // the recommended rate arrives asynchronously; adopt it if no fee is set + // yet and the user is not editing the field + onRecommendedFeeChanged: { + if (control.fee === 0 && control.recommendedFee > 0 && !control.readOnly && !feeInput.activeFocus) { + control.fee = BeamGlobals.getDefaultFee(control.currency) + } + } + RowLayout { Layout.fillWidth: true @@ -75,16 +83,13 @@ ColumnLayout { } } - Item { - x: feeInput.width - feeCurrencyLabel.width - 8 - y: 6 - SFText { - id: feeCurrencyLabel - font.pixelSize: 14 - color: isValid ? Style.content_main : Style.validator_error - text: control.feeLabel - visible: (control.feeLabel || "").length - } + SFText { + id: feeCurrencyLabel + Layout.alignment: Qt.AlignVCenter + font.pixelSize: 14 + color: isValid ? Style.content_main : Style.validator_error + text: control.feeLabel + visible: (control.feeLabel || "").length } Item { diff --git a/ui/view/controls/SwapCurrencyAmountPane.qml b/ui/view/controls/SwapCurrencyAmountPane.qml index b428e0b5c..85773fb38 100644 --- a/ui/view/controls/SwapCurrencyAmountPane.qml +++ b/ui/view/controls/SwapCurrencyAmountPane.qml @@ -26,6 +26,9 @@ Rectangle { property alias amountWrapMode: amountField.wrapMode property var onClick: function() {} property string swapSettingsPane: "" + // custom-token cards have no static svg logo (item 4): a locally-generated + // colored-circle+letter icon is supplied here instead of currencyIcon + property Component iconDelegate: null id: control Layout.fillWidth: true @@ -60,7 +63,14 @@ Rectangle { source: currencyIcon width: 26 height: 26 - visible: currencyIcon.length + visible: currencyIcon.length && !control.iconDelegate + } + + Loader { + anchors.verticalCenter: parent.verticalCenter + active: control.iconDelegate !== null + visible: active + sourceComponent: control.iconDelegate } Repeater { @@ -135,7 +145,7 @@ Rectangle { id: clickArea anchors.fill: parent acceptedButtons: Qt.LeftButton - onClicked: main.openSwapSettings(swapSettingsPane) + onClicked: main.openSettings(swapSettingsPane) hoverEnabled: true onPositionChanged: clickArea.cursorShape = Qt.PointingHandCursor; } diff --git a/ui/view/controls/SwapEthSettings.qml b/ui/view/controls/SwapEthSettings.qml index 2840ddcef..1c69c632e 100644 --- a/ui/view/controls/SwapEthSettings.qml +++ b/ui/view/controls/SwapEthSettings.qml @@ -7,7 +7,6 @@ import "../utils.js" as Utils SettingsFoldable { id: control - height: 362 property string generalTitle: "" property alias showSeedDialogTitle: seedPhraseDialog.showSeedDialogTitle @@ -22,11 +21,46 @@ SettingsFoldable { property alias infuraProjectID: infuraProjectIDInput.text property alias accountIndex: accountIndexInput.text + property alias useCustomRpc: useCustomRpcSwitch.checked + property alias customRpcUrl: customRpcUrlInput.text property alias seedPhrases: seedPhraseDialog.seedPhrasesElectrum property alias phrasesSeparator: seedPhraseDialog.phrasesSeparatorElectrum property bool isCurrentSeedValid: false + // read-only feedback from the last "check connection" run; bound straight + // to the viewmodel so no push-mirroring is needed + readonly property string endpointCheckResult: ethSettings ? ethSettings.endpointCheckResult : "" + readonly property bool endpointCheckOk: ethSettings ? !!ethSettings.endpointCheckOk : false + + // backing SwapEthSettingsItem, used directly for the custom-token flow + // rather than mirroring every field through property aliases + Connections + property var ethSettings: undefined + property string newTokenSymbol: "" + property int newTokenDecimals: 0 + property string newTokenError: "" + + function resetTokenLookup() { + newTokenSymbol = "" + newTokenDecimals = 0 + newTokenError = "" + } + onUseCustomRpcChanged: resetTokenLookup() + onCustomRpcUrlChanged: resetTokenLookup() + onInfuraProjectIDChanged: resetTokenLookup() + // collapsing the section should not leave the token input focused/open underneath + onFoldedChanged: if (control.folded) addTokenPane.hide() + + Connections { + target: ethSettings + function onTokenInfoReady(contract, symbol, decimals, error) { + if (contract !== newTokenAddress.text.trim()) return + newTokenSymbol = symbol + newTokenDecimals = decimals + newTokenError = error + } + } + // function to get ethereum addresses property var getEthereumAddresses: undefined @@ -61,12 +95,15 @@ SettingsFoldable { signal restoreSeedPhrases signal copySeedPhrases signal validateCurrentSeedPhrase + signal validateEndpoint QtObject { id: internalValues property string initialInfuraProjectID property string initialSeed property string initialAccountIndex + property bool initialUseCustomRpc + property string initialCustomRpcUrl property bool isSeedChanged: false @@ -76,16 +113,21 @@ SettingsFoldable { control.restoreSeedPhrases() accountIndex = initialAccountIndex + useCustomRpc = initialUseCustomRpc + customRpcUrl = initialCustomRpcUrl } function save() { initialInfuraProjectID = infuraProjectID isSeedChanged = false initialAccountIndex = accountIndex + initialUseCustomRpc = useCustomRpc + initialCustomRpcUrl = customRpcUrl } function isChanged() { - return initialInfuraProjectID !== infuraProjectID || isSeedChanged ||initialAccountIndex != accountIndex + return initialInfuraProjectID !== infuraProjectID || isSeedChanged || initialAccountIndex != accountIndex || + initialUseCustomRpc !== useCustomRpc || initialCustomRpcUrl !== customRpcUrl } } @@ -134,6 +176,41 @@ SettingsFoldable { content: ColumnLayout { spacing: 0 + // Infura & Custom RPC switch + RowLayout { + height: 20 + spacing: 10 + Layout.fillWidth: true + SFText { + //% "Infura" + text: qsTrId("settings-eth-infura") + color: useCustomRpcSwitch.checked ? control.color : Style.active + font.pixelSize: 14 + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: useCustomRpcSwitch.checked = !useCustomRpcSwitch.checked + } + } + CustomSwitch { + id: useCustomRpcSwitch + alwaysGreen: true + spacing: 0 + enabled: canEdit + } + SFText { + //% "Custom RPC" + text: qsTrId("settings-eth-custom-rpc") + color: useCustomRpcSwitch.checked ? Style.active : control.color + font.pixelSize: 14 + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: useCustomRpcSwitch.checked = !useCustomRpcSwitch.checked + } + } + } + GridLayout { Layout.topMargin: 20 columns: 2 @@ -141,6 +218,7 @@ SettingsFoldable { rowSpacing: 13 SFText { + visible: !useCustomRpcSwitch.checked font.pixelSize: 14 color: control.color //% "Infura project ID" @@ -149,6 +227,7 @@ SettingsFoldable { SFTextInput { id: infuraProjectIDInput + visible: !useCustomRpcSwitch.checked Layout.fillWidth: true color: Style.content_main font.pixelSize: 14 @@ -179,6 +258,38 @@ SettingsFoldable { } } + SFText { + visible: useCustomRpcSwitch.checked + Layout.topMargin: 20 + font.pixelSize: 14 + color: control.color + //% "Ethereum RPC endpoint" + text: qsTrId("settings-eth-rpc-endpoint") + } + + SFTextInput { + id: customRpcUrlInput + visible: useCustomRpcSwitch.checked + Layout.fillWidth: true + color: Style.content_main + font.pixelSize: 14 + activeFocusOnTab: true + underlineVisible: canEdit + readOnly: !canEdit + placeholderText: "https://mainnet.infura.io/v3/..." + } + + SFText { + visible: useCustomRpcSwitch.checked + Layout.fillWidth: true + Layout.topMargin: 7 + font.pixelSize: 12 + color: Style.content_secondary + wrapMode: Text.Wrap + //% "Works with any Ethereum JSON-RPC endpoint. Keyless public options: ethereum-rpc.publicnode.com, eth.drpc.org, rpc.mevblocker.io - or run your own node." + text: qsTrId("settings-eth-rpc-note") + } + // seed: new || edit RowLayout { visible: canEdit @@ -303,20 +414,30 @@ SettingsFoldable { } } - // buttons - // "cancel" "apply" - // "connect to node" or "connect to electrum" + // check connection / cancel / apply / connect / disconnect - all on one centered row RowLayout { visible: control.canChangeConnection Layout.preferredHeight: 52 Layout.fillWidth: true Layout.topMargin: 30 - spacing: 15 + spacing: 10 Item { Layout.fillWidth: true } + CustomButton { + Layout.preferredHeight: 38 + Layout.preferredWidth: 160 + leftPadding: 20 + rightPadding: 20 + //% "Check connection" + text: qsTrId("settings-eth-check-connection") + // don't allow checking the last-applied endpoint while there are unapplied edits on screen + enabled: (isConnected || canApplySettings()) && !isSettingsChanged() + onClicked: validateEndpoint() + } + CustomButton { visible: applySettingsButtonId.visible Layout.preferredHeight: 38 @@ -375,6 +496,7 @@ SettingsFoldable { enabled: isSettingsChanged() && canApplySettings() onClicked: applyChanges() Layout.preferredHeight: 38 + Layout.preferredWidth: 160 } PrimaryButton { @@ -387,12 +509,189 @@ SettingsFoldable { icon.source: "qrc:/assets/icon-done.svg" onClicked: connectToNode(); Layout.preferredHeight: 38 - Layout.preferredWidth: /*editElectrum ? 253 : */193 + Layout.preferredWidth: 160 } Item { Layout.fillWidth: true } } + + SFText { + visible: endpointCheckResult !== "" + Layout.fillWidth: true + Layout.topMargin: 10 + font.pixelSize: 14 + wrapMode: Text.Wrap + color: endpointCheckOk ? Style.active : Style.validator_error + text: (endpointCheckOk ? "✓ " : "✗ ") + endpointCheckResult + } + + // + // Custom ERC-20 tokens (used as swap-offer receive currencies) + // + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 30 + spacing: 10 + visible: control.ethSettings !== undefined && control.ethSettings !== null + + SFText { + font.pixelSize: 14 + color: control.color + //% "Custom ERC-20 tokens" + text: qsTrId("settings-swap-token-section-title") + } + + // built in - always supported natively, can't be removed + Repeater { + model: control.ethSettings ? control.ethSettings.builtinTokens : [] + delegate: RowLayout { + Layout.fillWidth: true + spacing: 10 + SvgImage { + visible: !!modelData.icon + source: modelData.icon ? modelData.icon : "" + sourceSize: Qt.size(26, 26) + } + TokenIcon { + visible: !modelData.icon + tokenColor: modelData.color + symbol: modelData.symbol + } + SFText { + Layout.fillWidth: true + font.pixelSize: 14 + color: Style.content_main + elide: Text.ElideMiddle + text: modelData.symbol + " " + modelData.contract + } + SFText { + font.pixelSize: 12 + color: control.color + //% "built-in" + text: qsTrId("settings-swap-token-builtin") + } + } + } + + // user-added + Repeater { + model: control.ethSettings ? control.ethSettings.customTokens : [] + delegate: RowLayout { + Layout.fillWidth: true + spacing: 10 + TokenIcon { + tokenColor: modelData.color + symbol: modelData.symbol + } + SFText { + Layout.fillWidth: true + font.pixelSize: 14 + color: Style.content_main + elide: Text.ElideMiddle + text: modelData.symbol + " " + modelData.contract + } + LinkButton { + //% "remove" + text: qsTrId("settings-swap-token-remove") + onClicked: control.ethSettings.removeCustomToken(modelData.contract) + } + } + } + + // "+ Add token" link; clicking it reveals the input row below + LinkButton { + visible: !addTokenPane.visible + //% "+ Add token" + text: qsTrId("settings-swap-token-add-open") + onClicked: { + addTokenPane.visible = true + newTokenAddress.forceActiveFocus() + } + } + + ColumnLayout { + id: addTokenPane + visible: false + Layout.fillWidth: true + spacing: 10 + + function hide() { + addTokenPane.visible = false + newTokenAddress.focus = false + newTokenAddress.text = "" + control.resetTokenLookup() + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + SFTextInput { + id: newTokenAddress + Layout.fillWidth: true + font.pixelSize: 14 + color: Style.content_main + underlineVisible: true + //% "0x contract address" + placeholderText: qsTrId("settings-swap-token-address-placeholder") + onTextChanged: control.resetTokenLookup() + } + + LinkButton { + //% "cancel" + text: qsTrId("settings-swap-token-add-cancel") + onClicked: addTokenPane.hide() + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 15 + + CustomButton { + Layout.preferredHeight: 38 + leftPadding: 25 + rightPadding: 25 + //% "Look up" + text: qsTrId("settings-swap-token-lookup") + // the lookup queries the last-applied endpoint + enabled: newTokenAddress.text.trim().length > 0 && !isSettingsChanged() + onClicked: control.ethSettings.lookupToken(newTokenAddress.text.trim()) + } + + CustomButton { + Layout.preferredHeight: 38 + leftPadding: 25 + rightPadding: 25 + //% "Add" + text: qsTrId("settings-swap-token-add") + enabled: newTokenSymbol.length > 0 + onClicked: { + control.ethSettings.addCustomToken(newTokenAddress.text.trim(), newTokenSymbol, newTokenDecimals) + // the rejection error is emitted synchronously + if (newTokenError.length === 0) + addTokenPane.hide() + } + } + + Item { + Layout.fillWidth: true + } + } + + SFText { + visible: newTokenSymbol.length > 0 || newTokenError.length > 0 + Layout.fillWidth: true + font.pixelSize: 12 + wrapMode: Text.Wrap + color: newTokenError.length > 0 ? Style.validator_error : Style.content_secondary + text: newTokenError.length > 0 ? newTokenError : + //% "%1, %2 decimals" + qsTrId("settings-swap-token-info").arg(newTokenSymbol).arg(newTokenDecimals) + } + } + } } } \ No newline at end of file diff --git a/ui/view/controls/SwapTokenInfoDialog.qml b/ui/view/controls/SwapTokenInfoDialog.qml index 8750d1f13..f4349b9af 100644 --- a/ui/view/controls/SwapTokenInfoDialog.qml +++ b/ui/view/controls/SwapTokenInfoDialog.qml @@ -63,7 +63,7 @@ CustomDialog { Layout.alignment: Qt.AlignTop font.pixelSize: 14 color: Style.content_disabled - text: qsTrId("sent-amount-label") + ":" + text: qsTrId("atomic-swap-i-send") + ":" visible: viewModel.sendAmount.length } @@ -82,7 +82,7 @@ CustomDialog { Layout.alignment: Qt.AlignTop font.pixelSize: 14 color: Style.content_disabled - text: qsTrId("receive-amount-swap-label") + ":" + text: qsTrId("atomic-swap-i-receive") + ":" visible: viewModel.receiveAmount.length } diff --git a/ui/view/controls/SwapTransactionDetails.qml b/ui/view/controls/SwapTransactionDetails.qml index f34fbeb03..20c1a5614 100644 --- a/ui/view/controls/SwapTransactionDetails.qml +++ b/ui/view/controls/SwapTransactionDetails.qml @@ -37,6 +37,7 @@ RowLayout { property var swapCoinRefundTxConfirmations property var stateDetails + property var coinsUnlockNote // property var onOpenExternal: null signal textCopied(string text) @@ -365,6 +366,26 @@ RowLayout { } } + RowLayout { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: root.coinsUnlockNote != "" + SvgImage { + sourceSize: Qt.size(16, 16) + source: "qrc:/assets/icon-attention.svg" + } + SFLabel { + Layout.fillWidth: true + copyMenuEnabled: true + font.pixelSize: 14 + color: Style.content_main + wrapMode: Text.Wrap + elide: Text.ElideMiddle + text: root.coinsUnlockNote + onCopyText: textCopied(text) + } + } + SFText { enabled: failureReasonLabel.enabled visible: enabled diff --git a/ui/view/controls/TokenIcon.qml b/ui/view/controls/TokenIcon.qml new file mode 100644 index 000000000..5528365f5 --- /dev/null +++ b/ui/view/controls/TokenIcon.qml @@ -0,0 +1,33 @@ +import QtQuick +import "." + +// Deterministic, local-only colored-circle icon for an ERC-20 token, matching the +// look of Confidential Asset icons (colored circle + glyph) without any network +// lookup: the color is a hash of the contract address (see beamui::ColorFromString), +// the glyph is just the token symbol's first letter. +Item { + id: control + + property color tokenColor: "#8192a3" + property string symbol: "" + property int size: 26 + + width: size + height: size + implicitWidth: size + implicitHeight: size + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: control.tokenColor + } + + SFText { + anchors.centerIn: parent + text: control.symbol.length ? control.symbol.charAt(0).toUpperCase() : "?" + color: "white" + font.pixelSize: control.size * 0.42 + font.bold: true + } +} diff --git a/ui/view/create_asset_swap.qml b/ui/view/create_asset_swap.qml index e66d1c716..08fb1a77a 100644 --- a/ui/view/create_asset_swap.qml +++ b/ui/view/create_asset_swap.qml @@ -55,8 +55,8 @@ ColumnLayout { // Send amount // Panel { - //% "Send amount" - title: qsTrId("sent-amount-label") + //% "I send" + title: qsTrId("atomic-swap-i-send") Layout.fillWidth: true content: AmountInput { @@ -147,8 +147,8 @@ ColumnLayout { // Receive amount // Panel { - //% "Receive amount" - title: qsTrId("receive-amount-swap-label") + //% "I receive" + title: qsTrId("atomic-swap-i-receive") Layout.fillWidth: true content: AmountInput { diff --git a/ui/view/create_atomic_swap.qml b/ui/view/create_atomic_swap.qml index f061850c2..071f1fd21 100644 --- a/ui/view/create_atomic_swap.qml +++ b/ui/view/create_atomic_swap.qml @@ -32,6 +32,29 @@ ColumnLayout { } } + // currencyIdx is a plain int property: the first imperative write to it + // (combo onActivated, swap-arrow, Beam-invariant enforcement) destroys the + // declarative "currencyIdx: viewModel.sentCurrencyIndex" binding. Resync + // manually whenever the VM re-derives the index (e.g. currList mutated by + // adding/removing a token in settings). + Connections { + target: viewModel + function onCurrListChanged() { + if (sentAmountInput.currencyIdx !== viewModel.sentCurrencyIndex) + sentAmountInput.currencyIdx = viewModel.sentCurrencyIndex + if (receiveAmountInput.currencyIdx !== viewModel.receiveCurrencyIndex) + receiveAmountInput.currencyIdx = viewModel.receiveCurrencyIndex + } + function onSentCurrencyIndexChanged() { + if (sentAmountInput.currencyIdx !== viewModel.sentCurrencyIndex) + sentAmountInput.currencyIdx = viewModel.sentCurrencyIndex + } + function onReceiveCurrencyIndexChanged() { + if (receiveAmountInput.currencyIdx !== viewModel.receiveCurrencyIndex) + receiveAmountInput.currencyIdx = viewModel.receiveCurrencyIndex + } + } + SwapTokenInfoDialog { id: tokenInfoDialog token: viewModel.transactionToken @@ -115,29 +138,29 @@ Update your settings and try again." // Send amount // Panel { - //% "Send amount" - title: qsTrId("sent-amount-label") + //% "I send" + title: qsTrId("atomic-swap-i-send") Layout.fillWidth: true content: AmountInput { id: sentAmountInput color: Style.accent_outgoing currencies: viewModel.currList - currencyIdx: viewModel.sentCurrency + currencyIdx: viewModel.sentCurrencyIndex amount: viewModel.amountSent rate: viewModel.secondCurrencySendRateValue rateUnit: viewModel.secondCurrencyUnitName multi: true resetAmount: false - currColor: currencyError() || !BeamGlobals.canReceive(currencyIdx) ? Style.validator_error : Style.content_main + currColor: currencyError() || !BeamGlobals.canReceive(viewModel.sentCurrency) ? Style.validator_error : Style.content_main error: getErrorText() function getErrorText() { - if(!BeamGlobals.canReceive(currencyIdx)) { + if(!BeamGlobals.canReceive(viewModel.sentCurrency)) { /*% "%1 is not connected, please review your settings and try again" */ - return qsTrId("swap-currency-na-message").arg(BeamGlobals.getCurrencyName(currencyIdx)).replace("\n", "") + return qsTrId("swap-currency-na-message").arg(BeamGlobals.getCurrencyName(viewModel.sentCurrency)).replace("\n", "") } if(!viewModel.isSendFeeOK) { //% "The swap amount must be greater than the transaction fee" @@ -151,8 +174,8 @@ please review your settings and try again" } onCurrencyIdxChanged: { - if(sentAmountInput.currencyIdx != OldWalletCurrency.CurrBeam && - receiveAmountInput.currencyIdx != OldWalletCurrency.CurrBeam) { + // asset entries are BEAM-side too, so key on isBEAM, not the index + if(!sentAmountInput.isBeam && !receiveAmountInput.isBeam) { receiveAmountInput.currencyIdx = OldWalletCurrency.CurrBeam } } @@ -166,7 +189,7 @@ please review your settings and try again" Binding { target: viewModel - property: "sentCurrency" + property: "sentCurrencyIndex" value: sentAmountInput.currencyIdx } } @@ -184,7 +207,8 @@ please review your settings and try again" currency: viewModel.sentCurrency minFee: currency == OldWalletCurrency.CurrBeam ? viewModel.minimalBeamFeeGrothes : BeamGlobals.getMinimalFee(currency, false) maxFee: BeamGlobals.getMaximumFee(currency) - recommendedFee: BeamGlobals.getRecommendedFee(currency) + // feeRatesRevision makes the binding re-query the cached rate + recommendedFee: { viewModel.feeRatesRevision; return BeamGlobals.getRecommendedFee(currency); } feeLabel: BeamGlobals.getFeeRateLabel(currency) color: Style.accent_outgoing readOnly: false @@ -348,43 +372,45 @@ please review your settings and try again" // Receive amount // Panel { - //% "Receive amount" - title: qsTrId("receive-amount-swap-label") + //% "I receive" + title: qsTrId("atomic-swap-i-receive") Layout.fillWidth: true content: AmountInput { id: receiveAmountInput currencies: viewModel.currList - currencyIdx: viewModel.receiveCurrency + currencyIdx: viewModel.receiveCurrencyIndex amount: viewModel.amountToReceive rate: viewModel.secondCurrencyReceiveRateValue rateUnit: viewModel.secondCurrencyUnitName multi: true resetAmount: false - currColor: currencyError() || !BeamGlobals.canReceive(currencyIdx) ? Style.validator_error : Style.content_main + currColor: currencyError() || !BeamGlobals.canReceive(viewModel.receiveCurrency) ? Style.validator_error : Style.content_main error: getErrorText() function getErrorText() { - if(!BeamGlobals.canReceive(currencyIdx)) { + if(!BeamGlobals.canReceive(viewModel.receiveCurrency)) { /*% "%1 is not connected, please review your settings and try again" */ - return qsTrId("swap-currency-na-message").arg(BeamGlobals.getCurrencyName(currencyIdx)).replace("\n", "") + return qsTrId("swap-currency-na-message").arg(BeamGlobals.getCurrencyName(viewModel.receiveCurrency)).replace("\n", "") } if(!viewModel.isReceiveFeeOK) { //% "The swap amount must be greater than the transaction fee" return qsTrId("send-less-than-fee") } if(!viewModel.isEnoughToReceive) { - //% "There is not enough funds to complete the transaction" - return qsTrId("send-not-enough") + // isEnoughToReceive only fails for an Ethereum-based receive + // side: the redeem tx gas is paid in ETH +/*% "Not enough ETH to pay the redeem transaction fee (%1 needed)" +*/ + return qsTrId("swap-not-enough-eth-redeem").arg(BeamGlobals.calcWithdrawTxFee(viewModel.receiveCurrency, viewModel.receiveFee)) } return "" } onCurrencyIdxChanged: { - if(receiveAmountInput.currencyIdx != OldWalletCurrency.CurrBeam && - sentAmountInput.currencyIdx != OldWalletCurrency.CurrBeam) { + if(!receiveAmountInput.isBeam && !sentAmountInput.isBeam) { sentAmountInput.currencyIdx = OldWalletCurrency.CurrBeam } } @@ -398,7 +424,7 @@ please review your settings and try again" Binding { target: viewModel - property: "receiveCurrency" + property: "receiveCurrencyIndex" value: receiveAmountInput.currencyIdx } } @@ -415,7 +441,7 @@ please review your settings and try again" currency: viewModel.receiveCurrency minFee: BeamGlobals.getMinimalFee(currency, false) maxFee: BeamGlobals.getMaximumFee(currency) - recommendedFee: BeamGlobals.getRecommendedFee(currency) + recommendedFee: { viewModel.feeRatesRevision; return BeamGlobals.getRecommendedFee(currency); } feeLabel: BeamGlobals.getFeeRateLabel(currency) color: Style.accent_outgoing readOnly: false diff --git a/ui/view/qml.qrc b/ui/view/qml.qrc index 4236e81f8..8407fa54c 100644 --- a/ui/view/qml.qrc +++ b/ui/view/qml.qrc @@ -10,6 +10,7 @@ controls/qmldir controls/SvgImage.qml controls/SwapCurrencyAmountPane.qml + controls/TokenIcon.qml controls/SFLabel.qml controls/SFText.qml controls/SFTextInput.qml diff --git a/ui/view/send_swap.qml b/ui/view/send_swap.qml index d147d24c6..e2316963c 100644 --- a/ui/view/send_swap.qml +++ b/ui/view/send_swap.qml @@ -141,8 +141,8 @@ please review your settings and try again" // Send amount // Panel { - //% "Send amount" - title: qsTrId("sent-amount-label") + //% "I send" + title: qsTrId("atomic-swap-i-send") Layout.fillWidth: true content: @@ -199,7 +199,7 @@ please review your settings and try again" currency: viewModel.sendCurrency minFee: currency == OldWalletCurrency.CurrBeam ? viewModel.minimalBeamFeeGrothes : BeamGlobals.getMinimalFee(currency, false) maxFee: BeamGlobals.getMaximumFee(currency) - recommendedFee: BeamGlobals.getRecommendedFee(currency) + recommendedFee: { viewModel.feeRatesRevision; return BeamGlobals.getRecommendedFee(currency); } feeLabel: BeamGlobals.getFeeRateLabel(currency) color: Style.accent_outgoing readOnly: false @@ -251,6 +251,84 @@ please review your settings and try again" } } } + + // + // ERC-20 token details + // + FoldablePanel { + visible: viewModel.isErc20Swap + //% "Token" + title: qsTrId("swap-accept-token-title") + Layout.fillWidth: true + + content: ColumnLayout { + SFText { + font.pixelSize: 14 + color: Style.content_secondary + //% "Contract address" + text: qsTrId("swap-accept-token-contract-label") + } + SFLabel { + Layout.fillWidth: true + font.pixelSize: 14 + font.family: "Monospace" + color: Style.content_main + wrapMode: Text.WrapAnywhere + copyMenuEnabled: true + text: viewModel.tokenContract + } + SFText { + Layout.topMargin: 10 + font.pixelSize: 14 + color: Style.content_secondary + //% "Symbol / decimals" + text: qsTrId("swap-accept-token-symbol-label") + } + SFText { + font.pixelSize: 14 + color: Style.content_main + text: viewModel.tokenSymbol + " / " + viewModel.tokenDecimals + } + SFText { + Layout.topMargin: 10 + Layout.fillWidth: true + font.pixelSize: 12 + color: Style.validator_error + wrapMode: Text.Wrap + //% "Verify this token contract address carefully. Anyone can create a token with any name." + text: qsTrId("swap-accept-token-warning") + } + } + } + + // + // Confidential Asset details + // + FoldablePanel { + visible: viewModel.isBeamAssetSwap + //% "Confidential Asset" + title: qsTrId("swap-accept-asset-title") + Layout.fillWidth: true + + content: ColumnLayout { + SFText { + font.pixelSize: 14 + color: Style.content_main + //% "Asset id %1, unit %2" + text: qsTrId("swap-accept-asset-id-unit").arg(viewModel.beamAssetId).arg(viewModel.beamAssetUnitName) + } + SFText { + visible: viewModel.needsBeamForRedeemFee + Layout.topMargin: 10 + Layout.fillWidth: true + font.pixelSize: 12 + color: Style.content_secondary + wrapMode: Text.Wrap + //% "You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee." + text: qsTrId("swap-accept-asset-beam-fee") + } + } + } } // @@ -265,8 +343,8 @@ please review your settings and try again" // Receive amount // Panel { - //% "Receive amount" - title: qsTrId("receive-amount-swap-label") + //% "I receive" + title: qsTrId("atomic-swap-i-receive") Layout.fillWidth: true content: @@ -289,8 +367,11 @@ please review your settings and try again" return qsTrId("send-less-than-fee") } if(!viewModel.isEnoughToReceive) { - //% "There is not enough funds to complete the transaction" - return qsTrId("send-not-enough") + // isEnoughToReceive only fails for an Ethereum-based receive + // side: the redeem tx gas is paid in ETH +/*% "Not enough ETH to pay the redeem transaction fee (%1 needed)" +*/ + return qsTrId("swap-not-enough-eth-redeem").arg(BeamGlobals.calcWithdrawTxFee(viewModel.receiveCurrency, viewModel.receiveFee)) } return "" } @@ -322,7 +403,7 @@ please review your settings and try again" currency: viewModel.receiveCurrency minFee: BeamGlobals.getMinimalFee(currency, false) maxFee: BeamGlobals.getMaximumFee(currency) - recommendedFee: BeamGlobals.getRecommendedFee(currency) + recommendedFee: { viewModel.feeRatesRevision; return BeamGlobals.getRecommendedFee(currency); } feeLabel: BeamGlobals.getFeeRateLabel(currency) color: Style.accent_outgoing readOnly: false @@ -506,6 +587,14 @@ please review your settings and try again" flatFee: viewModel.sendCurrency == OldWalletCurrency.CurrBeam, rate: viewModel.secondCurrencySendRateValue, rateUnit: viewModel.secondCurrencyUnitName, + isErc20Swap: viewModel.isErc20Swap, + tokenContract: viewModel.tokenContract, + tokenSymbol: viewModel.tokenSymbol, + tokenDecimals: viewModel.tokenDecimals, + isBeamAssetSwap: viewModel.isBeamAssetSwap, + beamAssetId: viewModel.beamAssetId, + beamAssetUnitName: viewModel.beamAssetUnitName, + needsBeamForRedeemFee: viewModel.needsBeamForRedeemFee, }) dialogObject.onAccepted.connect(function () { diff --git a/ui/view/swap_confirm.qml b/ui/view/swap_confirm.qml index 6b3570909..4081d0a8f 100644 --- a/ui/view/swap_confirm.qml +++ b/ui/view/swap_confirm.qml @@ -35,6 +35,16 @@ ConfirmationDialog { property string rateUnit: "" property bool showRate: rateUnit.length > 0 + // CA / ERC-20 extended offer info, set by the caller (send_swap.qml) + property bool isErc20Swap: false + property string tokenContract: "" + property string tokenSymbol: "" + property int tokenDecimals: 0 + property bool isBeamAssetSwap: false + property int beamAssetId: 0 + property string beamAssetUnitName: "" + property bool needsBeamForRedeemFee: false + readonly property string feeLabel: { //% "Fee" if (!control.swapMode) return [qsTrId("send-regular-fee"), ":"].join("") @@ -282,6 +292,33 @@ ConfirmationDialog { wrapMode: Text.WordWrap text: control.onlineMessage } + + // + // ERC-20 token / Confidential Asset confirmation notes + // + SFText { + visible: control.isErc20Swap + Layout.columnSpan: 2 + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + font.pixelSize: 12 + color: Style.validator_error + wrapMode: Text.WordWrap + //% "%1 contract: %2. Verify this token contract address carefully. Anyone can create a token with any name." + text: qsTrId("swap-accept-token-warning-confirm").arg(control.tokenSymbol).arg(control.tokenContract) + } + + SFText { + visible: control.needsBeamForRedeemFee + Layout.columnSpan: 2 + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + font.pixelSize: 12 + color: Style.content_disabled + wrapMode: Text.WordWrap + //% "You are receiving a Confidential Asset. A small BEAM balance is required to pay the redeem transaction fee." + text: qsTrId("swap-accept-asset-beam-fee") + } } } } \ No newline at end of file diff --git a/ui/viewmodel/atomic_swap/swap_eth_settings_item.cpp b/ui/viewmodel/atomic_swap/swap_eth_settings_item.cpp index 0de9d5e45..3b2cc6d7d 100644 --- a/ui/viewmodel/atomic_swap/swap_eth_settings_item.cpp +++ b/ui/viewmodel/atomic_swap/swap_eth_settings_item.cpp @@ -14,13 +14,19 @@ #include "swap_eth_settings_item.h" +#include + #include "mnemonic/mnemonic.h" #include "model/app_model.h" +#include "model/swap_eth_client_model.h" #include "viewmodel/qml_globals.h" #include "seed_phrase_item.h" #include "viewmodel/settings_helpers.h" +#include "viewmodel/ui_helpers.h" #include "wallet/transactions/swaps/bridges/ethereum/common.h" +#include "wallet/transactions/swaps/common.h" +#include "model/settings.h" using namespace beam; @@ -35,7 +41,10 @@ SwapEthSettingsItem::SwapEthSettingsItem() auto coinClient = m_coinClient.lock(); connect(coinClient.get(), SIGNAL(statusChanged()), this, SIGNAL(connectionStatusChanged())); connect(coinClient.get(), SIGNAL(connectionErrorChanged()), this, SIGNAL(connectionErrorChanged())); + connect(coinClient.get(), &SwapEthClientModel::endpointValidated, this, &SwapEthSettingsItem::onEndpointValidated); + connect(coinClient.get(), &SwapEthClientModel::gotTokenInfo, this, &SwapEthSettingsItem::tokenInfoReady); LoadSettings(); + loadCustomTokens(); } SwapEthSettingsItem::~SwapEthSettingsItem() @@ -58,8 +67,11 @@ void SwapEthSettingsItem::applySettings() m_settings->m_shouldConnect = m_shouldConnect; m_settings->m_projectID = m_infuraProjectID.toStdString(); m_settings->m_secretWords = GetSeedPhraseFromSeedItems(); + m_settings->m_useCustomRpc = m_useCustomRpc; + m_settings->m_customRpcUrl = m_customRpcUrl.trimmed().toStdString(); coinClient->SetSettings(*m_settings); + clearEndpointCheck(); } void SwapEthSettingsItem::clearSettings() @@ -109,6 +121,135 @@ void SwapEthSettingsItem::validateCurrentSeedPhrase() setIsCurrentSeedValid(isValidMnemonic(seedPhrases, language::en)); } +void SwapEthSettingsItem::validateEndpoint() +{ + if (auto c = m_coinClient.lock()) + { + c->validateEndpoint(); + } +} + +void SwapEthSettingsItem::lookupToken(const QString& contractAddress) +{ + const auto address = contractAddress.trimmed().toStdString(); + if (!wallet::IsValidEthContractAddress(address)) + { + //% "Invalid contract address" + emit tokenInfoReady(contractAddress, QString(), 0, qtTrId("settings-swap-token-invalid-address")); + return; + } + + if (auto c = m_coinClient.lock()) + { + c->requestTokenInfo(address); + } +} + +void SwapEthSettingsItem::addCustomToken(const QString& contractAddress, const QString& symbol, uint decimals) +{ + const auto address = contractAddress.trimmed().toStdString(); + if (!wallet::IsValidEthContractAddress(address) || symbol.isEmpty()) + { + return; + } + + const auto normalized = QString::fromStdString(address).toLower(); + if (isBuiltinTokenContract(normalized)) + { + //% "This token is already supported" + emit tokenInfoReady(contractAddress, QString(), 0, qtTrId("settings-swap-token-already-added")); + return; + } + for (const auto& token : m_customTokens) + { + if (token.value("contract").toString().toLower() == normalized) + { + //% "This token is already supported" + emit tokenInfoReady(contractAddress, QString(), 0, qtTrId("settings-swap-token-already-added")); + return; // already added + } + } + + QMap token; + token.insert("contract", QString::fromStdString(address)); + token.insert("symbol", symbol); + token.insert("decimals", decimals); + m_customTokens.push_back(token); + + saveCustomTokens(); + emit customTokensChanged(); +} + +void SwapEthSettingsItem::removeCustomToken(const QString& contractAddress) +{ + const auto normalized = contractAddress.trimmed().toLower(); + for (int i = 0; i < m_customTokens.size(); ++i) + { + if (m_customTokens[i].value("contract").toString().toLower() == normalized) + { + m_customTokens.removeAt(i); + saveCustomTokens(); + emit customTokensChanged(); + return; + } + } +} + +QList> SwapEthSettingsItem::getCustomTokens() const +{ + QList> result; + for (auto token : m_customTokens) + { + token.insert("color", beamui::ColorFromString(token.value("contract").toString())); + result.push_back(token); + } + return result; +} + +QList> SwapEthSettingsItem::getBuiltinTokens() const +{ + QList> result; + static const std::tuple builtins[] = { + { wallet::AtomicSwapCoin::Usdt, "USDT", "qrc:/assets/icon-usdt.svg" }, + { wallet::AtomicSwapCoin::Dai, "DAI", "qrc:/assets/icon-dai.svg" }, + { wallet::AtomicSwapCoin::WBTC, "WBTC", "qrc:/assets/icon-wbtc.svg" }, + }; + + for (const auto& [coin, symbol, icon] : builtins) + { + QMap token; + const auto contract = QString::fromStdString(m_settings->GetTokenContractAddress(coin)); + token.insert("symbol", QString(symbol)); + token.insert("contract", contract); + token.insert("color", beamui::ColorFromString(contract)); + token.insert("icon", QString(icon)); + result.push_back(token); + } + return result; +} + +bool SwapEthSettingsItem::isBuiltinTokenContract(const QString& normalizedAddress) const +{ + for (const auto& token : getBuiltinTokens()) + { + if (token.value("contract").toString().toLower() == normalizedAddress) + { + return true; + } + } + return false; +} + +void SwapEthSettingsItem::loadCustomTokens() +{ + m_customTokens = AppModel::getInstance().getSettings().getEthCustomTokens(); +} + +void SwapEthSettingsItem::saveCustomTokens() +{ + AppModel::getInstance().getSettings().setEthCustomTokens(m_customTokens); +} + QStringList SwapEthSettingsItem::getEthereumAddresses() const { QStringList result; @@ -131,7 +272,7 @@ QString SwapEthSettingsItem::getTitle() const { if (m_settings->m_shouldConnect) { - //% "Ethereum node" + //% "Ethereum" return qtTrId("settings-swap-ethereum-node"); } @@ -206,6 +347,8 @@ void SwapEthSettingsItem::LoadSettings() { SetSeedPhrase(m_settings->m_secretWords); infuraProjectID(str2qstr(m_settings->m_projectID)); + setUseCustomRpc(m_settings->m_useCustomRpc); + setCustomRpcUrl(str2qstr(m_settings->m_customRpcUrl)); setAccountIndex(m_settings->m_accountIndex); shouldConnect(m_settings->m_shouldConnect); @@ -256,6 +399,8 @@ void SwapEthSettingsItem::SetDefaultSettings(bool clearSeed) { infuraProjectID(""); setAccountIndex(0); + setUseCustomRpc(false); + setCustomRpcUrl(""); if (clearSeed) { @@ -274,6 +419,7 @@ void SwapEthSettingsItem::infuraProjectID(const QString& value) { m_infuraProjectID = value; emit infuraProjectIDChanged(); + clearEndpointCheck(); } } @@ -339,6 +485,99 @@ QString SwapEthSettingsItem::getConnectionError() const } } +bool SwapEthSettingsItem::useCustomRpc() const +{ + return m_useCustomRpc; +} + +void SwapEthSettingsItem::setUseCustomRpc(bool value) +{ + if (value != m_useCustomRpc) + { + m_useCustomRpc = value; + emit useCustomRpcChanged(); + clearEndpointCheck(); + } +} + +QString SwapEthSettingsItem::customRpcUrl() const +{ + return m_customRpcUrl; +} + +void SwapEthSettingsItem::setCustomRpcUrl(const QString& value) +{ + if (value != m_customRpcUrl) + { + m_customRpcUrl = value; + emit customRpcUrlChanged(); + clearEndpointCheck(); + } +} + +QString SwapEthSettingsItem::endpointCheckResult() const +{ + return m_endpointCheckResult; +} + +bool SwapEthSettingsItem::endpointCheckOk() const +{ + return m_endpointCheckOk; +} + +QString SwapEthSettingsItem::getChainName(quint64 chainID) +{ + switch (chainID) + { + //% "Ethereum Mainnet" + case 1: return qtTrId("settings-eth-chain-mainnet"); + //% "Sepolia Testnet" + case 11155111: return qtTrId("settings-eth-chain-sepolia"); + default: + //% "chain %1" + return qtTrId("settings-eth-chain-other").arg(chainID); + } +} + +void SwapEthSettingsItem::onEndpointValidated(quint64 chainID, quint64 blockNumber, bool ok, bool wrongNetwork, const QString& errorMsg) +{ + m_endpointCheckOk = ok; + if (!ok) + { + if (wrongNetwork) + { + //% "Connected to %1, but Ethereum Mainnet is required" + m_endpointCheckResult = qtTrId("settings-eth-endpoint-wrong-network").arg(getChainName(chainID)); + } + else + { + //% "Unable to connect" + m_endpointCheckResult = qtTrId("settings-eth-endpoint-failed"); + if (!errorMsg.isEmpty()) + { + m_endpointCheckResult += " (" + errorMsg + ")"; + } + } + } + else + { + //% "Connected to %1. Latest block: %2" + m_endpointCheckResult = qtTrId("settings-eth-endpoint-ok") + .arg(getChainName(chainID)).arg(QLocale().toString((qulonglong)blockNumber)); + } + emit endpointCheckResultChanged(); +} + +void SwapEthSettingsItem::clearEndpointCheck() +{ + if (!m_endpointCheckResult.isEmpty() || m_endpointCheckOk) + { + m_endpointCheckResult.clear(); + m_endpointCheckOk = false; + emit endpointCheckResultChanged(); + } +} + std::vector SwapEthSettingsItem::GetSeedPhraseFromSeedItems() const { assert(static_cast(m_seedPhraseItems.size()) == WORD_COUNT); diff --git a/ui/viewmodel/atomic_swap/swap_eth_settings_item.h b/ui/viewmodel/atomic_swap/swap_eth_settings_item.h index f5a7568f2..9b445de15 100644 --- a/ui/viewmodel/atomic_swap/swap_eth_settings_item.h +++ b/ui/viewmodel/atomic_swap/swap_eth_settings_item.h @@ -15,6 +15,8 @@ #pragma once #include +#include +#include #include #include #include "wallet/transactions/swaps/bridges/ethereum/settings.h" @@ -36,12 +38,20 @@ class SwapEthSettingsItem : public QObject Q_PROPERTY(bool isCurrentSeedValid READ isCurrentSeedValid NOTIFY isCurrentSeedValidChanged) Q_PROPERTY(QString infuraProjectID READ infuraProjectID WRITE infuraProjectID NOTIFY infuraProjectIDChanged) Q_PROPERTY(unsigned int accountIndex READ getAccountIndex WRITE setAccountIndex NOTIFY accountIndexChanged) - + Q_PROPERTY(bool canChangeConnection READ canChangeConnection NOTIFY canChangeConnectionChanged) Q_PROPERTY(bool isConnected READ getIsConnected NOTIFY connectionChanged) Q_PROPERTY(QString connectionStatus READ getConnectionStatus NOTIFY connectionStatusChanged) Q_PROPERTY(QString connectionError READ getConnectionError NOTIFY connectionErrorChanged) + Q_PROPERTY(bool useCustomRpc READ useCustomRpc WRITE setUseCustomRpc NOTIFY useCustomRpcChanged) + Q_PROPERTY(QString customRpcUrl READ customRpcUrl WRITE setCustomRpcUrl NOTIFY customRpcUrlChanged) + Q_PROPERTY(QString endpointCheckResult READ endpointCheckResult NOTIFY endpointCheckResultChanged) + Q_PROPERTY(bool endpointCheckOk READ endpointCheckOk NOTIFY endpointCheckResultChanged) + Q_PROPERTY(QList> customTokens READ getCustomTokens NOTIFY customTokensChanged) + // fixed DAI/USDT/WBTC contracts the wallet already supports natively, shown as + // non-removable rows above the user-added custom tokens + Q_PROPERTY(QList> builtinTokens READ getBuiltinTokens CONSTANT) public: SwapEthSettingsItem(); @@ -56,6 +66,11 @@ class SwapEthSettingsItem : public QObject Q_INVOKABLE void copySeedPhrases(); Q_INVOKABLE void validateCurrentSeedPhrase(); Q_INVOKABLE QStringList getEthereumAddresses() const; + Q_INVOKABLE void validateEndpoint(); + + Q_INVOKABLE void lookupToken(const QString& contractAddress); + Q_INVOKABLE void addCustomToken(const QString& contractAddress, const QString& symbol, uint decimals); + Q_INVOKABLE void removeCustomToken(const QString& contractAddress); private: QString getTitle() const; @@ -83,7 +98,23 @@ class SwapEthSettingsItem : public QObject QString getConnectionStatus() const; QString getConnectionError() const; + bool useCustomRpc() const; + void setUseCustomRpc(bool value); + QString customRpcUrl() const; + void setCustomRpcUrl(const QString& value); + QString endpointCheckResult() const; + bool endpointCheckOk() const; + void onEndpointValidated(quint64 chainID, quint64 blockNumber, bool ok, bool wrongNetwork, const QString& errorMsg); + std::vector GetSeedPhraseFromSeedItems() const; + static QString getChainName(quint64 chainID); + void clearEndpointCheck(); + + QList> getCustomTokens() const; + QList> getBuiltinTokens() const; + bool isBuiltinTokenContract(const QString& normalizedAddress) const; + void loadCustomTokens(); + void saveCustomTokens(); signals: void infuraProjectIDChanged(); @@ -95,6 +126,11 @@ class SwapEthSettingsItem : public QObject void connectionStatusChanged(); void connectionErrorChanged(); void foldedChanged(); + void useCustomRpcChanged(); + void customRpcUrlChanged(); + void endpointCheckResultChanged(); + void customTokensChanged(); + void tokenInfoReady(const QString& contract, const QString& symbol, uint decimals, const QString& error); private: std::weak_ptr m_coinClient; @@ -105,4 +141,9 @@ class SwapEthSettingsItem : public QObject QString m_infuraProjectID; unsigned int m_accountIndex; bool m_isFolded = true; + bool m_useCustomRpc = false; + QString m_customRpcUrl; + QString m_endpointCheckResult; + bool m_endpointCheckOk = false; + QList> m_customTokens; // {"contract","symbol","decimals"}, persisted per wallet }; \ No newline at end of file diff --git a/ui/viewmodel/atomic_swap/swap_offer_item.cpp b/ui/viewmodel/atomic_swap/swap_offer_item.cpp index 34ca1c779..326e591a1 100644 --- a/ui/viewmodel/atomic_swap/swap_offer_item.cpp +++ b/ui/viewmodel/atomic_swap/swap_offer_item.cpp @@ -15,9 +15,11 @@ #include "swap_offer_item.h" #include "utility/helpers.h" #include "wallet/core/common.h" +#include "wallet/transactions/swaps/common.h" #include "viewmodel/ui_helpers.h" #include "viewmodel/qml_globals.h" - +#include "swap_utils.h" +#include SwapOfferItem::SwapOfferItem(QObject* parent /* = nullptr*/) : QObject(parent) @@ -69,22 +71,57 @@ QString SwapOfferItem::rate() const if (!beamAmount) return QString(); + QString otherAmountStr; + uint8_t otherDecimals = beamui::getCurrencyDecimals(getSwapCoinType()); + + if (m_offer.ResolveCoin() == beam::wallet::AtomicSwapCoin::Erc20Token) + { + uint8_t onChainDecimals = 0; + m_offer.GetParameter(beam::wallet::TxParameterID::AtomicSwapTokenDecimals, onChainDecimals); + otherDecimals = beamui::tokenWalletDecimals(onChainDecimals); + otherAmountStr = beamui::AmountToUIStringExactDecimals(otherCoinAmount, otherDecimals); + } + else + { + otherAmountStr = beamui::AmountToUIString(otherCoinAmount, getSwapCoinType(), false); + } + return QMLGlobals::divideWithPrecision( - beamui::AmountToUIString(otherCoinAmount, getSwapCoinType(), false), - beamui::AmountToUIString(beamAmount), - beamui::getCurrencyDecimals(getSwapCoinType())); + otherAmountStr, + beamui::AmountToUIString(beamAmount), + otherDecimals); } QString SwapOfferItem::amountSend() const { - auto coinType = isSendBeam() ? beamui::Currencies::Beam : getSwapCoinType(); - return beamui::AmountToUIString(rawAmountSend(), coinType); + return isSendBeam() ? amountBeamSide(rawAmountSend()) : amountSwapCoinSide(rawAmountSend()); } QString SwapOfferItem::amountReceive() const { - auto coinType = isSendBeam() ? getSwapCoinType() : beamui::Currencies::Beam; - return beamui::AmountToUIString(rawAmountReceive(), coinType); + return isSendBeam() ? amountSwapCoinSide(rawAmountReceive()) : amountBeamSide(rawAmountReceive()); +} + +QString SwapOfferItem::amountBeamSide(beam::Amount value) const +{ + beam::Asset::ID assetId = 0; + m_offer.GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetID, assetId); + if (assetId != 0) + { + std::string assetName; + m_offer.GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetName, assetName); + return beamui::AmountToUIString(value, QString::fromStdString(assetName)); + } + return beamui::AmountToUIString(value, beamui::Currencies::Beam); +} + +QString SwapOfferItem::amountSwapCoinSide(beam::Amount value) const +{ + if (m_offer.ResolveCoin() == beam::wallet::AtomicSwapCoin::Erc20Token) + { + return swapui::erc20AmountString(m_offer, value, true); + } + return beamui::AmountToUIString(value, getSwapCoinType()); } bool SwapOfferItem::isOwnOffer() const @@ -114,9 +151,31 @@ beamui::Currencies SwapOfferItem::getSwapCoinType() const QString SwapOfferItem::getSwapCoinName() const { + if (m_offer.ResolveCoin() == beam::wallet::AtomicSwapCoin::Erc20Token) + { + return swapui::erc20Symbol(m_offer); + } return toString(getSwapCoinType()); } +beam::wallet::AtomicSwapCoin SwapOfferItem::resolvedSwapCoin() const +{ + return m_offer.ResolveCoin(); +} + +QString SwapOfferItem::getBeamSideName() const +{ + beam::Asset::ID assetId = 0; + m_offer.GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetID, assetId); + if (assetId != 0) + { + std::string assetName; + m_offer.GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetName, assetName); + return assetName.empty() ? QString("beam") : QString::fromStdString(assetName); + } + return "beam"; +} + void SwapOfferItem::reset(const beam::wallet::SwapOffer& offer) { m_offer = offer; diff --git a/ui/viewmodel/atomic_swap/swap_offer_item.h b/ui/viewmodel/atomic_swap/swap_offer_item.h index a8d718f34..f02671b50 100644 --- a/ui/viewmodel/atomic_swap/swap_offer_item.h +++ b/ui/viewmodel/atomic_swap/swap_offer_item.h @@ -42,6 +42,8 @@ class SwapOfferItem : public QObject beam::wallet::TxParameters getTxParameters() const; beam::wallet::TxID getTxID() const; QString getSwapCoinName() const; + QString getBeamSideName() const; // "beam", or the CA's unit name for extended offers + beam::wallet::AtomicSwapCoin resolvedSwapCoin() const; // SwapOffer::ResolveCoin() protected: void reset(const beam::wallet::SwapOffer& offer); @@ -49,6 +51,8 @@ class SwapOfferItem : public QObject private: beamui::Currencies getSwapCoinType() const; + QString amountBeamSide(beam::Amount value) const; + QString amountSwapCoinSide(beam::Amount value) const; beam::wallet::SwapOffer m_offer; /// TxParameters subclass bool m_isBeamSide; /// pay beam to receive other coin diff --git a/ui/viewmodel/atomic_swap/swap_offers_list.cpp b/ui/viewmodel/atomic_swap/swap_offers_list.cpp index 64b3e7bb4..676957f6a 100644 --- a/ui/viewmodel/atomic_swap/swap_offers_list.cpp +++ b/ui/viewmodel/atomic_swap/swap_offers_list.cpp @@ -97,7 +97,7 @@ QVariant SwapOffersList::data(const QModelIndex &index, int role) const case Roles::Pair: { auto swapCoin = value->getSwapCoinName(); - const QString beam = "beam"; + auto beam = value->getBeamSideName(); return value->isSendBeam() ? beam + '-' + swapCoin : swapCoin + '-' + beam; } default: diff --git a/ui/viewmodel/atomic_swap/swap_offers_view.cpp b/ui/viewmodel/atomic_swap/swap_offers_view.cpp index 2aeb2026c..d99c2f549 100644 --- a/ui/viewmodel/atomic_swap/swap_offers_view.cpp +++ b/ui/viewmodel/atomic_swap/swap_offers_view.cpp @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. #include +#include +#include #include "model/app_model.h" #include "model/settings.h" +#include "model/assets_manager.h" #include "swap_offers_view.h" #include "viewmodel/ui_helpers.h" #include "viewmodel/qml_globals.h" @@ -212,6 +215,16 @@ SwapOffersViewModel::SwapOffersViewModel() monitorAllOffersFitBalance(); + connect(AppModel::getInstance().getAssets().get(), &AssetsManager::assetsListChanged, this, &SwapOffersViewModel::coinFilterListChanged); + connect(&AppModel::getInstance().getSettings(), &WalletSettings::ethCustomTokensChanged, this, &SwapOffersViewModel::coinFilterListChanged); + connect(&AppModel::getInstance().getSettings(), &WalletSettings::ethCustomTokensChanged, this, &SwapOffersViewModel::customTokenCardsChanged); + + { + auto ethClient = AppModel::getInstance().getSwapEthClient(); + connect(ethClient.get(), SIGNAL(tokenBalancesChanged()), this, SIGNAL(customTokenCardsChanged())); + connect(ethClient.get(), SIGNAL(statusChanged()), this, SIGNAL(customTokenCardsChanged())); + } + m_walletModel->getAsync()->getSwapOffers(); m_walletModel->getAsync()->getTransactions(); } @@ -512,13 +525,43 @@ bool SwapOffersViewModel::isOfferFitBalance(const SwapOfferItem& offer) bool isSendBeam = offer.isSendBeam(); beam::AmountBig::Number beamOfferAmount = isSendBeam ? offer.rawAmountSend() : offer.rawAmountReceive(); - if (isSendBeam && beamOfferAmount > m_walletModel->getAvailable(beam::Asset::s_BeamID)) - return false; - + if (isSendBeam) + { + // when the user would send a Confidential Asset, check that asset's + // balance rather than plain BEAM (0 == plain BEAM, getAvailable(0) + // is the BEAM balance, so this covers both cases) + beam::Asset::ID beamAssetId = 0; + offer.getTxParameters().GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetID, beamAssetId); + if (beamOfferAmount > m_walletModel->getAvailable(beamAssetId)) + return false; + } + auto swapCoinOfferAmount = isSendBeam ? offer.rawAmountReceive() : offer.rawAmountSend(); - // TODO: find better solution to get AtomicSwapCoin - auto swapCoin = offer.getTxParameters().GetParameter(beam::wallet::TxParameterID::AtomicSwapCoin); - auto swapCoinClientWrapper = getSwapCoinClientWrapper(*swapCoin); + auto swapCoin = offer.resolvedSwapCoin(); + + if (swapCoin == beam::wallet::AtomicSwapCoin::Erc20Token) + { + if (isSendBeam) + return true; // sending BEAM/asset, receiving the token: no lock-side balance to check + + // no live per-token balance tracking here (out of scope for the list + // filter); an offer "fits" only if the user has already added the + // contract in the ethereum settings + std::string contract; + offer.getTxParameters().GetParameter(beam::wallet::TxParameterID::AtomicSwapTokenContract, contract); + // contract addresses are hex and commonly EIP-55 mixed-case; compare + // case-insensitively + const auto offerContract = QString::fromStdString(contract); + const auto storedTokens = AppModel::getInstance().getSettings().getEthCustomTokens(); + for (const auto& token : storedTokens) + { + if (token.value("contract").toString().compare(offerContract, Qt::CaseInsensitive) == 0) + return true; + } + return false; + } + + auto swapCoinClientWrapper = getSwapCoinClientWrapper(swapCoin); if (swapCoinClientWrapper->getIsConnected()) { @@ -616,6 +659,97 @@ QQmlListProperty SwapOffersViewModel::getSwapClients() return beamui::CreateQmlListProperty(this, m_swapClientWrappers); } +namespace +{ + QMap MakeCoinFilterEntry(const QString& text, const QString& pairName) + { + // matches the classic-pair separator convention (atomic_swap.qml): + // one side of "pair" equals this name, delimited by '-' + auto escaped = QRegularExpression::escape(pairName); + QMap entry; + entry.insert("text", text); + entry.insert("pair", "^(" + escaped + "-)|(-" + escaped + ")$"); + return entry; + } +} + +QList> SwapOffersViewModel::getCoinFilterList() const +{ + QList> result; + + QMap allEntry; + //% "(all)" + allEntry.insert("text", qtTrId("atomic-swap-all-coins")); + allEntry.insert("pair", ""); + result.push_back(allEntry); + + static const char* classicCoins[] = { "BTC", "DAI", "DASH", "DOGE", "ETH", "LTC", "QTUM", "USDT", "WBTC" }; + for (const auto* coin : classicCoins) + { + result.push_back(MakeCoinFilterEntry(coin, coin)); + } + + for (const auto& token : AppModel::getInstance().getSettings().getEthCustomTokens()) + { + auto symbol = token.value("symbol").toString(); + if (!symbol.isEmpty()) + { + result.push_back(MakeCoinFilterEntry(symbol, symbol)); + } + } + + for (const auto& asset : AppModel::getInstance().getAssets()->getAssetsList()) + { + auto assetId = asset.value("assetId").toUInt(); + if (assetId == 0) + { + continue; // plain BEAM is covered by the classic pairs already + } + auto unitName = asset.value("unitName").toString(); + if (!unitName.isEmpty()) + { + result.push_back(MakeCoinFilterEntry(unitName, unitName)); + } + } + + return result; +} + +QList> SwapOffersViewModel::getCustomTokenCards() const +{ + QList> result; + + auto ethClient = AppModel::getInstance().getSwapEthClient(); + if (!ethClient || ethClient->GetSettings().IsActivated() == false) + { + return result; // match the DAI/USDT/WBTC cards' visibility rule: eth client must be connected + } + + for (const auto& token : AppModel::getInstance().getSettings().getEthCustomTokens()) + { + const auto contract = token.value("contract").toString(); + const auto symbol = token.value("symbol").toString(); + const auto decimals = static_cast(token.value("decimals").toUInt()); + if (contract.isEmpty() || symbol.isEmpty()) + { + continue; + } + + const auto walletDecimals = beamui::tokenWalletDecimals(decimals); + const auto balance = ethClient->getTokenBalance(contract); + + QMap card; + card.insert("contract", contract); + card.insert("symbol", symbol); + card.insert("available", beamui::AmountToUIStringExactDecimals(balance, walletDecimals)); + card.insert("color", beamui::ColorFromString(contract)); + card.insert("isConnected", ethClient->getStatus() == beam::ethereum::Client::Status::Connected); + result.push_back(card); + } + + return result; +} + SwapCoinClientWrapper* SwapOffersViewModel::getSwapCoinClientWrapper(beam::wallet::AtomicSwapCoin swapCoinType) const { auto it = std::find_if(m_swapClientWrappers.cbegin(), m_swapClientWrappers.cend(), diff --git a/ui/viewmodel/atomic_swap/swap_offers_view.h b/ui/viewmodel/atomic_swap/swap_offers_view.h index 2e109ba33..14fd0600b 100644 --- a/ui/viewmodel/atomic_swap/swap_offers_view.h +++ b/ui/viewmodel/atomic_swap/swap_offers_view.h @@ -83,6 +83,13 @@ class SwapOffersViewModel : public QObject Q_PROPERTY(bool isOffersLoaded READ isOffersLoaded NOTIFY offersLoaded) Q_PROPERTY(int activeTxCount READ getActiveTxCount NOTIFY allTransactionsChanged) Q_PROPERTY(QQmlListProperty swapClientList READ getSwapClients CONSTANT) + // {"text","pair"} entries for the currency filter combo: the fixed classic + // coins plus one entry per stored custom ERC-20 token and (if the wallet + // holds any) per Confidential Asset + Q_PROPERTY(QList> coinFilterList READ getCoinFilterList NOTIFY coinFilterListChanged) + // one card per stored custom ERC-20 token, live balance from the eth client; + // {"contract","symbol","available","color"} -- only meaningful while the eth client is connected + Q_PROPERTY(QList> customTokenCards READ getCustomTokenCards NOTIFY customTokenCardsChanged) public: SwapOffersViewModel(); @@ -96,6 +103,8 @@ class SwapOffersViewModel : public QObject bool isOffersLoaded() const; int getActiveTxCount() const; QQmlListProperty getSwapClients(); + QList> getCoinFilterList() const; + QList> getCustomTokenCards() const; Q_INVOKABLE void cancelOffer(const QVariant& variantTxID); Q_INVOKABLE void cancelTx(const QVariant& variantTxID); @@ -118,6 +127,8 @@ public slots: void beamAvailableChanged(); void offerRemovedFromTable(QVariant variantTxID); void offersLoaded(); + void coinFilterListChanged(); + void customTokenCardsChanged(); private: void monitorAllOffersFitBalance(); diff --git a/ui/viewmodel/atomic_swap/swap_tx_object.cpp b/ui/viewmodel/atomic_swap/swap_tx_object.cpp index 410dc2a1b..95f114595 100644 --- a/ui/viewmodel/atomic_swap/swap_tx_object.cpp +++ b/ui/viewmodel/atomic_swap/swap_tx_object.cpp @@ -13,6 +13,7 @@ // limitations under the License. #include "swap_tx_object.h" +#include "swap_utils.h" #include "wallet/transactions/swaps/common.h" #include "wallet/transactions/swaps/swap_transaction.h" #include "core/ecc.h" @@ -20,6 +21,7 @@ #include "model/app_model.h" #include "viewmodel/fee_helpers.h" #include "viewmodel/ui_helpers.h" +#include using namespace beam; @@ -67,6 +69,23 @@ namespace return ""; } + QString getCoinsUnlockNoteStr(const beam::wallet::SwapTxDescription& tx, Height currentHeight) + { + if (tx.isRedeemTxRegistered()) + { + return ""; + } + + auto minHeightRefund = tx.getMinRefundTxHeight(); + if (minHeightRefund && currentHeight < *minHeightRefund) + { + QString time = beamui::convertBeamHeightDiffToTime(*minHeightRefund - currentHeight); + //% "While the swap is in progress, if the other side goes offline your coins will be automatically unlocked in %1 at most." + return qtTrId("swap-details-unlock-note").arg(time); + } + return ""; + } + QString getInProgressRefundingStr(const beam::wallet::SwapTxDescription& tx, double blocksPerHour, Height currentBeamHeight) { @@ -175,6 +194,10 @@ bool SwapTxObject::isDeleteAvailable() const auto SwapTxObject::getSwapCoinName() const -> QString { + if (m_swapTx.getSwapCoin() == beam::wallet::AtomicSwapCoin::Erc20Token) + { + return swapui::erc20Symbol(_tx); + } return toString(beamui::convertSwapCoinToCurrency(m_swapTx.getSwapCoin())); } @@ -189,12 +212,38 @@ QString SwapTxObject::getSentAmountWithCurrency() const QString SwapTxObject::getAmountWithCurrency() const { + beam::Asset::ID assetId = 0; + _tx.GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetID, assetId); + if (assetId != 0) + { + std::string assetName; + _tx.GetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetName, assetName); + return beamui::AmountToUIString(_tx.m_amount, QString::fromStdString(assetName)); + } return AmountToUIString(_tx.m_amount, beamui::Currencies::Beam); } +QString SwapTxObject::getSwapCoinAmountString(beam::Amount value, bool withCurrency) const +{ + if (m_swapTx.getSwapCoin() == beam::wallet::AtomicSwapCoin::Erc20Token) + { + return swapui::erc20AmountString(_tx, value, withCurrency); + } + return withCurrency ? AmountToUIString(value, beamui::convertSwapCoinToCurrency(m_swapTx.getSwapCoin())) + : beamui::AmountToUIString(value); +} + QString SwapTxObject::getSentAmount() const { - QString amount = beamui::AmountToUIString(getSentAmountValue()); + QString amount; + if (_tx.m_txType == beam::wallet::TxType::AtomicSwap && !m_swapTx.isBeamSide()) + { + amount = getSwapCoinAmountString(getSentAmountValue(), false); + } + else + { + amount = beamui::AmountToUIString(getSentAmountValue()); + } return amount == "0" ? "" : amount; } @@ -219,7 +268,15 @@ QString SwapTxObject::getReceivedAmountWithCurrency() const QString SwapTxObject::getReceivedAmount() const { - QString amount = beamui::AmountToUIString(getReceivedAmountValue()); + QString amount; + if (_tx.m_txType == beam::wallet::TxType::AtomicSwap && m_swapTx.isBeamSide()) + { + amount = getSwapCoinAmountString(getReceivedAmountValue(), false); + } + else + { + amount = beamui::AmountToUIString(getReceivedAmountValue()); + } return amount == "0" ? "" : amount; } @@ -238,7 +295,7 @@ QString SwapTxObject::getSwapAmountWithCurrency(bool sent) const bool s = sent ? !isBeamSide : isBeamSide; if (s) { - return AmountToUIString(m_swapTx.getSwapAmount(), beamui::convertSwapCoinToCurrency(m_swapTx.getSwapCoin())); + return getSwapCoinAmountString(m_swapTx.getSwapAmount(), true); } return getAmountWithCurrency(); } @@ -345,6 +402,42 @@ QString SwapTxObject::getStateDetails() const return ""; } +QString SwapTxObject::getCoinsUnlockNote() const +{ + if (getTxDescription().m_txType == beam::wallet::TxType::AtomicSwap) + { + switch (getTxDescription().m_status) + { + case beam::wallet::TxStatus::Pending: + case beam::wallet::TxStatus::InProgress: + { + Height currentHeight = AppModel::getInstance().getWalletModel()->getCurrentHeight(); + auto state = m_swapTx.getState(); + if (state) + { + switch (*state) + { + case wallet::AtomicSwapTransaction::State::BuildingBeamLockTX: + case wallet::AtomicSwapTransaction::State::BuildingBeamRefundTX: + case wallet::AtomicSwapTransaction::State::BuildingBeamRedeemTX: + case wallet::AtomicSwapTransaction::State::HandlingContractTX: + case wallet::AtomicSwapTransaction::State::SendingBeamLockTX: + case wallet::AtomicSwapTransaction::State::SendingRedeemTX: + case wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX: + return getCoinsUnlockNoteStr(m_swapTx, currentHeight); + default: + break; + } + } + break; + } + default: + break; + } + } + return ""; +} + beam::wallet::AtomicSwapCoin SwapTxObject::getSwapCoinType() const { return m_swapTx.getSwapCoin(); diff --git a/ui/viewmodel/atomic_swap/swap_tx_object.h b/ui/viewmodel/atomic_swap/swap_tx_object.h index 02d4a0058..eed094b03 100644 --- a/ui/viewmodel/atomic_swap/swap_tx_object.h +++ b/ui/viewmodel/atomic_swap/swap_tx_object.h @@ -48,6 +48,7 @@ class SwapTxObject : public TxObject auto getFee() const -> QString override; auto getFailureReason() const -> QString override; QString getStateDetails() const override; + QString getCoinsUnlockNote() const; beam::wallet::AtomicSwapCoin getSwapCoinType() const; auto getStatus() const -> QString override; QString getAmountWithCurrency() const; @@ -70,6 +71,7 @@ class SwapTxObject : public TxObject private: auto getSwapAmountValue(bool sent) const -> beam::Amount; auto getSwapAmountWithCurrency(bool sent) const -> QString; + QString getSwapCoinAmountString(beam::Amount value, bool withCurrency) const; beam::wallet::SwapTxDescription m_swapTx; uint32_t m_lockTxMinConfirmations = 0; diff --git a/ui/viewmodel/atomic_swap/swap_tx_object_list.cpp b/ui/viewmodel/atomic_swap/swap_tx_object_list.cpp index e96f8d30d..60b1a9517 100644 --- a/ui/viewmodel/atomic_swap/swap_tx_object_list.cpp +++ b/ui/viewmodel/atomic_swap/swap_tx_object_list.cpp @@ -72,7 +72,8 @@ auto SwapTxObjectList::roleNames() const -> QHash { static_cast(Roles::BeamLockTxKernelId), "beamLockTxKernelId" }, { static_cast(Roles::BeamRedeemTxKernelId), "beamRedeemTxKernelId" }, { static_cast(Roles::BeamRefundTxKernelId), "beamRefundTxKernelId" }, - { static_cast(Roles::StateDetails), "stateDetails" } + { static_cast(Roles::StateDetails), "stateDetails" }, + { static_cast(Roles::CoinsUnlockNote), "coinsUnlockNote" } }; return roles; } @@ -241,6 +242,9 @@ auto SwapTxObjectList::data(const QModelIndex &index, int role) const -> QVarian case Roles::StateDetails: return value->getStateDetails(); + case Roles::CoinsUnlockNote: + return value->getCoinsUnlockNote(); + default: return QVariant(); } diff --git a/ui/viewmodel/atomic_swap/swap_tx_object_list.h b/ui/viewmodel/atomic_swap/swap_tx_object_list.h index 21843a7fa..f7a8471a1 100644 --- a/ui/viewmodel/atomic_swap/swap_tx_object_list.h +++ b/ui/viewmodel/atomic_swap/swap_tx_object_list.h @@ -75,7 +75,8 @@ class SwapTxObjectList : public ListModel> BeamLockTxKernelId, BeamRedeemTxKernelId, BeamRefundTxKernelId, - StateDetails + StateDetails, + CoinsUnlockNote }; Q_ENUM(Roles) diff --git a/ui/viewmodel/atomic_swap/swap_utils.cpp b/ui/viewmodel/atomic_swap/swap_utils.cpp index 4d199ce7f..d0a917325 100644 --- a/ui/viewmodel/atomic_swap/swap_utils.cpp +++ b/ui/viewmodel/atomic_swap/swap_utils.cpp @@ -13,6 +13,9 @@ // limitations under the License. #include "swap_utils.h" #include "viewmodel/qml_globals.h" +#include "viewmodel/ui_helpers.h" +#include "model/app_model.h" +#include "wallet/transactions/swaps/bridges/ethereum/common.h" namespace swapui { @@ -62,4 +65,67 @@ namespace swapui return result; } + + QString erc20Symbol(const beam::wallet::TxParameters& params) + { + std::string symbol; + params.GetParameter(beam::wallet::TxParameterID::AtomicSwapTokenSymbol, symbol); + return symbol.empty() ? QString("ERC20") : QString::fromStdString(symbol); + } + + QString erc20AmountString(const beam::wallet::TxParameters& params, beam::Amount value, bool withSymbol) + { + uint8_t onChainDecimals = 0; + params.GetParameter(beam::wallet::TxParameterID::AtomicSwapTokenDecimals, onChainDecimals); + // exact-decimals variant: tokenWalletDecimals(0) is a valid 0, which the + // unitName overload would silently replace with the BEAM default + auto amountStr = beamui::AmountToUIStringExactDecimals(value, beamui::tokenWalletDecimals(onChainDecimals)); + if (!withSymbol) + { + return amountStr; + } + std::string symbol; + params.GetParameter(beam::wallet::TxParameterID::AtomicSwapTokenSymbol, symbol); + return symbol.empty() ? amountStr : amountStr + " " + QString::fromStdString(symbol); + } + + bool isTokenSide(OldWalletCurrency::OldCurrency currency, const QString& tokenContract) + { + return currency == OldWalletCurrency::OldCurrency::CurrEthereum && !tokenContract.isEmpty(); + } + + uint8_t effectiveSwapDecimals(OldWalletCurrency::OldCurrency currency, const QString& tokenContract, uint32_t tokenDecimals) + { + if (isTokenSide(currency, tokenContract)) + { + return beamui::tokenWalletDecimals(tokenDecimals); + } + return beamui::getCurrencyDecimals(convertCurrency(currency)); + } + + void connectFeeRateClients(QObject* receiver, const std::function& onChanged) + { + if (auto ethClient = AppModel::getInstance().getSwapEthClient(); ethClient) + { + QObject::connect(ethClient.get(), &SwapEthClientModel::estimatedFeeRateChanged, receiver, onChanged); + } + for (auto coin : {beam::wallet::AtomicSwapCoin::Bitcoin, beam::wallet::AtomicSwapCoin::Litecoin, + beam::wallet::AtomicSwapCoin::Qtum, beam::wallet::AtomicSwapCoin::Dogecoin, + beam::wallet::AtomicSwapCoin::Dash, beam::wallet::AtomicSwapCoin::Bitcoin_Cash}) + { + if (auto client = AppModel::getInstance().getSwapCoinClient(coin); client) + { + QObject::connect(client.get(), &SwapCoinClientModel::estimatedFeeRateChanged, receiver, onChanged); + } + } + } + + bool enoughEthForTokenLock(beam::Amount feeRate) + { + // no live balance for arbitrary custom tokens: only check the ETH + // that pays the lock gas (incl. the approve calls, kApproveTxGasLimit) + const beam::Amount lockFee = feeRate * + (beam::ethereum::kLockTxGasLimit + 2 * beam::ethereum::kApproveTxGasLimit); + return AppModel::getInstance().getSwapEthClient()->getAvailable(beam::wallet::AtomicSwapCoin::Ethereum) >= lockFee; + } } diff --git a/ui/viewmodel/atomic_swap/swap_utils.h b/ui/viewmodel/atomic_swap/swap_utils.h index 8b60bc4ac..47222e850 100644 --- a/ui/viewmodel/atomic_swap/swap_utils.h +++ b/ui/viewmodel/atomic_swap/swap_utils.h @@ -14,10 +14,25 @@ #pragma once #include +#include #include "viewmodel/currencies.h" +#include "wallet/core/common.h" namespace swapui { QString getSwapFeeTitle(OldWalletCurrency::OldCurrency currency); QList> getUICurrList(); + + // ERC-20 symbol from the offer/tx parameters, "ERC20" when absent + QString erc20Symbol(const beam::wallet::TxParameters& params); + // token amount in wallet units, optionally suffixed with the symbol + QString erc20AmountString(const beam::wallet::TxParameters& params, beam::Amount value, bool withSymbol); + // true when @currency is the pair side a custom ERC-20 token rides on + bool isTokenSide(OldWalletCurrency::OldCurrency currency, const QString& tokenContract); + // token side -> tokenWalletDecimals, otherwise the classic per-currency table + uint8_t effectiveSwapDecimals(OldWalletCurrency::OldCurrency currency, const QString& tokenContract, uint32_t tokenDecimals); + // connect every swap-coin client's estimatedFeeRateChanged to @onChanged + void connectFeeRateClients(QObject* receiver, const std::function& onChanged); + // the ETH balance covers a token lock's gas (incl. the approve calls) at @feeRate + bool enoughEthForTokenLock(beam::Amount feeRate); } diff --git a/ui/viewmodel/fee_helpers.cpp b/ui/viewmodel/fee_helpers.cpp index c55a727ce..3fa5ee778 100644 --- a/ui/viewmodel/fee_helpers.cpp +++ b/ui/viewmodel/fee_helpers.cpp @@ -25,6 +25,7 @@ #include "wallet/transactions/swaps/bridges/dogecoin/dogecoin_side.h" #include "wallet/transactions/swaps/bridges/ethereum/ethereum_side.h" #include "wallet/transactions/swaps/utils.h" +#include "ui_helpers.h" beam::Amount minFeeBeam(bool isShielded) { @@ -148,7 +149,8 @@ QString calcWithdrawTxFee(OldWalletCurrency::OldCurrency currency, beam::Amount case OldWalletCurrency::OldCurrency::CurrWrappedBTC: { auto swapCoin = convertCurrencyToSwapCoin(currency); auto total = beam::wallet::EthereumSide::CalcWithdrawTxFee(feeRate, swapCoin); - return QString::fromStdString(std::to_string(total)) + " gwei"; + // the withdraw tx fee is paid in ETH, also for token withdrawals + return beamui::AmountToUIString(total, beamui::Currencies::Ethereum); } default: { return QString(); diff --git a/ui/viewmodel/qml_globals.cpp b/ui/viewmodel/qml_globals.cpp index 0d9da48b4..d1317e9b8 100644 --- a/ui/viewmodel/qml_globals.cpp +++ b/ui/viewmodel/qml_globals.cpp @@ -265,7 +265,8 @@ bool QMLGlobals::haveSwapClient(OldWalletCurrency::OldCurrency currency) { return AppModel::getInstance().getSwapEthClient()->GetSettings().IsActivated(); } - return AppModel::getInstance().getSwapCoinClient(swapCoin)->GetSettings().IsActivated(); + auto client = AppModel::getInstance().getSwapCoinClient(swapCoin); + return client && client->GetSettings().IsActivated(); } QString QMLGlobals::rawTxParametrsToTokenStr(const QVariant& variantTxParams) @@ -292,7 +293,7 @@ bool QMLGlobals::canReceive(OldWalletCurrency::OldCurrency currency) return client->GetSettings().IsActivated() && client->getStatus() == beam::ethereum::Client::Status::Connected; } auto client = AppModel::getInstance().getSwapCoinClient(swapCoin); - return client->GetSettings().IsActivated() && client->getStatus() == beam::bitcoin::Client::Status::Connected; + return client && client->GetSettings().IsActivated() && client->getStatus() == beam::bitcoin::Client::Status::Connected; } QString QMLGlobals::getBeamUnit() const @@ -407,7 +408,8 @@ QString QMLGlobals::getRecommendedFee(OldWalletCurrency::OldCurrency currency) } auto swapCoin = convertCurrencyToSwapCoin(currency); - return QString::fromStdString(std::to_string(AppModel::getInstance().getSwapCoinClient(swapCoin)->getEstimatedFeeRate())); + auto client = AppModel::getInstance().getSwapCoinClient(swapCoin); + return client ? QString::fromStdString(std::to_string(client->getEstimatedFeeRate())) : QString::fromStdString(std::to_string(0)); } QString QMLGlobals::getDefaultFee(OldWalletCurrency::OldCurrency currency) @@ -416,14 +418,7 @@ QString QMLGlobals::getDefaultFee(OldWalletCurrency::OldCurrency currency) { return QString::fromStdString(std::to_string(minFeeBeam())); } - - if (isEthereumBased(currency)) - { - return QString::fromStdString(std::to_string(AppModel::getInstance().getSwapEthClient()->getGasPrice())); - } - - auto swapCoin = convertCurrencyToSwapCoin(currency); - return QString::fromStdString(std::to_string(AppModel::getInstance().getSwapCoinClient(swapCoin)->getEstimatedFeeRate())); + return getRecommendedFee(currency); } QString QMLGlobals::divideWithPrecision(const QString& dividend, const QString& divider, uint precision) diff --git a/ui/viewmodel/receive_swap_view.cpp b/ui/viewmodel/receive_swap_view.cpp index f84034e7e..b0238eda2 100644 --- a/ui/viewmodel/receive_swap_view.cpp +++ b/ui/viewmodel/receive_swap_view.cpp @@ -20,7 +20,11 @@ #include "qml_globals.h" #include "fee_helpers.h" #include "wallet/transactions/swaps/bridges/ethereum/ethereum_side.h" +#include "wallet/transactions/swaps/bridges/ethereum/common.h" #include "atomic_swap/swap_utils.h" +#include "model/assets_manager.h" +#include "model/settings.h" +#include namespace { enum @@ -82,11 +86,70 @@ ReceiveSwapViewModel::ReceiveSwapViewModel() connect(_walletModel, &WalletModel::coinsSelected, this, &ReceiveSwapViewModel::onCoinsSelected); connect(_rates.get(), &ExchangeRatesManager::rateUnitChanged, this, &ReceiveSwapViewModel::secondCurrencyUnitNameChanged); connect(_rates.get(), &ExchangeRatesManager::activeRateChanged, this, &ReceiveSwapViewModel::secondCurrencyRateChanged); + connect(AppModel::getInstance().getAssets().get(), &AssetsManager::assetsListChanged, this, [this]() + { + // assets feed currList (one entry per asset), so a change there + // reshuffles both the combo model and index mapping + emit currListChanged(); + emit sentCurrencyIndexChanged(); + emit receiveCurrencyIndexChanged(); + if (_selectedBeamAssetId != 0) + { + // drop the selection if its asset vanished from the wallet + for (const auto& asset : AppModel::getInstance().getAssets()->getAssetsList()) + { + if (asset.value("assetId").toUInt() == static_cast(_selectedBeamAssetId)) + { + return; + } + } + setSelectedBeamAssetId(0); + } + }); + connect(&AppModel::getInstance().getSettings(), &WalletSettings::ethCustomTokensChanged, this, [this]() + { + // the stored token list feeds currList (one entry per token), so a + // change there reshuffles both the combo model and index mapping + emit currListChanged(); + emit sentCurrencyIndexChanged(); + emit receiveCurrencyIndexChanged(); + if (!_selectedTokenContract.isEmpty()) + { + // drop the selection if its token was removed in the settings + for (const auto& token : getCustomTokensList()) + { + if (token.value("contract").toString() == _selectedTokenContract) + { + return; + } + } + setSelectedTokenContract(QString()); + } + }); + // the combo index depends on both the settled currency and (for the + // Ethereum side) which token is stamped; keep it in sync with either + connect(this, &ReceiveSwapViewModel::sentCurrencyChanged, this, &ReceiveSwapViewModel::sentCurrencyIndexChanged); + connect(this, &ReceiveSwapViewModel::receiveCurrencyChanged, this, &ReceiveSwapViewModel::receiveCurrencyIndexChanged); + connect(this, &ReceiveSwapViewModel::selectedTokenChanged, this, &ReceiveSwapViewModel::sentCurrencyIndexChanged); + connect(this, &ReceiveSwapViewModel::selectedTokenChanged, this, &ReceiveSwapViewModel::receiveCurrencyIndexChanged); + connect(this, &ReceiveSwapViewModel::selectedBeamAssetChanged, this, &ReceiveSwapViewModel::sentCurrencyIndexChanged); + connect(this, &ReceiveSwapViewModel::selectedBeamAssetChanged, this, &ReceiveSwapViewModel::receiveCurrencyIndexChanged); + + swapui::connectFeeRateClients(this, [this]() + { + ++_feeRatesRevision; + emit feeRatesRevisionChanged(); + }); generateNewAddress(); updateTransactionToken(); } +unsigned int ReceiveSwapViewModel::getFeeRatesRevision() const +{ + return _feeRatesRevision; +} + //void ReceiveSwapViewModel::onGeneratedNewAddress(const beam::wallet::WalletAddress& addr) //{ // _receiverAddress = addr; @@ -135,13 +198,13 @@ QString ReceiveSwapViewModel::getRate() const if (!beamAmount) return QString(); - beamui::Currencies otherCurrency = - convertCurrency(isSendBeam() ? _receiveCurrency : _sentCurrency); + auto otherOldCurrency = isSendBeam() ? _receiveCurrency : _sentCurrency; + uint8_t otherDecimals = effectiveDecimals(otherOldCurrency); return QMLGlobals::divideWithPrecision( - beamui::AmountToUIString(otherCoinAmount, otherCurrency, false), + beamui::AmountToUIStringExactDecimals(otherCoinAmount, otherDecimals), beamui::AmountToUIString(beamAmount), - beamui::getCurrencyDecimals(otherCurrency)); + otherDecimals); } void ReceiveSwapViewModel::onSwapParamsLoaded(const beam::ByteBuffer& params) @@ -202,12 +265,12 @@ void ReceiveSwapViewModel::onCoinsSelected(const beam::wallet::CoinsSelectionInf QString ReceiveSwapViewModel::getAmountToReceive() const { - return beamui::AmountToUIString(_amountToReceiveGrothes, convertCurrency(_receiveCurrency), false); + return beamui::AmountToUIStringExactDecimals(_amountToReceiveGrothes, effectiveDecimals(_receiveCurrency)); } void ReceiveSwapViewModel::setAmountToReceive(QString value) { - auto amount = beamui::UIStringToAmount(value, convertCurrency(_receiveCurrency)); + auto amount = beamui::UIStringToAmountExactDecimals(value, effectiveDecimals(_receiveCurrency)); if (amount != _amountToReceiveGrothes) { _amountToReceiveGrothes = amount; @@ -219,7 +282,7 @@ void ReceiveSwapViewModel::setAmountToReceive(QString value) QString ReceiveSwapViewModel::getAmountSent() const { - return beamui::AmountToUIString(_amountSentGrothes, convertCurrency(_sentCurrency), false); + return beamui::AmountToUIStringExactDecimals(_amountSentGrothes, effectiveDecimals(_sentCurrency)); } unsigned int ReceiveSwapViewModel::getReceiveFee() const @@ -229,7 +292,7 @@ unsigned int ReceiveSwapViewModel::getReceiveFee() const void ReceiveSwapViewModel::setAmountSent(QString value) { - auto amount = beamui::UIStringToAmount(value, convertCurrency(_sentCurrency)); + auto amount = beamui::UIStringToAmountExactDecimals(value, effectiveDecimals(_sentCurrency)); if (amount != _amountSentGrothes) { bool isPreviouseSendWasZero = _amountSentGrothes == 0; @@ -292,6 +355,9 @@ void ReceiveSwapViewModel::setReceiveCurrency(OldWalletCurrency::OldCurrency val // different units for different currencies. example BTC and ETH QString amount = getAmountToReceive(); _receiveCurrency = value; + // deferred: the QML sides-flip updates sent/receive currency one binding at + // a time, so the pair passes through transient states here + QMetaObject::invokeMethod(this, &ReceiveSwapViewModel::syncExtendedSelections, Qt::QueuedConnection); setAmountToReceive(amount); emit receiveCurrencyChanged(); updateTransactionToken(); @@ -315,6 +381,9 @@ void ReceiveSwapViewModel::setSentCurrency(OldWalletCurrency::OldCurrency value) // different units for different currencies. example BTC and ETH QString amount = getAmountSent(); _sentCurrency = value; + // deferred: the QML sides-flip updates sent/receive currency one binding at + // a time, so the pair passes through transient states here + QMetaObject::invokeMethod(this, &ReceiveSwapViewModel::syncExtendedSelections, Qt::QueuedConnection); setAmountSent(amount); emit sentCurrencyChanged(); updateTransactionToken(); @@ -412,12 +481,23 @@ bool ReceiveSwapViewModel::isEnough() const if (_sentCurrency == OldWalletCurrency::OldCurrency::CurrBeam) { + if (_selectedBeamAssetId != 0) + { + // the offered value is a Confidential Asset; the fee is still paid in BEAM + return _walletModel->getAvailable(_selectedBeamAssetId) >= beam::AmountBig::Number(_amountSentGrothes) && + _walletModel->getAvailable(beam::Asset::s_BeamID) >= beam::AmountBig::Number(_sentFeeGrothes); + } return _walletModel->getAvailable(beam::Asset::s_BeamID) >= total; } auto swapCoin = convertCurrencyToSwapCoin(_sentCurrency); if (isEthereumBased(_sentCurrency)) { + if (isTokenSide(_sentCurrency)) + { + return swapui::enoughEthForTokenLock(_sentFeeGrothes); + } + if (_sentCurrency == OldWalletCurrency::OldCurrency::CurrEthereum) { total = _amountSentGrothes + beam::wallet::EthereumSide::CalcLockTxFee(_sentFeeGrothes, swapCoin); @@ -517,8 +597,14 @@ void ReceiveSwapViewModel::updateTransactionToken() emit isReceiveFeeOKChanged(); _isBeamSide = (_sentCurrency == OldWalletCurrency::OldCurrency::CurrBeam); - auto swapCoin = convertCurrencyToSwapCoin( - _isBeamSide ? _receiveCurrency : _sentCurrency); + const auto otherCurrency = _isBeamSide ? _receiveCurrency : _sentCurrency; + + // a custom ERC-20 token replaces plain Ethereum as the swap coin: the core + // (EthereumSide::IsERC20Token, offers board ExtendedOffer wire coin) keys on + // AtomicSwapCoin::Erc20Token, the contract itself travels in param 43 + const bool tokenActive = isTokenSide(otherCurrency); + auto swapCoin = tokenActive ? beam::wallet::AtomicSwapCoin::Erc20Token + : convertCurrencyToSwapCoin(otherCurrency); auto beamAmount = _isBeamSide ? _amountSentGrothes : _amountToReceiveGrothes; auto swapAmount = @@ -529,13 +615,35 @@ void ReceiveSwapViewModel::updateTransactionToken() QPointer guard = this; - auto onSwapParams = [guard, this](beam::wallet::TxParameters&& params) + auto onSwapParams = [guard, this, tokenActive](beam::wallet::TxParameters&& params) { if (!guard) return; _txParameters = std::move(params); + // both stampings re-checked against the current pair here: stale + // selections must never ride on an unrelated offer + if (_selectedBeamAssetId != 0 && + (_sentCurrency == OldWalletCurrency::OldCurrency::CurrBeam || + _receiveCurrency == OldWalletCurrency::OldCurrency::CurrBeam)) + { + _txParameters.SetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetID, + beam::Asset::ID(_selectedBeamAssetId)); + _txParameters.SetParameter(beam::wallet::TxParameterID::AtomicSwapBeamAssetName, + getSelectedBeamAssetUnitName().toStdString()); + } + + if (tokenActive && !_selectedTokenContract.isEmpty()) + { + _txParameters.SetParameter(beam::wallet::TxParameterID::AtomicSwapTokenContract, + _selectedTokenContract.toStdString()); + _txParameters.SetParameter(beam::wallet::TxParameterID::AtomicSwapTokenSymbol, + _selectedTokenSymbol.toStdString()); + _txParameters.SetParameter(beam::wallet::TxParameterID::AtomicSwapTokenDecimals, + static_cast(_selectedTokenDecimals)); + } + #ifdef BEAM_CLIENT_VERSION _txParameters.SetParameter( beam::wallet::TxParameterID::ClientVersion, @@ -611,5 +719,285 @@ QString ReceiveSwapViewModel::getReceiveFeeTitle() const QList> ReceiveSwapViewModel::getCurrList() const { - return swapui::getUICurrList(); + auto list = swapui::getUICurrList(); + + // Confidential Assets ride the BEAM swap side; each known asset gets its + // own entry appended after the classic currencies so it is picked in the + // same combo as BEAM/BTC/.../WBTC. "assetId" marks these entries for + // selectCurrencyByListIndex/currencyToListIndex. + for (const auto& asset : AppModel::getInstance().getAssets()->getAssetsList()) + { + const auto assetId = asset.value("assetId").toUInt(); + if (assetId == static_cast(beam::Asset::s_BeamID)) + { + continue; + } + + QMap entry; + const auto name = asset.value("unitName").toString(); + + entry.insert("isBEAM", true); + entry.insert("unitName", name); + entry.insert("unitNameWithId", QString("%1 (%2)").arg(name).arg(assetId)); + entry.insert("icon", asset.value("icon")); + entry.insert("iconWidth", 22); + entry.insert("iconHeight", 22); + entry.insert("decimals", beamui::getCurrencyDecimals(beamui::Currencies::Beam)); + entry.insert("assetId", assetId); + + list.push_back(entry); + } + + // stored custom ERC-20 tokens ride the Ethereum swap coin; each gets its + // own entry appended after the classic currencies so it is picked in the + // same combo as BEAM/BTC/.../WBTC (issue #1267). "tokenContract" marks + // these entries for selectCurrencyByListIndex/currencyToListIndex; classic + // entries simply don't carry the key (falsy/empty when read back in QML). + for (const auto& token : getCustomTokensList()) + { + QMap entry; + const auto symbol = token.value("symbol").toString(); + const auto decimals = static_cast(beamui::tokenWalletDecimals(token.value("decimals").toUInt())); + + entry.insert("isBEAM", false); + entry.insert("unitName", symbol); + entry.insert("unitNameWithId", symbol); + // the combo delegate only renders a file icon (SvgImage source path, + // no room for the generated TokenIcon glyph); reuse the ETH icon + entry.insert("icon", "qrc:/assets/icon-eth.svg"); + entry.insert("iconWidth", 22); + entry.insert("iconHeight", 22); + entry.insert("decimals", decimals); + entry.insert("tokenContract", token.value("contract")); + + list.push_back(entry); + } + + return list; +} + +unsigned int ReceiveSwapViewModel::getSelectedBeamAssetId() const +{ + return static_cast(_selectedBeamAssetId); +} + +void ReceiveSwapViewModel::setSelectedBeamAssetId(unsigned int value) +{ + auto assetId = static_cast(value); + if (assetId != _selectedBeamAssetId) + { + _selectedBeamAssetId = assetId; + emit selectedBeamAssetChanged(); + emit currListChanged(); + emit enoughChanged(); + updateTransactionToken(); + } +} + +QString ReceiveSwapViewModel::getSelectedBeamAssetUnitName() const +{ + if (_selectedBeamAssetId == 0) + { + return QMLGlobals::getCurrencyUnitName(OldWalletCurrency::OldCurrency::CurrBeam); + } + return AppModel::getInstance().getAssets()->getUnitName(_selectedBeamAssetId, AssetsManager::NoShorten); +} + +QList> ReceiveSwapViewModel::getCustomTokensList() const +{ + return AppModel::getInstance().getSettings().getEthCustomTokens(); +} + +QString ReceiveSwapViewModel::getSelectedTokenContract() const +{ + return _selectedTokenContract; +} + +void ReceiveSwapViewModel::setSelectedTokenContract(const QString& value) +{ + if (value == _selectedTokenContract) + { + return; + } + + // the eth-side amount is stored in wallet units whose scale follows the + // selected token's decimals: keep the typed value, not the raw units + const bool sentIsEth = _sentCurrency == OldWalletCurrency::OldCurrency::CurrEthereum; + const bool receiveIsEth = _receiveCurrency == OldWalletCurrency::OldCurrency::CurrEthereum; + const QString ethSideAmount = sentIsEth ? getAmountSent() + : receiveIsEth ? getAmountToReceive() : QString(); + + _selectedTokenContract = value; + _selectedTokenSymbol.clear(); + _selectedTokenDecimals = 0; + + if (!value.isEmpty()) + { + for (const auto& token : getCustomTokensList()) + { + if (token.value("contract").toString() == value) + { + _selectedTokenSymbol = token.value("symbol").toString(); + _selectedTokenDecimals = token.value("decimals").toUInt(); + break; + } + } + } + + emit selectedTokenChanged(); + emit currListChanged(); + + if (sentIsEth) + { + setAmountSent(ethSideAmount); + emit amountSentChanged(); + } + else if (receiveIsEth) + { + setAmountToReceive(ethSideAmount); + emit amountReceiveChanged(); + } + + updateTransactionToken(); +} + +QString ReceiveSwapViewModel::getSelectedTokenSymbol() const +{ + return _selectedTokenSymbol; +} + +unsigned int ReceiveSwapViewModel::getSelectedTokenDecimals() const +{ + return _selectedTokenDecimals; +} + +int ReceiveSwapViewModel::getSentCurrencyIndex() const +{ + return currencyToListIndex(_sentCurrency); +} + +void ReceiveSwapViewModel::setSentCurrencyIndex(int index) +{ + selectCurrencyByListIndex(true, index); +} + +int ReceiveSwapViewModel::getReceiveCurrencyIndex() const +{ + return currencyToListIndex(_receiveCurrency); +} + +void ReceiveSwapViewModel::setReceiveCurrencyIndex(int index) +{ + selectCurrencyByListIndex(false, index); +} + +int ReceiveSwapViewModel::currencyToListIndex(OldWalletCurrency::OldCurrency currency) const +{ + if (currency == OldWalletCurrency::OldCurrency::CurrBeam && _selectedBeamAssetId != 0) + { + const auto list = getCurrList(); + for (int i = 0; i < list.size(); ++i) + { + if (list[i].value("assetId").toUInt() == static_cast(_selectedBeamAssetId)) + { + return i; + } + } + // the selected asset vanished from the wallet's list; fall back to + // the plain BEAM slot + return static_cast(OldWalletCurrency::OldCurrency::CurrBeam); + } + if (isTokenSide(currency)) + { + const auto list = getCurrList(); + for (int i = 0; i < list.size(); ++i) + { + if (list[i].value("tokenContract").toString() == _selectedTokenContract) + { + return i; + } + } + // the selected token vanished from settings mid-flight (race with + // syncExtendedSelections); fall back to the plain Ethereum slot + return static_cast(OldWalletCurrency::OldCurrency::CurrEthereum); + } + return static_cast(currency); +} + +void ReceiveSwapViewModel::selectCurrencyByListIndex(bool isSent, int index) +{ + const auto list = getCurrList(); + if (index < 0 || index >= list.size()) + { + // a stale/removed entry (e.g. combo model changed underneath a + // pending selection): ignore rather than crash + return; + } + + const auto contract = list[index].value("tokenContract").toString(); + const auto assetId = list[index].value("assetId").toUInt(); + const auto currency = !contract.isEmpty() ? OldWalletCurrency::OldCurrency::CurrEthereum + : assetId != 0 ? OldWalletCurrency::OldCurrency::CurrBeam + : static_cast(index); + + if (!contract.isEmpty()) + { + // stamp the token identity before flipping the enum so + // isTokenSide()/effectiveDecimals() already see it once + // setSentCurrency/setReceiveCurrency runs its amount conversion + setSelectedTokenContract(contract); + } + else if (currency == OldWalletCurrency::OldCurrency::CurrEthereum) + { + // explicit "plain ETH" pick on a side that already was Ethereum: + // syncExtendedSelections() only clears once neither side is + // Ethereum anymore, so drop the stale token stamp here + setSelectedTokenContract(QString()); + } + + if (assetId != 0) + { + setSelectedBeamAssetId(assetId); + } + else if (currency == OldWalletCurrency::OldCurrency::CurrBeam) + { + // explicit plain-BEAM pick drops a stale asset stamp + setSelectedBeamAssetId(0); + } + + if (isSent) + { + setSentCurrency(currency); + } + else + { + setReceiveCurrency(currency); + } +} + +bool ReceiveSwapViewModel::isTokenSide(OldWalletCurrency::OldCurrency currency) const +{ + return swapui::isTokenSide(currency, _selectedTokenContract); +} + +uint8_t ReceiveSwapViewModel::effectiveDecimals(OldWalletCurrency::OldCurrency currency) const +{ + return swapui::effectiveSwapDecimals(currency, _selectedTokenContract, _selectedTokenDecimals); +} + +void ReceiveSwapViewModel::syncExtendedSelections() +{ + if (_selectedBeamAssetId != 0 && + _sentCurrency != OldWalletCurrency::OldCurrency::CurrBeam && + _receiveCurrency != OldWalletCurrency::OldCurrency::CurrBeam) + { + setSelectedBeamAssetId(0); + } + + if (!_selectedTokenContract.isEmpty() && + _sentCurrency != OldWalletCurrency::OldCurrency::CurrEthereum && + _receiveCurrency != OldWalletCurrency::OldCurrency::CurrEthereum) + { + setSelectedTokenContract(QString()); + } } diff --git a/ui/viewmodel/receive_swap_view.h b/ui/viewmodel/receive_swap_view.h index 6d87e3dc7..53b75c23f 100644 --- a/ui/viewmodel/receive_swap_view.h +++ b/ui/viewmodel/receive_swap_view.h @@ -47,6 +47,21 @@ class ReceiveSwapViewModel: public QObject Q_PROPERTY(unsigned int minimalBeamFeeGrothes READ getMinimalBeamFeeGrothes NOTIFY minimalBeamFeeGrothesChanged) Q_PROPERTY(QList> currList READ getCurrList NOTIFY currListChanged) + Q_PROPERTY(unsigned int selectedBeamAssetId READ getSelectedBeamAssetId WRITE setSelectedBeamAssetId NOTIFY selectedBeamAssetChanged) + Q_PROPERTY(QString selectedBeamAssetUnitName READ getSelectedBeamAssetUnitName NOTIFY selectedBeamAssetChanged) + + // custom ERC-20 tokens stored per wallet (settings, added via the ethereum + // settings pane): each is appended to currList as its own entry (after the + // classic currencies) so it is picked in the SAME combo as BEAM/BTC/.../WBTC. + // OldCurrency stays a closed enum (extending it would ripple through every + // exhaustive switch over it); instead the combo index identifies the entry, + // and sentCurrencyIndex/receiveCurrencyIndex translate that index to/from + // sentCurrency==CurrEthereum + the underlying _selectedTokenContract stamp. + Q_PROPERTY(int sentCurrencyIndex READ getSentCurrencyIndex WRITE setSentCurrencyIndex NOTIFY sentCurrencyIndexChanged) + Q_PROPERTY(int receiveCurrencyIndex READ getReceiveCurrencyIndex WRITE setReceiveCurrencyIndex NOTIFY receiveCurrencyIndexChanged) + // bumped on estimatedFeeRateChanged so QML fee bindings re-evaluate + Q_PROPERTY(unsigned int feeRatesRevision READ getFeeRatesRevision NOTIFY feeRatesRevisionChanged) + public: ReceiveSwapViewModel(); @@ -72,6 +87,11 @@ class ReceiveSwapViewModel: public QObject void secondCurrencyUnitNameChanged(); void minimalBeamFeeGrothesChanged(); void currListChanged(); + void selectedBeamAssetChanged(); + void selectedTokenChanged(); + void sentCurrencyIndexChanged(); + void receiveCurrencyIndexChanged(); + void feeRatesRevisionChanged(); public: Q_INVOKABLE void generateNewAddress(); @@ -119,6 +139,13 @@ class ReceiveSwapViewModel: public QObject void loadSwapParams(); void storeSwapParams(); + // true when @currency is the pair side the selected custom ERC-20 token rides on + bool isTokenSide(OldWalletCurrency::OldCurrency currency) const; + // token side -> min(token decimals, 9) (WalletUnitsPerToken rule), otherwise the classic table + uint8_t effectiveDecimals(OldWalletCurrency::OldCurrency currency) const; + // drops the asset/token selections that no longer match the chosen pair + void syncExtendedSelections(); + bool isSendBeam() const; QString getRate() const; @@ -131,6 +158,26 @@ class ReceiveSwapViewModel: public QObject unsigned int getMinimalBeamFeeGrothes() const; QList> getCurrList() const; + unsigned int getSelectedBeamAssetId() const; + void setSelectedBeamAssetId(unsigned int value); + QString getSelectedBeamAssetUnitName() const; + + QList> getCustomTokensList() const; + QString getSelectedTokenContract() const; + void setSelectedTokenContract(const QString& value); + QString getSelectedTokenSymbol() const; + unsigned int getSelectedTokenDecimals() const; + + // combo index (into currList) <-> (sentCurrency/receiveCurrency, _selectedTokenContract) + int getSentCurrencyIndex() const; + void setSentCurrencyIndex(int index); + int getReceiveCurrencyIndex() const; + void setReceiveCurrencyIndex(int index); + int currencyToListIndex(OldWalletCurrency::OldCurrency currency) const; + void selectCurrencyByListIndex(bool isSent, int index); + + unsigned int getFeeRatesRevision() const; + private slots: //void onGeneratedNewAddress(const beam::wallet::WalletAddress& walletAddr); void onSwapParamsLoaded(const beam::ByteBuffer& token); @@ -157,4 +204,14 @@ private slots: beam::Amount _minimalBeamFeeGrothes; bool _feeChangedByUI = false; + + // Confidential Asset being offered on the BEAM side, 0 == plain BEAM + beam::Asset::ID _selectedBeamAssetId = 0; + + // custom ERC-20 token being offered on the non-BEAM side, empty == none + QString _selectedTokenContract; + QString _selectedTokenSymbol; + unsigned int _selectedTokenDecimals = 0; + + unsigned int _feeRatesRevision = 0; }; diff --git a/ui/viewmodel/send_swap_view.cpp b/ui/viewmodel/send_swap_view.cpp index 50afcd254..9b41a5181 100644 --- a/ui/viewmodel/send_swap_view.cpp +++ b/ui/viewmodel/send_swap_view.cpp @@ -20,6 +20,7 @@ #include "fee_helpers.h" #include "atomic_swap/swap_utils.h" #include "wallet/transactions/swaps/bridges/ethereum/ethereum_side.h" +#include "wallet/transactions/swaps/bridges/ethereum/common.h" #include #include @@ -41,6 +42,17 @@ SendSwapViewModel::SendSwapViewModel() connect(_walletModel, &WalletModel::coinsSelected, this, &SendSwapViewModel::onCoinsSelected); connect(_rates.get(), SIGNAL(rateUnitChanged()), SIGNAL(secondCurrencyUnitNameChanged())); connect(_rates.get(), SIGNAL(activeRateChanged()), SIGNAL(secondCurrencyRateChanged())); + + swapui::connectFeeRateClients(this, [this]() + { + ++_feeRatesRevision; + emit feeRatesRevisionChanged(); + }); +} + +unsigned int SendSwapViewModel::getFeeRatesRevision() const +{ + return _feeRatesRevision; } QString SendSwapViewModel::getToken() const @@ -66,19 +78,39 @@ void SendSwapViewModel::fillParameters(const beam::wallet::TxParameters& paramet if (peerAddr && swapAmount && beamAmount && swapCoin && isBeamSide && peerResponseTime && offeredTime && minHeight) { + // extended-offer fields first: the amount/currency setters below key the + // conversion decimals on them for ERC-20 offers + auto tokenContract = parameters.GetParameter(TxParameterID::AtomicSwapTokenContract); + auto tokenSymbol = parameters.GetParameter(TxParameterID::AtomicSwapTokenSymbol); + auto tokenDecimals = parameters.GetParameter(TxParameterID::AtomicSwapTokenDecimals); + _tokenContract = tokenContract ? QString::fromStdString(*tokenContract) : QString(); + _tokenSymbol = tokenSymbol ? QString::fromStdString(*tokenSymbol) : QString(); + _tokenDecimals = tokenDecimals ? *tokenDecimals : 0; + + auto beamAssetId = parameters.GetParameter(TxParameterID::AtomicSwapBeamAssetID); + auto beamAssetName = parameters.GetParameter(TxParameterID::AtomicSwapBeamAssetName); + _beamAssetId = beamAssetId ? *beamAssetId : 0; + _beamAssetUnitName = beamAssetName ? QString::fromStdString(*beamAssetName) : QString(); + + // Erc20Token has no OldCurrency of its own: it is handled as the + // Ethereum currency slot plus the token params above + const auto swapSideCurrency = (*swapCoin == AtomicSwapCoin::Erc20Token) + ? OldWalletCurrency::OldCurrency::CurrEthereum + : convertSwapCoinToCurrency(*swapCoin); + if (*isBeamSide) // other participant is not a beam side { // Do not set fee, it is set automatically based on the currency param setSendCurrency(OldWalletCurrency::OldCurrency::CurrBeam); setSendAmount(beamui::AmountToUIString(*beamAmount)); - setReceiveCurrency(convertSwapCoinToCurrency(*swapCoin)); - setReceiveAmount(beamui::AmountToUIString(*swapAmount, beamui::convertSwapCoinToCurrency(*swapCoin), false)); + setReceiveCurrency(swapSideCurrency); + setReceiveAmount(beamui::AmountToUIStringExactDecimals(*swapAmount, effectiveDecimals(swapSideCurrency))); } else { // Do not set fee, it is set automatically based on the currency param - setSendCurrency(convertSwapCoinToCurrency(*swapCoin)); - setSendAmount(beamui::AmountToUIString(*swapAmount, beamui::convertSwapCoinToCurrency(*swapCoin), false)); + setSendCurrency(swapSideCurrency); + setSendAmount(beamui::AmountToUIStringExactDecimals(*swapAmount, effectiveDecimals(swapSideCurrency))); setReceiveCurrency(OldWalletCurrency::OldCurrency::CurrBeam); setReceiveAmount(beamui::AmountToUIString(*beamAmount)); } @@ -92,6 +124,8 @@ void SendSwapViewModel::fillParameters(const beam::wallet::TxParameters& paramet _txParameters = parameters; _isBeamSide = *isBeamSide; + + emit currListChanged(); // unit names/decimals follow the token/asset fields } _tokenGeneratebByNewAppVersionMessage.clear(); @@ -158,12 +192,12 @@ bool SendSwapViewModel::getParametersValid() const QString SendSwapViewModel::getSendAmount() const { - return beamui::AmountToUIString(_sendAmountGrothes, convertCurrency(_sendCurrency), false); + return beamui::AmountToUIStringExactDecimals(_sendAmountGrothes, effectiveDecimals(_sendCurrency)); } void SendSwapViewModel::setSendAmount(QString value) { - const auto amount = beamui::UIStringToAmount(value, convertCurrency(_sendCurrency)); + const auto amount = beamui::UIStringToAmountExactDecimals(value, effectiveDecimals(_sendCurrency)); if (amount != _sendAmountGrothes) { _sendAmountGrothes = amount; @@ -173,7 +207,7 @@ void SendSwapViewModel::setSendAmount(QString value) if (_sendCurrency == OldWalletCurrency::OldCurrency::CurrBeam && _walletModel->hasShielded(beam::Asset::s_BeamID)) { - _walletModel->getAsync()->selectCoins(_sendAmountGrothes, _sendFeeGrothes, beam::Asset::s_BeamID); + _walletModel->getAsync()->selectCoins(_sendAmountGrothes, _sendFeeGrothes, _beamAssetId); } } } @@ -195,7 +229,7 @@ void SendSwapViewModel::setSendFee(unsigned int value) if (_sendCurrency == OldWalletCurrency::OldCurrency::CurrBeam && _walletModel->hasShielded(beam::Asset::s_BeamID) && _sendAmountGrothes) { _feeChangedByUI = true; - _walletModel->getAsync()->selectCoins(_sendAmountGrothes, _sendFeeGrothes, beam::Asset::s_BeamID); + _walletModel->getAsync()->selectCoins(_sendAmountGrothes, _sendFeeGrothes, _beamAssetId); } } } @@ -220,12 +254,12 @@ void SendSwapViewModel::setSendCurrency(OldWalletCurrency::OldCurrency value) QString SendSwapViewModel::getReceiveAmount() const { - return beamui::AmountToUIString(_receiveAmountGrothes, convertCurrency(_receiveCurrency), false); + return beamui::AmountToUIStringExactDecimals(_receiveAmountGrothes, effectiveDecimals(_receiveCurrency)); } void SendSwapViewModel::setReceiveAmount(QString value) { - const auto amount = beamui::UIStringToAmount(value, convertCurrency(_receiveCurrency)); + const auto amount = beamui::UIStringToAmountExactDecimals(value, effectiveDecimals(_receiveCurrency)); if (amount != _receiveAmountGrothes) { _receiveAmountGrothes = amount; @@ -314,12 +348,12 @@ void SendSwapViewModel::onChangeCalculated(beam::Amount changeAsset, beam::Amoun { using namespace beam; - // only BEAM used in swap for the moment - assert(assetID == Asset::s_BeamID); assert(AmountBig::get_Hi(changeAsset) == 0); - assert(changeBeam == AmountBig::get_Lo(changeAsset)); - _changeGrothes = changeBeam; + // the balance check needs the change of what is being spent: the asset's + // change for an asset spend, BEAM change otherwise (the values coincide + // when assetID is BEAM) + _changeGrothes = AmountBig::get_Lo(changeAsset); emit enoughChanged(); emit canSendChanged(); } @@ -344,8 +378,29 @@ void SendSwapViewModel::onCoinsSelected(const beam::wallet::CoinsSelectionInfo& bool SendSwapViewModel::isEnough() const { auto total = _sendAmountGrothes + _sendFeeGrothes + _changeGrothes; + + // acceptor receives a Confidential Asset on the peer's BEAM side: the + // redeem tx fee is a separate BEAM spend on top of whatever is checked below + if (needsBeamForRedeemFee()) + { + auto beamAvailable = beam::AmountBig::get_Lo(_walletModel->getAvailable(beam::Asset::s_BeamID)); + if (beamAvailable < _receiveFeeGrothes) + { + return false; + } + } + if (OldWalletCurrency::OldCurrency::CurrBeam == _sendCurrency) { + if (_beamAssetId != 0) + { + // sending a Confidential Asset; the tx fee is still paid in BEAM + auto assetAvailable = beam::AmountBig::get_Lo(_walletModel->getAvailable(_beamAssetId)); + auto beamAvailable = beam::AmountBig::get_Lo(_walletModel->getAvailable(beam::Asset::s_BeamID)); + return assetAvailable >= _sendAmountGrothes + _changeGrothes && + beamAvailable >= _sendFeeGrothes; + } + auto available = beam::AmountBig::get_Lo(_walletModel->getAvailable(beam::Asset::s_BeamID)); return available >= total; } @@ -353,6 +408,11 @@ bool SendSwapViewModel::isEnough() const auto swapCoin = convertCurrencyToSwapCoin(_sendCurrency); if (isEthereumBased(_sendCurrency)) { + if (isTokenSide(_sendCurrency)) + { + return swapui::enoughEthForTokenLock(_sendFeeGrothes); + } + if (_sendCurrency == OldWalletCurrency::OldCurrency::CurrEthereum) { total = _sendAmountGrothes + beam::wallet::EthereumSide::CalcLockTxFee(_sendFeeGrothes, swapCoin); @@ -387,7 +447,7 @@ void SendSwapViewModel::recalcAvailable() { case OldWalletCurrency::OldCurrency::CurrBeam: _changeGrothes = 0; - _walletModel->getAsync()->calcChange(_sendAmountGrothes, _sendFeeGrothes, beam::Asset::s_BeamID); + _walletModel->getAsync()->calcChange(_sendAmountGrothes, _sendFeeGrothes, _beamAssetId); return; default: // TODO:SWAP implement for all currencies @@ -481,13 +541,13 @@ QString SendSwapViewModel::getRate() const if (!beamAmount) return QString(); - beamui::Currencies otherCurrency = - convertCurrency(isSendBeam() ? _receiveCurrency : _sendCurrency); + auto otherOldCurrency = isSendBeam() ? _receiveCurrency : _sendCurrency; + uint8_t otherDecimals = effectiveDecimals(otherOldCurrency); return QMLGlobals::divideWithPrecision( - beamui::AmountToUIString(otherCoinAmount, otherCurrency, false), + beamui::AmountToUIStringExactDecimals(otherCoinAmount, otherDecimals), beamui::AmountToUIString(beamAmount), - beamui::getCurrencyDecimals(otherCurrency)); + otherDecimals); } QString SendSwapViewModel::getSecondCurrencySendRateValue() const @@ -536,5 +596,76 @@ QString SendSwapViewModel::getReceiveFeeTitle() const QList> SendSwapViewModel::getCurrList() const { - return swapui::getUICurrList(); + auto list = swapui::getUICurrList(); + + // show the offer's real unit instead of "BEAM"/"ETH" for asset/token offers; + // the token entry also carries the token's wallet decimals + if (_beamAssetId != 0 && !_beamAssetUnitName.isEmpty()) + { + auto& beamEntry = list[static_cast(OldWalletCurrency::OldCurrency::CurrBeam)]; + beamEntry["unitName"] = _beamAssetUnitName; + beamEntry["unitNameWithId"] = _beamAssetUnitName; + beamEntry["assetId"] = static_cast(_beamAssetId); + } + + if (!_tokenContract.isEmpty()) + { + auto& ethEntry = list[static_cast(OldWalletCurrency::OldCurrency::CurrEthereum)]; + const QString symbol = _tokenSymbol.isEmpty() ? QString("ERC20") : _tokenSymbol; + ethEntry["unitName"] = symbol; + ethEntry["unitNameWithId"] = symbol; + ethEntry["decimals"] = static_cast(std::min(_tokenDecimals, 9)); + } + + return list; +} + +bool SendSwapViewModel::isErc20Swap() const +{ + return !_tokenContract.isEmpty(); +} + +QString SendSwapViewModel::getTokenContract() const +{ + return _tokenContract; +} + +QString SendSwapViewModel::getTokenSymbol() const +{ + return _tokenSymbol; +} + +uint SendSwapViewModel::getTokenDecimals() const +{ + return _tokenDecimals; +} + +bool SendSwapViewModel::isBeamAssetSwap() const +{ + return _beamAssetId != 0; +} + +uint SendSwapViewModel::getBeamAssetId() const +{ + return static_cast(_beamAssetId); +} + +QString SendSwapViewModel::getBeamAssetUnitName() const +{ + return _beamAssetUnitName; +} + +bool SendSwapViewModel::needsBeamForRedeemFee() const +{ + return isBeamAssetSwap() && !isSendBeam(); +} + +bool SendSwapViewModel::isTokenSide(OldWalletCurrency::OldCurrency currency) const +{ + return swapui::isTokenSide(currency, _tokenContract); +} + +uint8_t SendSwapViewModel::effectiveDecimals(OldWalletCurrency::OldCurrency currency) const +{ + return swapui::effectiveSwapDecimals(currency, _tokenContract, _tokenDecimals); } diff --git a/ui/viewmodel/send_swap_view.h b/ui/viewmodel/send_swap_view.h index eee122370..4db5bc060 100644 --- a/ui/viewmodel/send_swap_view.h +++ b/ui/viewmodel/send_swap_view.h @@ -55,6 +55,17 @@ class SendSwapViewModel: public QObject Q_PROPERTY(unsigned int minimalBeamFeeGrothes READ getMinimalBeamFeeGrothes NOTIFY minimalBeamFeeGrothesChanged) Q_PROPERTY(QList> currList READ getCurrList NOTIFY currListChanged) + Q_PROPERTY(bool isErc20Swap READ isErc20Swap NOTIFY tokenChanged) + Q_PROPERTY(QString tokenContract READ getTokenContract NOTIFY tokenChanged) + Q_PROPERTY(QString tokenSymbol READ getTokenSymbol NOTIFY tokenChanged) + Q_PROPERTY(uint tokenDecimals READ getTokenDecimals NOTIFY tokenChanged) + Q_PROPERTY(bool isBeamAssetSwap READ isBeamAssetSwap NOTIFY tokenChanged) + Q_PROPERTY(uint beamAssetId READ getBeamAssetId NOTIFY tokenChanged) + Q_PROPERTY(QString beamAssetUnitName READ getBeamAssetUnitName NOTIFY tokenChanged) + Q_PROPERTY(bool needsBeamForRedeemFee READ needsBeamForRedeemFee NOTIFY tokenChanged) + // bumped on estimatedFeeRateChanged so QML fee bindings re-evaluate + Q_PROPERTY(unsigned int feeRatesRevision READ getFeeRatesRevision NOTIFY feeRatesRevisionChanged) + public: SendSwapViewModel(); @@ -113,6 +124,16 @@ class SendSwapViewModel: public QObject QString getReceiveFeeTitle() const; QList> getCurrList() const; + bool isErc20Swap() const; + QString getTokenContract() const; + QString getTokenSymbol() const; + uint getTokenDecimals() const; + bool isBeamAssetSwap() const; + uint getBeamAssetId() const; + QString getBeamAssetUnitName() const; + bool needsBeamForRedeemFee() const; + unsigned int getFeeRatesRevision() const; + public: Q_INVOKABLE void setParameters(const QVariant& parameters); /// used to pass TxParameters directly without Token generation Q_INVOKABLE void sendMoney(); @@ -139,6 +160,7 @@ class SendSwapViewModel: public QObject void tokenGeneratebByNewAppVersion(); void minimalBeamFeeGrothesChanged(); void currListChanged(); + void feeRatesRevisionChanged(); public slots: void onChangeCalculated(beam::Amount changeAsset, beam::Amount changeBeam, beam::Asset::ID assetId); @@ -148,6 +170,11 @@ public slots: void fillParameters(const beam::wallet::TxParameters& parameters); void recalcAvailable(); + // true when @currency is the pair side the offer's custom ERC-20 token rides on + bool isTokenSide(OldWalletCurrency::OldCurrency currency) const; + // token side -> min(token decimals, 9) (WalletUnitsPerToken rule), otherwise the classic table + uint8_t effectiveDecimals(OldWalletCurrency::OldCurrency currency) const; + beam::Amount _sendAmountGrothes; beam::Amount _sendFeeGrothes; OldWalletCurrency::OldCurrency _sendCurrency; @@ -169,4 +196,13 @@ public slots: beam::Amount _minimalBeamFeeGrothes; bool _feeChangedByUI = false; + unsigned int _feeRatesRevision = 0; + + // extended-offer fields (params 41-45), empty/0 when the offer is a + // classic swap + QString _tokenContract; + QString _tokenSymbol; + uint8_t _tokenDecimals = 0; + beam::Asset::ID _beamAssetId = 0; + QString _beamAssetUnitName; }; diff --git a/ui/viewmodel/ui_helpers.cpp b/ui/viewmodel/ui_helpers.cpp index 2d1ba07d4..d0cda6792 100644 --- a/ui/viewmodel/ui_helpers.cpp +++ b/ui/viewmodel/ui_helpers.cpp @@ -131,6 +131,18 @@ namespace beamui return QString::fromStdString(samount) + (unitName.isEmpty() ? "" : " " + unitName); } + QString AmountToUIStringExactDecimals(const Amount& value, uint8_t decimalPlaces) + { + return QString::fromStdString(libbitcoin::encode_base10(value, decimalPlaces)); + } + + beam::Amount UIStringToAmountExactDecimals(const QString& value, uint8_t decimalPlaces) + { + beam::Amount amount = 0; + libbitcoin::decode_base10(amount, value.toStdString(), decimalPlaces, true); + return amount; + } + QString AmountBigToUIString(const beam::AmountBig::Number& value) { beam::wallet::PrintableAmount print(value, true); @@ -466,6 +478,22 @@ namespace beamui return qtTrId("tx-regular"); } + QColor ColorFromString(const QString& identifier) + { + // same fixed palette AssetsManager uses for Confidential Assets (assets_manager.cpp), + // indexed by a hash of the identifier instead of a numeric asset id -- keeps custom + // ERC-20 token icons visually consistent with the rest of the app, purely locally. + static const QColor palette[] = { + QColor("#72fdff"), QColor("#2acf1d"), QColor("#ffbb54"), QColor("#d885ff"), QColor("#008eff"), + QColor("#ff746b"), QColor("#91e300"), QColor("#ffe75a"), QColor("#9643ff"), QColor("#395bff"), + QColor("#ff3b3b"), QColor("#73ff7c"), QColor("#ffa86c"), QColor("#ff3abe"), QColor("#00aee1"), + QColor("#ff5200"), QColor("#6464ff"), QColor("#ff7a21"), QColor("#63afff"), QColor("#c81f68") + }; + constexpr int paletteSize = sizeof(palette) / sizeof(palette[0]); + const auto idx = static_cast(qHash(identifier.toLower())) % paletteSize; + return palette[idx]; + } + QString getReasonString(beam::wallet::TxFailureReason reason) { // clang doesn't allow to make 'auto reasons' so for the moment assertions below are a bit pointles diff --git a/ui/viewmodel/ui_helpers.h b/ui/viewmodel/ui_helpers.h index df93b18e3..a7abb2c56 100644 --- a/ui/viewmodel/ui_helpers.h +++ b/ui/viewmodel/ui_helpers.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include "wallet/core/common.h" #ifdef BEAM_ATOMIC_SWAP_SUPPORT @@ -74,6 +75,16 @@ namespace beamui /// Convert amount to ui string with "." as a separator. With the default @coinType, no currency label added. QString AmountToUIString(const beam::Amount& value, Currencies coinType = Currencies::Unknown, bool currencyLabel = true); QString AmountToUIString(const beam::Amount& value, const QString& unitName, uint8_t decimalPlaces = 0); + /// exact-decimals conversions: 0 is a valid decimal count here, unlike the overloads above/below + QString AmountToUIStringExactDecimals(const beam::Amount& value, uint8_t decimalPlaces); + + // token amounts are quoted in wallet units, min(decimals, 9) fractional + // digits (WalletUnitsPerToken rule, wallet/transactions/swaps/common.cpp) + constexpr uint8_t tokenWalletDecimals(uint32_t onChainDecimals) + { + return onChainDecimals < 9u ? static_cast(onChainDecimals) : uint8_t(9); + } + beam::Amount UIStringToAmountExactDecimals(const QString& value, uint8_t decimalPlaces); QString AmountBigToUIString(const beam::AmountBig::Number& value); // value -> s"value GROTH" @@ -103,5 +114,10 @@ namespace beamui QString GetTokenTypeUIString(const std::string& token, bool choiceOffline); + // deterministic, local-only color for a string identifier (e.g. an ERC-20 contract + // address), drawn from the same fixed palette AssetsManager uses for Confidential + // Assets -- no network lookups, so the same address always maps to the same color. + QColor ColorFromString(const QString& identifier); + QString getReasonString(beam::wallet::TxFailureReason reason); } // namespace beamui