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.
+
+ 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