diff --git a/app/src/androidTest/java/io/novafoundation/nova/MetadataShortenerTest.kt b/app/src/androidTest/java/io/novafoundation/nova/MetadataShortenerTest.kt index e44877fb79..69c6c7cccb 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/MetadataShortenerTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/MetadataShortenerTest.kt @@ -70,7 +70,7 @@ class MetadataShortenerTest : BaseIntegrationTest() { extrinsicBuilder.nativeTransfer(accountId = signer.accountId, amount = BigInteger.ONE) extrinsicBuilder.systemRemark(remark = byteArrayOf(1, 2, 3)) - extrinsicBuilder.build() + extrinsicBuilder.buildExtrinsic() } @Test diff --git a/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt index c0b6c73744..773e2ef19c 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt @@ -89,7 +89,7 @@ class MoonbaseSendIntagrationTest { val extrinsic = extrinsicBuilderFactory.create(chain, signer, accountId) .nativeTransfer(accountId, chain.utilityAsset.planksFromAmount(BigDecimal.ONE), keepAlive = true) - .build() + .buildExtrinsic().extrinsicHex val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic) diff --git a/build.gradle b/build.gradle index ed6932e762..3500232981 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { // App version - versionName = '8.7.0' - versionCode = 155 + versionName = '8.7.2' + versionCode = 156 applicationId = "io.novafoundation.nova" releaseApplicationSuffix = "market" @@ -51,7 +51,7 @@ buildscript { web3jVersion = '4.9.5' - substrateSdkVersion = '2.1.4' + substrateSdkVersion = '2.2.0' gifVersion = '1.2.19' diff --git a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/model/SystemProperties.kt b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/model/SystemProperties.kt index 29ad0ac458..ae801b64b2 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/model/SystemProperties.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/model/SystemProperties.kt @@ -26,7 +26,7 @@ private class WrapToListSerializer : JsonDeserializer> { } return json.asJsonArray.map { - context.deserialize(json, valueType) + context.deserialize(it, valueType) } } } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt index b0f219daa3..8c8b907eef 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt @@ -128,7 +128,6 @@ fun Date.formatDateSinceEpoch(resourceManager: ResourceManager): String { val currentDays = System.currentTimeMillis().daysFromMillis() val diff = currentDays - time.daysFromMillis() - if (diff < 0) throw IllegalArgumentException("Past date should be less than current") return when (diff) { 0L -> resourceManager.getString(R.string.today) 1L -> resourceManager.getString(R.string.yesterday) diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index 1ff7e730c6..9c7ef274da 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -1131,7 +1131,7 @@ Por favor, asegúrate de que la biometría está activada en los Ajustes Biometría desactivada en los Ajustes Comunidad - Email + Obtenga ayuda por Email General Cada operación de firma en monederos con par de claves (creados en nova wallet o importados) debería requerir verificación PIN antes de construir la firma Solicitar autenticación para firmar operaciones @@ -1143,7 +1143,7 @@ Seguridad Soporte & Retroalimentación Twitter - Wiki + Wiki y centro de ayuda Youtube La convicción se establecerá en 0.1x cuando se Abstenga No puedes hacer stake con Staking Directo y Pools de Nominación al mismo tiempo diff --git a/common/src/main/res/values-fr-rFR/strings.xml b/common/src/main/res/values-fr-rFR/strings.xml index 93563f4c2d..df5fbce85c 100644 --- a/common/src/main/res/values-fr-rFR/strings.xml +++ b/common/src/main/res/values-fr-rFR/strings.xml @@ -198,7 +198,7 @@ Staking Acquisition Acheter des tokens - Vous avez récupéré vos DOT des crowdloans ? Commencez à staker vos DOT aujourd\'hui pour obtenir les récompenses maximales ! + Vous avez récupéré vos DOT des crowdloans ? Commencez à staker vos DOT aujourd\'hui pour obtenir les récompenses maximales ! Boostez vos DOT 🚀 Filtrer les actifs Tous les réseaux @@ -525,10 +525,10 @@ Ajouter une connexion Scannez le code QR Un problème a été identifié avec votre sauvegarde. Vous avez la possibilité de supprimer la sauvegarde actuelle et d\'en créer une nouvelle. %s avant de continuer. - Assurez-vous d\'avoir sauvegardé les phrases secrètes pour tous les portefeuilles + Assurez-vous d\'avoir sauvegardé les phrases secrètes pour tous les portefeuilles Sauvegarde trouvée mais vide ou corrompue À l\'avenir, sans le mot de passe de sauvegarde, il est impossible de restaurer vos portefeuilles à partir de la Sauvegarde Cloud.\n%s - Ce mot de passe ne peut pas être récupéré. + Ce mot de passe ne peut pas être récupéré. Souvenez-vous du mot de passe de sauvegarde Confirmer le mot de passe Mot de passe de sauvegarde @@ -1131,7 +1131,7 @@ Veuillez vous assurer que la biométrie est activée dans les paramètres La biométrie est désactivée dans les paramètres Communauté - E-mail + Obtenez de l\'aide par Email Général Chaque opération de signature sur les portefeuilles avec paire de clés (créée dans Nova Wallet ou importée) doit nécessiter une vérification PIN avant de construire la signature Demander une authentification pour signer les opérations @@ -1143,7 +1143,7 @@ Sécurité Assistance et commentaires Twitter - Wiki + Wiki et centre d\'aide YouTube La conviction sera fixée à 0,1x lorsque vous vous abstenez Vous ne pouvez pas staker avec le Stake direct et les Pools de Nomination en même temps @@ -1417,7 +1417,7 @@ Je veux staker Yield Boost Type de staking - Vous retirez tous vos tokens et ne pouvez pas en stake davantage. + Vous retirez tous vos tokens et ne pouvez pas en stake davantage. Impossible de stake plus Lors d\'un retrait partiel, vous devez laisser au moins %s en jeu. Voulez-vous effectuer un retrait complet en libérant également %s restants? Montant trop faible reste en stake @@ -1466,8 +1466,8 @@ Donnez un nom à votre portefeuille Cela ne sera visible que par vous et vous pourrez le modifier plus tard. Votre portefeuille est prêt - Vous avez des tokens verrouillés sur votre solde en raison de %s. Pour continuer, vous devez entrer moins de %s ou plus de %s. Pour stake un autre montant, vous devez supprimer vos verrous %s. - Vous ne pouvez pas stake le montant spécifié + Vous avez des tokens verrouillés sur votre solde en raison de %s. Pour continuer, vous devez entrer moins de %s ou plus de %s. Pour stake un autre montant, vous devez supprimer vos verrous %s. + Vous ne pouvez pas stake le montant spécifié Sélectionné : %d (max %d) Solde disponible : %1$s (%2$s) %s avec vos tokens staked @@ -1563,7 +1563,7 @@ Aucune donnée récupérée Vous avez déjà voté pour tous les référendums disponibles ou il n\'y a pas de référendums pour voter en ce moment. Revenez plus tard. Vous avez déjà voté pour tous les référendums disponibles - Demandé : + Demandé : Liste de vote %d restant Confirmer les votes diff --git a/common/src/main/res/values-in/strings.xml b/common/src/main/res/values-in/strings.xml index 8849ecdfb8..2c83b7e843 100644 --- a/common/src/main/res/values-in/strings.xml +++ b/common/src/main/res/values-in/strings.xml @@ -1123,7 +1123,7 @@ Pastikan biometrik diaktifkan dalam Pengaturan Biometrik dinonaktifkan dalam Pengaturan Komunitas - Email + Dapatkan dukungan melalui Email Umum Setiap operasi tanda tangan pada dompet dengan pasangan kunci (dibuat di dompet nova atau diimpor) harus memerlukan verifikasi PIN sebelum membuat tanda tangan Minta autentikasi untuk penandatanganan operasi @@ -1135,7 +1135,7 @@ Keamanan Dukungan & Masukan Twitter - Wiki + Wiki & Pusat Bantuan Youtube Keyakinan akan diatur ke 0,1x ketika Abstain Anda tidak dapat melakukan staking dengan Staking Langsung dan Pool Nominasi pada saat yang sama diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index 47d2572d74..df030d8362 100644 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -1131,7 +1131,7 @@ Si prega, assicurarsi che la biometria sia abilitata nelle impostazioni Biometria disabilitata nelle impostazioni Comunità - Email + Ottieni supporto via Email Generale Ogni operazione di firma sui portafogli con coppia di chiavi (creata in un portafoglio di nova o importata) dovrebbe richiedere la verifica del PIN prima di costruire la firma Richiedi autenticazione per la firma operazioni @@ -1143,7 +1143,7 @@ Sicurezza Supporto e Feedback Twitter - Wiki + Wiki e Centro assistenza Youtube La convinzione sarà impostata a 0.1x quando Abstain Non puoi fare staking contemporaneamente con Direct Staking e con Nomination Pools diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index d76dbd4d06..afb5a07e86 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -1123,7 +1123,7 @@ 設定で生体認証が有効になっていることを確認してください 設定で生体認証が無効になっています コミュニティ - Eメール + メールでサポートを受ける 一般 ウォレットでの署名操作(Nova Walletで作成またはインポートされたもの)は、署名の作成前にPIN確認が必要です 操作の署名に認証を要求 @@ -1135,7 +1135,7 @@ セキュリティ サポート&フィードバック Twitter - ウィキ + ウィキとヘルプセンター Youtube 棄権時には信念が0.1xに設定されます Direct StakingとNomination Poolsを同時にステークすることはできません diff --git a/common/src/main/res/values-ko/strings.xml b/common/src/main/res/values-ko/strings.xml index 2c227c0998..4142aa26be 100644 --- a/common/src/main/res/values-ko/strings.xml +++ b/common/src/main/res/values-ko/strings.xml @@ -564,8 +564,8 @@ 크라우드론 종료됨 추천 코드를 입력하세요 크라우드론 정보 - 크라우드론 %s 배우기]]> - 크라우드론 웹사이트]]> + 크라우드론 %s 배우기 + %s의 크라우드론 웹사이트 임대 기간 파라체인을 선택하여 %s을(를) 기여하세요. 기여한 토큰을 돌려받게 되며, 파라체인이 슬롯에 당선되면 경매 종료 후 보상을 받게 됩니다. 기여를 위해 지갑에 %s 계정을 추가해야 합니다. @@ -821,7 +821,7 @@ 비밀번호 구절을 공유하지 마세요! 아무도 화면을 볼 수 없도록 하고\n스크린샷을 찍지 마세요 아무에게도 %s 하지 마세요 - 공유하지 마세요 + 공유하지 마세요 다른 것을 시도해 보세요. 유효하지 않은 니모닉 구절입니다. 단어 순서를 다시 한번 확인하세요 %d 개 이상의 지갑을 선택할 수 없습니다 @@ -1123,7 +1123,7 @@ 설정에서 생체 인식이 활성화되었는지 확인하세요 설정에서 생체 인식이 비활성화되었습니다 커뮤니티 - 이메일 + 이메일을 통해 지원 받기 일반 키 페어가 있는 지갑(Nova Wallet에서 생성 또는 가져옴)에 대한 각 서명 작업은 서명을 생성하기 전에 PIN 확인이 필요합니다 작업 서명을 위해 인증 요청 @@ -1135,9 +1135,9 @@ 보안 지원 및 피드백 Twitter - 위키 + 위키 및 도움말 센터 Youtube - 기권 시 확신도가 0.1x로 설정됩니다 + 기권 시 확신도가 0.1x로 설정됩니다 직접 Staking과 Nomination Pools를 동시에 사용할 수 없습니다 이미 Staking 중 고급 Staking 관리 diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index 889f66d6bf..641ea2cf6e 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -535,7 +535,7 @@ Dodaj połączenie Skanuj kod QR Wykryto problem z twoją kopią zapasową. Masz możliwość usunięcia bieżącej kopii zapasowej i utworzenia nowej. %s przed kontynuowaniem. - Upewnij się, że zapisałeś Passphrases dla wszystkich portfeli + Upewnij się, że zapisałeś Passphrases dla wszystkich portfeli Znaleziono kopię zapasową, ale jest pusta lub uszkodzona W przyszłości bez hasła kopii zapasowej nie będzie można przywrócić portfeli z kopii zapasowej w Chmurze.\n%s Tego hasła nie można odzyskać. @@ -1147,7 +1147,7 @@ Proszę upewnij się, że biometria jest włączona w Ustawieniach Biometria wyłączona w Ustawieniach Społeczność - Email + Uzyskaj wsparcie przez Email Ogólne Każda operacja podpisania na portfelach z parą kluczy (utworzonych w Nova Wallet lub zaimportowanych) powinna wymagać weryfikacji PIN przed utworzeniem podpisu Prośba o uwierzytelnienie dla operacji podpisywania @@ -1159,7 +1159,7 @@ Bezpieczeństwo Wsparcie i opinie Twitter - Wiki + Wiki i Centrum Pomocy Youtube Conviction zostanie ustawione na 0.1x podczas Wstrzymania się Nie możesz stake z Direct Staking i Nomination Pools jednocześnie diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index 28432220cc..f42fb5c89f 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -1131,7 +1131,7 @@ Por favor, certifique-se de que a biometria está ativada nas Configurações Biometria desativada nas Configurações Comunidade - Escrever para os desenvolvedores + Obtenha suporte via Email Geral Cada operação de assinatura em carteiras com par de chaves (criada na nova wallet ou importada) deve exigir a verificação do PIN antes da construção da assinatura Solicitar autenticação para assinatura de operações @@ -1143,7 +1143,7 @@ Segurança Suporte & Feedback Twitter - Wiki + Wiki e Central de Ajuda Youtube A convicção será definida como 0.1x quando Abstain Você não pode fazer staking com Staking Direto e Pools de Nomeação ao mesmo tempo diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 3726bd30f6..cd1f87d762 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -1147,7 +1147,7 @@ Пожалуйста, убедитесь, что биометрия включена в настройках Биометрия отключена в настройках Сообщество - Написать разработчикам + Получите поддержку по Email Основные Каждая операция подписи на кошельках с парой ключей (созданной в кошельке nova или импортированной) должна требовать проверки PIN-кода перед созданием подписи Запрашивать аутентификацию для подписи операций @@ -1159,7 +1159,7 @@ Безопасность Поддержка и обратная связь Twitter - Руководство пользователя + Вики и справочный центр Youtube Убежденность будет установлена на 0.1x при Воздержании Невозможно стейкать напрямую и в пуле одновременно diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index d88a54ece3..aa73d206fc 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -1131,7 +1131,7 @@ Lütfen, biyometrinin Ayarlar\'da etkinleştirildiğinden emin olun Ayarlar\'da biyometrik kapalı Topluluk - Email + Email yoluyla destek alın Genel Anahtar çiftiyle (nova cüzdanında oluşturulan veya içe aktarılan) cüzdanlarda sign işlemi yapılırken, imza oluşturmadan önce PIN doğrulaması gerektirir Operasyon imzalanırken kimlik doğrulama isteyin @@ -1143,7 +1143,7 @@ Güvenlik Destek & Geribildirim Twitter - Wiki + Wiki & Yardım Merkezi Youtube Çekinmelerde 0.1x olarak Ayarlanacak Aynı anda hem Doğrudan Stake hem de Nomination Havuzlarını kullanarak stake yapamazsınız diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index a99abb7976..6ebc3b7831 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -1123,7 +1123,7 @@ Vui lòng, đảm bảo sinh trắc học đã được bật trong Cài đặt Sinh trắc học đã bị vô hiệu hóa trong Cài đặt Cộng đồng - Email + Nhận hỗ trợ qua Email Chung Mỗi hoạt động ký trên ví với cặp khóa (tạo trong ví nova hoặc nhập) cần yêu cầu xác minh PIN trước khi tạo chữ ký Yêu cầu xác thực cho các hoạt động ký @@ -1135,9 +1135,9 @@ Bảo mật Hỗ trợ & Phản hồi Twitter - Wiki + Wiki & Trung tâm trợ giúp Youtube - Độ xác tín sẽ được đặt là 0.1x khi bỏ phiếu Abstain + Độ xác tín sẽ được đặt là 0.1x khi bỏ phiếu Abstain Bạn không thể Stake với Direct Staking và Nomination Pools cùng một lúc Đã staking Quản lý Stake nâng cao diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml index 99a9839a61..cc1c960d19 100644 --- a/common/src/main/res/values-zh-rCN/strings.xml +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -1123,7 +1123,7 @@ 请确保在设置中启用了生物识别 设置中禁用了生物识别 社区 - 电子邮件 + 通过电子邮件获得支持 通用 带有密钥对的钱包(在nova钱包中创建或导入的)上的每次签名操作都应在构造签名前要求进行PIN验证 请求操作签名的认证 @@ -1135,7 +1135,7 @@ 安全性 支持和反馈 Twitter - 维基 + 维基百科和帮助中心 Youtube 弃权时将把决心设置为 0.1x 不能同时使用直接质押和提名池进行质押 diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index b208452f63..862440d046 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,6 +1,10 @@ + Wiki & Help Center + + Get support via Email + You have already voted for all available referenda Confirm your votes Vote list @@ -624,8 +628,6 @@ Referendum not found - Wiki - Swap Repeat the operation @@ -2036,7 +2038,6 @@ Search results will be displayed here Search results Community - Email General Preferences Security diff --git a/core-db/src/androidTest/java/io/novafoundation/nova/core_db/dao/Helpers.kt b/core-db/src/androidTest/java/io/novafoundation/nova/core_db/dao/Helpers.kt index 52daa4215f..e9085c3b47 100644 --- a/core-db/src/androidTest/java/io/novafoundation/nova/core_db/dao/Helpers.kt +++ b/core-db/src/androidTest/java/io/novafoundation/nova/core_db/dao/Helpers.kt @@ -64,7 +64,7 @@ fun chainOf( supportProxy = false, swap = "", hasSubstrateRuntime = true, - nodeSelectionStrategy = ChainLocal.NodeSelectionStrategyLocal.ROUND_ROBIN, + nodeSelectionStrategy = ChainLocal.AutoBalanceStrategyLocal.ROUND_ROBIN, source = ChainLocal.Source.CUSTOM, customFee = "" ) diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt index c51dddb0d7..5a315b581b 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt @@ -51,6 +51,7 @@ import io.novafoundation.nova.core_db.migrations.AddBalanceModesToAssets_51_52 import io.novafoundation.nova.core_db.migrations.AddBrowserHostSettings_34_35 import io.novafoundation.nova.core_db.migrations.AddBuyProviders_7_8 import io.novafoundation.nova.core_db.migrations.AddChainColor_4_5 +import io.novafoundation.nova.core_db.migrations.AddChainForeignKeyForProxy_63_64 import io.novafoundation.nova.core_db.migrations.AddConnectionStateToChains_53_54 import io.novafoundation.nova.core_db.migrations.AddContributions_23_24 import io.novafoundation.nova.core_db.migrations.AddCurrencies_18_19 @@ -150,7 +151,7 @@ import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal @Database( - version = 63, + version = 64, entities = [ AccountLocal::class, NodeLocal::class, @@ -248,7 +249,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(AddFungibleNfts_55_56, ChainPushSupport_56_57) .addMigrations(AddLocalMigratorVersionToChainRuntimes_57_58, AddGloballyUniqueIdToMetaAccounts_58_59) .addMigrations(ChainNetworkManagement_59_60, AddBalanceHolds_60_61, ChainNetworkManagement_61_62) - .addMigrations(TinderGovBasket_62_63) + .addMigrations(TinderGovBasket_62_63, AddChainForeignKeyForProxy_63_64) .build() } return instance!! diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ChainConverters.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ChainConverters.kt index 32e52b7fe3..0a6caa3e36 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ChainConverters.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ChainConverters.kt @@ -3,16 +3,16 @@ package io.novafoundation.nova.core_db.converters import androidx.room.TypeConverter import io.novafoundation.nova.common.utils.enumValueOfOrNull import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLocal -import io.novafoundation.nova.core_db.model.chain.ChainLocal.NodeSelectionStrategyLocal +import io.novafoundation.nova.core_db.model.chain.ChainLocal.AutoBalanceStrategyLocal class ChainConverters { @TypeConverter - fun fromNodeStrategy(strategy: NodeSelectionStrategyLocal): String = strategy.name + fun fromNodeStrategy(strategy: AutoBalanceStrategyLocal): String = strategy.name @TypeConverter - fun toNodeStrategy(name: String): NodeSelectionStrategyLocal { - return enumValueOfOrNull(name) ?: NodeSelectionStrategyLocal.UNKNOWN + fun toNodeStrategy(name: String): AutoBalanceStrategyLocal { + return enumValueOfOrNull(name) ?: AutoBalanceStrategyLocal.UNKNOWN } @TypeConverter diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/dao/MetaAccountDao.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/dao/MetaAccountDao.kt index c1bd8796b5..f44aa3db4a 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/dao/MetaAccountDao.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/dao/MetaAccountDao.kt @@ -220,6 +220,18 @@ interface MetaAccountDao { @Query("SELECT * FROM meta_accounts WHERE isSelected = 1") suspend fun selectedMetaAccount(): RelationJoinedMetaAccountInfo? + @Query( + """ + DELETE FROM meta_accounts + WHERE id IN ( + SELECT proxiedMetaId + FROM proxy_accounts + WHERE chainId = :chainId + ) + """ + ) + fun deleteProxiedMetaAccountsByChain(chainId: String) + @Transaction suspend fun insertMetaAndChainAccounts( metaAccount: MetaAccountLocal, diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/63_64_AddChainForeignKeyForProxy.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/63_64_AddChainForeignKeyForProxy.kt new file mode 100644 index 0000000000..1e04855972 --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/63_64_AddChainForeignKeyForProxy.kt @@ -0,0 +1,37 @@ +package io.novafoundation.nova.core_db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val AddChainForeignKeyForProxy_63_64 = object : Migration(63, 64) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `proxy_accounts_new` ( + `proxiedMetaId` INTEGER NOT NULL, + `proxyMetaId` INTEGER NOT NULL, + `chainId` TEXT NOT NULL, + `proxiedAccountId` BLOB NOT NULL, + `proxyType` TEXT NOT NULL, + PRIMARY KEY(`proxyMetaId`, `proxiedAccountId`, `chainId`, `proxyType`), + FOREIGN KEY(`proxiedMetaId`) REFERENCES `meta_accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, + FOREIGN KEY(`proxyMetaId`) REFERENCES `meta_accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, + FOREIGN KEY(`chainId`) REFERENCES `chains`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE + ) + """ + ) + + database.execSQL( + """ + INSERT INTO `proxy_accounts_new` (`proxiedMetaId`, `proxyMetaId`, `chainId`, `proxiedAccountId`, `proxyType`) + SELECT `proxiedMetaId`, `proxyMetaId`, `chainId`, `proxiedAccountId`, `proxyType` + FROM `proxy_accounts` + """ + ) + + database.execSQL("DROP TABLE `proxy_accounts`") + + database.execSQL("ALTER TABLE `proxy_accounts_new` RENAME TO `proxy_accounts`") + } +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/ChainLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/ChainLocal.kt index 76ea6e964a..53bfaf03f3 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/ChainLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/ChainLocal.kt @@ -33,12 +33,17 @@ data class ChainLocal( val governance: String, val additional: String?, val connectionState: ConnectionStateLocal, + @Deprecated("Use autoBalanceStrategy") @ColumnInfo(defaultValue = NODE_SELECTION_STRATEGY_DEFAULT) - val nodeSelectionStrategy: NodeSelectionStrategyLocal, + val nodeSelectionStrategy: AutoBalanceStrategyLocal, @ColumnInfo(defaultValue = DEFAULT_NETWORK_SOURCE_STR) val source: Source ) : Identifiable { + @Suppress("DEPRECATION") + val autoBalanceStrategy: AutoBalanceStrategyLocal + get() = nodeSelectionStrategy + companion object { const val EMPTY_CHAIN_ICON = "" @@ -46,7 +51,7 @@ data class ChainLocal( const val DEFAULT_NETWORK_SOURCE_STR = "DEFAULT" } - enum class NodeSelectionStrategyLocal { + enum class AutoBalanceStrategyLocal { ROUND_ROBIN, UNIFORM, UNKNOWN } diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/NodeSelectionPreferencesLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/NodeSelectionPreferencesLocal.kt index 21cff260f8..39df5546a3 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/NodeSelectionPreferencesLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/NodeSelectionPreferencesLocal.kt @@ -26,9 +26,14 @@ data class NodeSelectionPreferencesLocal( val chainId: String, @ColumnInfo(defaultValue = DEFAULT_AUTO_BALANCE_DEFAULT_STR) val autoBalanceEnabled: Boolean, + @Deprecated("Use [selectedUnformattedWssNodeUrl]") val selectedNodeUrl: String? ) : Identifiable { + @Suppress("DEPRECATION") + val selectedUnformattedWssNodeUrl: String? + get() = selectedNodeUrl + companion object { const val DEFAULT_AUTO_BALANCE_DEFAULT_STR = "1" diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt index 6c093104a4..ac574c3043 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt @@ -7,6 +7,11 @@ import androidx.room.PrimaryKey import io.novafoundation.nova.core.model.CryptoType import java.util.UUID +/* + TODO: on next migration please add following changes: + - Foreign key for parentMetaId to remove proxy meta account automatically when proxied is deleted + - Foreign key to ProxyAccountLocal to remove proxies meta accounts automatically when chain is deleted + */ @Entity( tableName = MetaAccountLocal.TABLE_NAME, indices = [ diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ProxyAccountLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ProxyAccountLocal.kt index 6a69446458..1d2cfb5034 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ProxyAccountLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ProxyAccountLocal.kt @@ -4,6 +4,7 @@ import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Ignore import io.novafoundation.nova.common.utils.Identifiable +import io.novafoundation.nova.core_db.model.chain.ChainLocal import io.novasama.substrate_sdk_android.extensions.toHexString @Entity( @@ -21,6 +22,12 @@ import io.novasama.substrate_sdk_android.extensions.toHexString entity = MetaAccountLocal::class, onDelete = ForeignKey.CASCADE ), + ForeignKey( + parentColumns = ["id"], + childColumns = ["chainId"], + entity = ChainLocal::class, + onDelete = ForeignKey.CASCADE + ), ], primaryKeys = ["proxyMetaId", "proxiedAccountId", "chainId", "proxyType"] ) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountInteractor.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountInteractor.kt index ea0e752b91..496a655d8e 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountInteractor.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountInteractor.kt @@ -65,4 +65,6 @@ interface AccountInteractor { suspend fun hasSecretsAccounts(): Boolean suspend fun hasCustomChainAccounts(metaId: Long): Boolean + + suspend fun deleteProxiedMetaAccountsByChain(chainId: String) } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepository.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepository.kt index e2bd4405c8..a4589addbd 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepository.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepository.kt @@ -133,4 +133,6 @@ interface AccountRepository { suspend fun generateRestoreJson(metaAccount: MetaAccount, password: String): String suspend fun hasSecretsAccounts(): Boolean + + suspend fun deleteProxiedMetaAccountsByChain(chainId: String) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt index 67c9445875..7f7c00370a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt @@ -37,12 +37,12 @@ import io.novafoundation.nova.runtime.network.rpc.RpcCalls import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import java.math.BigInteger -import kotlinx.coroutines.CoroutineScope class RealExtrinsicService( private val rpcCalls: RpcCalls, @@ -107,7 +107,8 @@ class RealExtrinsicService( ): FeeResponse { val extrinsic = extrinsicBuilderFactory.createForFee(getFeeSigner(chain, origin), chain) .also { it.formExtrinsic() } - .build(submissionOptions.batchMode) + .buildExtrinsic(submissionOptions.batchMode) + .extrinsicHex return rpcCalls.getExtrinsicFee(chain, extrinsic) } @@ -121,7 +122,7 @@ class RealExtrinsicService( val signer = getFeeSigner(chain, origin) val extrinsicBuilder = extrinsicBuilderFactory.createForFee(signer, chain) extrinsicBuilder.formExtrinsic() - val extrinsic = extrinsicBuilder.build(submissionOptions.batchMode) + val extrinsic = extrinsicBuilder.buildExtrinsic(submissionOptions.batchMode).extrinsicHex return estimateFee(chain, extrinsic, signer, submissionOptions) } @@ -227,7 +228,7 @@ class RealExtrinsicService( feePayment.modifyExtrinsic(extrinsicBuilder) - extrinsicBuilder.build(submissionOptions.batchMode) + extrinsicBuilder.buildExtrinsic(submissionOptions.batchMode).extrinsicHex } extrinsicsToSubmit @@ -254,7 +255,7 @@ class RealExtrinsicService( feePayment.modifyExtrinsic(extrinsicBuilder) - val extrinsic = extrinsicBuilder.build(submissionOptions.batchMode) + val extrinsic = extrinsicBuilder.buildExtrinsic(submissionOptions.batchMode).extrinsicHex return SubmissionRaw(extrinsic, submissionOrigin) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AccountRepositoryImpl.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AccountRepositoryImpl.kt index 8d027b2c75..d6ce300a4b 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AccountRepositoryImpl.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AccountRepositoryImpl.kt @@ -299,6 +299,10 @@ class AccountRepositoryImpl( return accountDataSource.hasSecretsAccounts() } + override suspend fun deleteProxiedMetaAccountsByChain(chainId: String) { + accountDataSource.deleteProxiedMetaAccountsByChain(chainId) + } + override fun nodesFlow(): Flow> { return nodeDao.nodesFlow() .mapList { mapNodeLocalToNode(it) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSource.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSource.kt index a8f8596cf5..38d90549a0 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSource.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSource.kt @@ -105,6 +105,8 @@ interface AccountDataSource : SecretStoreV1 { suspend fun hasSecretsAccounts(): Boolean suspend fun getMetaAccountIdsByType(type: LightMetaAccount.Type): List + + suspend fun deleteProxiedMetaAccountsByChain(chainId: String) } suspend fun AccountDataSource.getMetaAccountTypeOrThrow(metaId: Long): LightMetaAccount.Type { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt index 2e0c5e7460..b6eb324565 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/datasource/AccountDataSourceImpl.kt @@ -152,6 +152,10 @@ class AccountDataSourceImpl( return metaAccountDao.getMetaAccountIdsByType(mapMetaAccountTypeToLocal(type)) } + override suspend fun deleteProxiedMetaAccountsByChain(chainId: String) { + return metaAccountDao.deleteProxiedMetaAccountsByChain(chainId) + } + override suspend fun hasSecretsAccounts(): Boolean { return metaAccountDao.hasMetaAccountsByType(MetaAccountLocal.Type.SECRETS) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/AccountInteractorImpl.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/AccountInteractorImpl.kt index c87d50820b..b457841e17 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/AccountInteractorImpl.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/AccountInteractorImpl.kt @@ -184,12 +184,7 @@ class AccountInteractorImpl( override suspend fun removeDeactivatedMetaAccounts() { accountRepository.removeDeactivatedMetaAccounts() - if (!accountRepository.isAccountSelected()) { - val metaAccounts = getActiveMetaAccounts() - if (metaAccounts.isNotEmpty()) { - accountRepository.selectMetaAccount(metaAccounts.first().id) - } - } + switchMetaAccountIfAccountNotSelected() } override suspend fun switchToNotDeactivatedAccountIfNeeded() { @@ -210,4 +205,19 @@ class AccountInteractorImpl( val metaAccount = getMetaAccount(metaId) return metaAccount.chainAccounts.isNotEmpty() } + + override suspend fun deleteProxiedMetaAccountsByChain(chainId: String) { + accountRepository.deleteProxiedMetaAccountsByChain(chainId) + + switchMetaAccountIfAccountNotSelected() + } + + private suspend fun switchMetaAccountIfAccountNotSelected() { + if (!accountRepository.isAccountSelected()) { + val metaAccounts = getActiveMetaAccounts() + if (metaAccounts.isNotEmpty()) { + accountRepository.selectMetaAccount(metaAccounts.first().id) + } + } + } } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/DAppBrowserViewModel.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/DAppBrowserViewModel.kt index fa8ea65ae5..6dbf8a672d 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/DAppBrowserViewModel.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/DAppBrowserViewModel.kt @@ -137,7 +137,7 @@ class DAppBrowserViewModel( return when (response) { is ExternalSignCommunicator.Response.Rejected -> ConfirmTxResponse.Rejected(response.requestId) - is ExternalSignCommunicator.Response.Signed -> ConfirmTxResponse.Signed(response.requestId, response.signature) + is ExternalSignCommunicator.Response.Signed -> ConfirmTxResponse.Signed(response.requestId, response.signature, response.modifiedTransaction) is ExternalSignCommunicator.Response.SigningFailed -> ConfirmTxResponse.SigningFailed(response.requestId, response.shouldPresent) is ExternalSignCommunicator.Response.Sent -> ConfirmTxResponse.Sent(response.requestId, response.txHash) } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/model/SignerPayload.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/model/SignerPayload.kt index c2f6717620..c3034aaebe 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/model/SignerPayload.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/model/SignerPayload.kt @@ -18,6 +18,8 @@ sealed class SignerPayload { val specVersion: String, val tip: String, val transactionVersion: String, + val metadataHash: String?, + val withSignedTransaction: Boolean?, val signedExtensions: List, val version: Int ) : SignerPayload() @@ -60,12 +62,14 @@ fun mapPolkadotJsSignerPayloadToPolkadotPayload(signerPayload: SignerPayload): P blockNumber = blockNumber, era = era, genesisHash = genesisHash, + metadataHash = metadataHash, method = method, nonce = nonce, specVersion = specVersion, tip = tip, transactionVersion = transactionVersion, signedExtensions = signedExtensions, + withSignedTransaction = withSignedTransaction, version = version ) } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/DefaultPolkadotJsState.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/DefaultPolkadotJsState.kt index 7d60144ec3..5a190e25c0 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/DefaultPolkadotJsState.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/DefaultPolkadotJsState.kt @@ -7,7 +7,6 @@ import io.novafoundation.nova.feature_dapp_impl.R import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor import io.novafoundation.nova.feature_dapp_impl.domain.browser.polkadotJs.PolkadotJsExtensionInteractor import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsTransportRequest -import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignerResult import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.model.mapPolkadotJsSignerPayloadToPolkadotPayload import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session import io.novafoundation.nova.feature_dapp_impl.web3.states.BaseState @@ -17,6 +16,7 @@ import io.novafoundation.nova.feature_dapp_impl.web3.states.Web3StateMachineHost import io.novafoundation.nova.feature_dapp_impl.web3.states.Web3StateMachineHost.NotAuthorizedException import io.novafoundation.nova.feature_dapp_impl.web3.states.hostApi.ConfirmTxResponse import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest +import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignerResult import kotlinx.coroutines.flow.flowOf class DefaultPolkadotJsState( @@ -75,7 +75,7 @@ class DefaultPolkadotJsState( when (val response = hostApi.confirmTx(signRequest)) { is ConfirmTxResponse.Rejected -> request.reject(NotAuthorizedException) is ConfirmTxResponse.Sent -> throw IllegalStateException("Unexpected 'Sent' response for PolkadotJs extension") - is ConfirmTxResponse.Signed -> request.accept(PolkadotSignerResult(response.requestId, response.signature)) + is ConfirmTxResponse.Signed -> request.accept(PolkadotSignerResult(response.requestId, response.signature, response.modifiedTransaction)) is ConfirmTxResponse.SigningFailed -> { if (response.shouldPresent) hostApi.showError(resourceManager.getString(R.string.dapp_sign_extrinsic_failed)) diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/hostApi/ConfirmTxResponse.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/hostApi/ConfirmTxResponse.kt index 792a14ca54..9466a9d194 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/hostApi/ConfirmTxResponse.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/hostApi/ConfirmTxResponse.kt @@ -11,7 +11,7 @@ sealed class ConfirmTxResponse : Parcelable { class Rejected(override val requestId: String) : ConfirmTxResponse() @Parcelize - class Signed(override val requestId: String, val signature: String) : ConfirmTxResponse() + class Signed(override val requestId: String, val signature: String, val modifiedTransaction: String?) : ConfirmTxResponse() @Parcelize class Sent(override val requestId: String, val txHash: String) : ConfirmTxResponse() diff --git a/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/ExternalSignCommunicator.kt b/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/ExternalSignCommunicator.kt index c3e8a76fbf..d324c7bfaf 100644 --- a/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/ExternalSignCommunicator.kt +++ b/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/ExternalSignCommunicator.kt @@ -23,7 +23,7 @@ interface ExternalSignCommunicator : ExternalSignRequester, ExternalSignResponde class Rejected(override val requestId: String) : Response() @Parcelize - class Signed(override val requestId: String, val signature: String) : Response() + class Signed(override val requestId: String, val signature: String, val modifiedTransaction: String? = null) : Response() @Parcelize class Sent(override val requestId: String, val txHash: String) : Response() diff --git a/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignPayload.kt b/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignPayload.kt index 2085daa013..88b221ec0d 100644 --- a/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignPayload.kt +++ b/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignPayload.kt @@ -19,6 +19,8 @@ sealed class PolkadotSignPayload : Parcelable { val specVersion: String, val tip: String, val transactionVersion: String, + val metadataHash: String?, + val withSignedTransaction: Boolean?, val signedExtensions: List, val version: Int ) : PolkadotSignPayload() diff --git a/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignerResult.kt b/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignerResult.kt index 0847b687d2..ea5b4601a5 100644 --- a/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignerResult.kt +++ b/feature-external-sign-api/src/main/java/io/novafoundation/nova/feature_external_sign_api/model/signPayload/polkadot/PolkadotSignerResult.kt @@ -1,3 +1,3 @@ package io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot -class PolkadotSignerResult(val id: String, val signature: String) +class PolkadotSignerResult(val id: String, val signature: String, val signedTransaction: String?) diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt index b02246d59c..4a496c133e 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt @@ -18,6 +18,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletReposit import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.runtime.di.ExtrinsicSerialization import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory +import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.network.rpc.RpcCalls import okhttp3.OkHttpClient @@ -64,4 +65,6 @@ interface ExternalSignFeatureDependencies { val gasPriceProviderFactory: GasPriceProviderFactory val rpcCalls: RpcCalls + + val metadataShortenerService: MetadataShortenerService } diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/PolkadotSignModule.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/PolkadotSignModule.kt index 9c757dbc5e..97854c1df7 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/PolkadotSignModule.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/PolkadotSignModule.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepos import io.novafoundation.nova.feature_external_sign_impl.domain.sign.polkadot.PolkadotSignInteractorFactory import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.runtime.di.ExtrinsicSerialization +import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module @@ -25,7 +26,8 @@ class PolkadotSignModule { tokenRepository: TokenRepository, @ExtrinsicSerialization extrinsicGson: Gson, addressIconGenerator: AddressIconGenerator, - signerProvider: SignerProvider + signerProvider: SignerProvider, + metadataShortenerService: MetadataShortenerService ) = PolkadotSignInteractorFactory( extrinsicService = extrinsicService, chainRegistry = chainRegistry, @@ -33,6 +35,7 @@ class PolkadotSignModule { tokenRepository = tokenRepository, extrinsicGson = extrinsicGson, addressIconGenerator = addressIconGenerator, - signerProvider = signerProvider + signerProvider = signerProvider, + metadataShortenerService = metadataShortenerService ) } diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/DAppParsedExtrinsic.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/DAppParsedExtrinsic.kt index 9adf25a405..214abbcd06 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/DAppParsedExtrinsic.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/DAppParsedExtrinsic.kt @@ -4,7 +4,7 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic import java.math.BigInteger -class DAppParsedExtrinsic( +data class DAppParsedExtrinsic( val address: String, val nonce: BigInteger, val specVersion: Int, @@ -13,5 +13,6 @@ class DAppParsedExtrinsic( val era: Era, val blockHash: ByteArray, val tip: BigInteger, + val metadataHash: ByteArray?, val call: Extrinsic.EncodingInstance.CallRepresentation ) diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt index f5be17d291..bd36f264a4 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.common.address.createAddressModel import io.novafoundation.nova.common.utils.asHexString import io.novafoundation.nova.common.utils.bigIntegerFromHex import io.novafoundation.nova.common.utils.intFromHex +import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.validation.EmptyValidationSystem import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi @@ -28,8 +29,10 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.extrinsic.CustomSignedExtensions +import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner +import io.novafoundation.nova.runtime.extrinsic.signer.generateMetadataProofWithSignerRestrictions import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getChainOrNull @@ -40,8 +43,10 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.EraType import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall +import io.novasama.substrate_sdk_android.runtime.extrinsic.CheckMetadataHash import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import io.novasama.substrate_sdk_android.runtime.extrinsic.Nonce +import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.fromHex import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.fromUtf8 @@ -50,6 +55,7 @@ import io.novasama.substrate_sdk_android.wsrpc.request.runtime.chain.RuntimeVers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext @@ -60,6 +66,7 @@ class PolkadotSignInteractorFactory( private val tokenRepository: TokenRepository, private val extrinsicGson: Gson, private val addressIconGenerator: AddressIconGenerator, + private val metadataShortenerService: MetadataShortenerService, private val signerProvider: SignerProvider, ) { @@ -72,7 +79,8 @@ class PolkadotSignInteractorFactory( addressIconGenerator = addressIconGenerator, request = request, wallet = wallet, - signerProvider = signerProvider + signerProvider = signerProvider, + metadataShortenerService = metadataShortenerService ) } @@ -84,12 +92,15 @@ class PolkadotExternalSignInteractor( private val addressIconGenerator: AddressIconGenerator, private val request: ExternalSignRequest.Polkadot, private val signerProvider: SignerProvider, + private val metadataShortenerService: MetadataShortenerService, wallet: ExternalSignWallet, accountRepository: AccountRepository ) : BaseExternalSignInteractor(accountRepository, wallet, signerProvider) { private val signPayload = request.payload + private val actualParsedExtrinsic = singleReplaySharedFlow() + override val validationSystem: ConfirmDAppOperationValidationSystem = EmptyValidationSystem() override suspend fun createAccountAddressModel(): AddressModel { @@ -126,8 +137,8 @@ class PolkadotExternalSignInteractor( is PolkadotSignPayload.Raw -> signBytes(signPayload) } }.fold( - onSuccess = { signature -> - ExternalSignCommunicator.Response.Signed(request.id, signature) + onSuccess = { signedResult -> + ExternalSignCommunicator.Response.Signed(request.id, signedResult.signature, signedResult.modifiedTransaction) }, onFailure = { error -> error.failedSigningIfNotCancelled(request.id) @@ -137,7 +148,7 @@ class PolkadotExternalSignInteractor( override suspend fun readableOperationContent(): String = withContext(Dispatchers.Default) { when (signPayload) { - is PolkadotSignPayload.Json -> readableExtrinsicContent(signPayload) + is PolkadotSignPayload.Json -> readableExtrinsicContent() is PolkadotSignPayload.Raw -> readableBytesContent(signPayload) } } @@ -148,29 +159,22 @@ class PolkadotExternalSignInteractor( val chain = signPayload.chainOrNull() ?: return@withContext null val signer = signPayload.feeSigner() - val extrinsicBuilder = signPayload.toExtrinsicBuilderWithoutCall(signer) - val runtime = chainRegistry.getRuntime(chain.id) + val (extrinsic, _, parsedExtrinsic) = signPayload.analyzeAndSign(signer) - val extrinsic = when (val callRepresentation = signPayload.callRepresentation(runtime)) { - is CallRepresentation.Instance -> extrinsicBuilder.call(callRepresentation.call).build() - is CallRepresentation.Bytes -> extrinsicBuilder.build(rawCallBytes = callRepresentation.bytes) - } + actualParsedExtrinsic.emit(parsedExtrinsic) - extrinsicService.estimateFee(chain, extrinsic, signer) + extrinsicService.estimateFee(chain, extrinsic.extrinsicHex, signer) } private fun readableBytesContent(signBytesPayload: PolkadotSignPayload.Raw): String { return signBytesPayload.data } - private suspend fun readableExtrinsicContent(extrinsicPayload: PolkadotSignPayload.Json): String { - val runtime = chainRegistry.getRuntime(extrinsicPayload.chain().id) - val parsedExtrinsic = parseDAppExtrinsic(runtime, extrinsicPayload) - - return extrinsicGson.toJson(parsedExtrinsic) + private suspend fun readableExtrinsicContent(): String { + return extrinsicGson.toJson(actualParsedExtrinsic.first()) } - private suspend fun signBytes(signBytesPayload: PolkadotSignPayload.Raw): String { + private suspend fun signBytes(signBytesPayload: PolkadotSignPayload.Raw): SignedResult { // assumption - only substrate dApps val substrateAccountId = signBytesPayload.address.toAccountId() @@ -181,18 +185,17 @@ class PolkadotExternalSignInteractor( SignerPayloadRaw.fromUtf8(signBytesPayload.data, substrateAccountId) } - return signer.signRaw(payload).asHexString() + val signature = signer.signRaw(payload).asHexString() + return SignedResult(signature, modifiedTransaction = null) } - private suspend fun signExtrinsic(extrinsicPayload: PolkadotSignPayload.Json): String { - val runtime = chainRegistry.getRuntime(extrinsicPayload.chain().id) + private suspend fun signExtrinsic(extrinsicPayload: PolkadotSignPayload.Json): SignedResult { val signer = resolveWalletSigner() - val extrinsicBuilder = extrinsicPayload.toExtrinsicBuilderWithoutCall(signer) + val (extrinsic, modifiedOriginal) = extrinsicPayload.analyzeAndSign(signer) - return when (val callRepresentation = extrinsicPayload.callRepresentation(runtime)) { - is CallRepresentation.Instance -> extrinsicBuilder.call(callRepresentation.call).buildSignature() - is CallRepresentation.Bytes -> extrinsicBuilder.buildSignature(rawCallBytes = callRepresentation.bytes) - } + val modifiedTx = if (modifiedOriginal) extrinsic.extrinsicHex else null + + return SignedResult(extrinsic.signatureHex, modifiedTx) } private suspend fun PolkadotSignPayload.Json.feeSigner(): FeeSigner { @@ -201,14 +204,16 @@ class PolkadotExternalSignInteractor( return signerProvider.feeSigner(resolveMetaAccount(), chain) } - private suspend fun PolkadotSignPayload.Json.toExtrinsicBuilderWithoutCall(signer: NovaSigner): ExtrinsicBuilder { + private suspend fun PolkadotSignPayload.Json.analyzeAndSign(signer: NovaSigner): ActualExtrinsic { val chain = chain() val runtime = chainRegistry.getRuntime(genesisHash) val parsedExtrinsic = parseDAppExtrinsic(runtime, this) val accountId = chain.accountIdOf(address) - return with(parsedExtrinsic) { + val actualMetadataHash = actualMetadataHash(chain, signer) + + val builder = with(parsedExtrinsic) { ExtrinsicBuilder( runtime = runtime, nonce = Nonce.singleTx(nonce), @@ -219,12 +224,44 @@ class PolkadotExternalSignInteractor( signer = signer, accountId = accountId, genesisHash = genesisHash, + checkMetadataHash = actualMetadataHash.checkMetadataHash, customSignedExtensions = CustomSignedExtensions.extensionsWithValues(), blockHash = blockHash, era = era, tip = tip ) } + + val extrinsic = when (val callRepresentation = callRepresentation(runtime)) { + is CallRepresentation.Instance -> builder.call(callRepresentation.call).buildExtrinsic() + is CallRepresentation.Bytes -> builder.buildExtrinsic(rawCallBytes = callRepresentation.bytes) + } + + val actualParsedExtrinsic = parsedExtrinsic.copy( + metadataHash = actualMetadataHash.checkMetadataHash.metadataHash + ) + + return ActualExtrinsic( + signedExtrinsic = extrinsic, + modifiedOriginal = actualMetadataHash.modifiedOriginal, + actualParsedExtrinsic = actualParsedExtrinsic + ) + } + + private suspend fun PolkadotSignPayload.Json.actualMetadataHash(chain: Chain, signer: NovaSigner): ActualMetadataHash { + // If a dapp haven't declared a permission to modify extrinsic - return whatever metadataHash present in payload + if (withSignedTransaction != true) { + return ActualMetadataHash(modifiedOriginal = false, hexHash = metadataHash) + } + + // If a dapp have specified metadata hash explicitly - use it + if (metadataHash != null) { + return ActualMetadataHash(modifiedOriginal = false, hexHash = metadataHash) + } + + // Else generate and use our own proof + val metadataProof = metadataShortenerService.generateMetadataProofWithSignerRestrictions(chain, signer) + return ActualMetadataHash(modifiedOriginal = true, checkMetadataHash = metadataProof.checkMetadataHash) } private fun PolkadotSignPayload.Json.callRepresentation(runtime: RuntimeSnapshot): CallRepresentation = runCatching { @@ -250,8 +287,35 @@ class PolkadotExternalSignInteractor( blockHash = blockHash.fromHex(), era = EraType.fromHex(runtime, era), tip = tip.bigIntegerFromHex(), - call = callRepresentation(runtime) + call = callRepresentation(runtime), + metadataHash = metadataHash?.fromHex() ) } } + + private class ActualMetadataHash(val modifiedOriginal: Boolean, val checkMetadataHash: CheckMetadataHash) { + + constructor(modifiedOriginal: Boolean, hash: ByteArray?) : this(modifiedOriginal, CheckMetadataHash(hash)) + + constructor(modifiedOriginal: Boolean, hexHash: String?) : this(modifiedOriginal, hexHash?.fromHex()) + } + + private data class ActualExtrinsic( + val signedExtrinsic: SendableExtrinsic, + val modifiedOriginal: Boolean, + val actualParsedExtrinsic: DAppParsedExtrinsic + ) + + private data class SignedResult(val signature: String, val modifiedTransaction: String?) } + +private fun CheckMetadataHash(hash: ByteArray?): CheckMetadataHash { + return if (hash != null) { + CheckMetadataHash.Enabled(hash) + } else { + CheckMetadataHash.Disabled + } +} + +private val CheckMetadataHash.metadataHash: ByteArray? + get() = if (this is CheckMetadataHash.Enabled) hash else null diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/request/AllHistoricalVotesRequest.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/request/AllHistoricalVotesRequest.kt index 341aed2b23..312bb2d51c 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/request/AllHistoricalVotesRequest.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/request/AllHistoricalVotesRequest.kt @@ -20,9 +20,7 @@ class AllHistoricalVotesRequest(address: String) { vote parent { referendumId - delegate { - accountId - } + delegateId standardVote } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/response/AllVotesResponse.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/response/AllVotesResponse.kt index c9892d131d..29e1b7626a 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/response/AllVotesResponse.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/offchain/delegation/v2/stats/response/AllVotesResponse.kt @@ -27,9 +27,7 @@ class DelegatedVoteRemote( class Parent( val referendumId: BigInteger, - val delegate: Delegate, + val delegateId: String, val standardVote: StandardVoteRemote? ) - - class Delegate(@SerializedName("accountId") val address: String) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2DelegationsRepository.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2DelegationsRepository.kt index 0c8e5130b0..625366550a 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2DelegationsRepository.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2DelegationsRepository.kt @@ -184,7 +184,7 @@ class Gov2DelegationsRepository( val standardVote = delegatedVoteRemote.vote UserVote.Delegated( - delegate = chain.accountIdOf(delegatedVoteRemote.parent.delegate.address), + delegate = chain.accountIdOf(delegatedVoteRemote.parent.delegateId), vote = AccountVote.Standard( balance = standardVote.amount, vote = Vote( diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/GovV2OnChainReferendaRepository.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/GovV2OnChainReferendaRepository.kt index e175bcc1e9..6f62803387 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/GovV2OnChainReferendaRepository.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/GovV2OnChainReferendaRepository.kt @@ -188,7 +188,7 @@ class GovV2OnChainReferendaRepository( "Rejected" -> OnChainReferendumStatus.Rejected(bindCompletedReferendumSince(asDictEnum.value)) "Cancelled" -> OnChainReferendumStatus.Cancelled(bindCompletedReferendumSince(asDictEnum.value)) "TimedOut" -> OnChainReferendumStatus.TimedOut(bindCompletedReferendumSince(asDictEnum.value)) - "Killed" -> OnChainReferendumStatus.Killed(bindCompletedReferendumSince(asDictEnum.value)) + "Killed" -> OnChainReferendumStatus.Killed(bindNumber(asDictEnum.value)) else -> throw IllegalArgumentException("Unsupported referendum status") } @@ -197,7 +197,7 @@ class GovV2OnChainReferendaRepository( status = referendumStatus ) } - .onFailure { Log.e(this.LOG_TAG, "Failed to decode on-chain referendum", it) } + .onFailure { Log.e(this.LOG_TAG, "Failed to decode on-chain referendum $id", it) } .getOrNull() private fun bindDecidingStatus(decoded: Any?): DecidingStatus? { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/TinderGovInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/TinderGovInteractor.kt index 76beae00a3..9ed15518eb 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/TinderGovInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/TinderGovInteractor.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.tinderg import io.novafoundation.nova.common.domain.filterLoaded import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_governance_api.data.model.TinderGovBasketItem import io.novafoundation.nova.feature_governance_api.data.model.VotingPower @@ -17,6 +18,8 @@ import io.novafoundation.nova.feature_governance_impl.data.repository.tindergov. import io.novafoundation.nova.feature_governance_impl.domain.referendum.details.call.ReferendumPreImageParser import io.novafoundation.nova.feature_governance_impl.domain.referendum.list.ReferendaSharedComputation import io.novafoundation.nova.feature_governance_impl.domain.referendum.list.filtering.ReferendaFilteringProvider +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation.StartStakingLandingValidationSystem +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation.startSwipeGovValidation import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.getCurrentAsset import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -54,6 +57,8 @@ interface TinderGovInteractor { suspend fun getVotingPowerState(): VotingPowerState suspend fun awaitAllItemsVoted(coroutineScope: CoroutineScope, basket: List) + + fun startSwipeGovValidationSystem(): StartStakingLandingValidationSystem } class RealTinderGovInteractor( @@ -130,6 +135,10 @@ class RealTinderGovInteractor( }.first() } + override fun startSwipeGovValidationSystem(): StartStakingLandingValidationSystem { + return ValidationSystem.startSwipeGovValidation() + } + private fun TinderGovBasketItem.isItemNotAvailableToVote( availableToVoteReferenda: Set, asset: Asset diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartStakingLandingValidationSystem.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartStakingLandingValidationSystem.kt new file mode 100644 index 0000000000..abb109bf3a --- /dev/null +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartStakingLandingValidationSystem.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation + +import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.domain.validation.hasChainAccount + +typealias StartStakingLandingValidationSystem = ValidationSystem + +fun ValidationSystem.Companion.startSwipeGovValidation(): StartStakingLandingValidationSystem = ValidationSystem { + hasChainAccount( + chain = { it.chain }, + metaAccount = { it.metaAccount }, + error = StartSwipeGovValidationFailure::NoChainAccountFound + ) +} diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartSwipeGovValidationFailure.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartSwipeGovValidationFailure.kt new file mode 100644 index 0000000000..dc782e4bd1 --- /dev/null +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartSwipeGovValidationFailure.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation + +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.validation.NoChainAccountFoundError +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +sealed class StartSwipeGovValidationFailure { + + class NoChainAccountFound( + override val chain: Chain, + override val account: MetaAccount, + override val addAccountState: NoChainAccountFoundError.AddAccountState + ) : StartSwipeGovValidationFailure(), NoChainAccountFoundError +} diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartSwipeGovValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartSwipeGovValidationPayload.kt new file mode 100644 index 0000000000..d27cff5cb7 --- /dev/null +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/tindergov/validation/StartSwipeGovValidationPayload.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation + +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class StartSwipeGovValidationPayload( + val chain: Chain, + val metaAccount: MetaAccount +) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListFragment.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListFragment.kt index e284d5e9b4..5ccafa4fc6 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListFragment.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListFragment.kt @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.ConcatAdapter import coil.ImageLoader import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.domain.dataOrNull +import io.novafoundation.nova.common.mixin.impl.observeValidations import io.novafoundation.nova.feature_governance_api.di.GovernanceFeatureApi import io.novafoundation.nova.feature_governance_impl.R import io.novafoundation.nova.feature_governance_impl.di.GovernanceFeatureComponent @@ -50,6 +51,8 @@ class ReferendaListFragment : BaseReferendaListFragment( override fun subscribe(viewModel: ReferendaListViewModel) { subscribeOnAssetClick(viewModel.assetSelectorMixin, imageLoader) + observeValidations(viewModel) + subscribeOnAssetChange(viewModel.assetSelectorMixin) { referendaHeaderAdapter.setAsset(it) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListViewModel.kt index 176e60a989..e04028a84e 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/ReferendaListViewModel.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.common.domain.ExtendedLoadingState import io.novafoundation.nova.common.domain.dataOrNull import io.novafoundation.nova.common.list.toListWithHeaders import io.novafoundation.nova.common.domain.mapLoading +import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.combineToPair @@ -14,6 +15,7 @@ import io.novafoundation.nova.common.utils.firstLoaded import io.novafoundation.nova.common.utils.formatting.format import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.withItemScope +import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.PlaceholderModel import io.novafoundation.nova.core.updater.UpdateSystem import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase @@ -34,10 +36,13 @@ import io.novafoundation.nova.feature_governance_impl.presentation.GovernanceRou import io.novafoundation.nova.feature_governance_impl.presentation.referenda.common.ReferendumFormatter import io.novafoundation.nova.feature_governance_impl.presentation.referenda.common.list.ReferendaListStateModel import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.ReferendumDetailsPayload +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.TinderGovInteractor +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation.StartSwipeGovValidationPayload import io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.model.ReferendaGroupModel import io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.model.ReferendumModel import io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.model.TinderGovBannerModel import io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.model.toReferendumDetailsPrefilledData +import io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.validation.handleStartSwipeGovValidationFailure import io.novafoundation.nova.feature_governance_impl.presentation.view.GovernanceLocksModel import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.assetSelector.AssetSelectorFactory @@ -64,8 +69,13 @@ class ReferendaListViewModel( private val governanceRouter: GovernanceRouter, private val referendumFormatter: ReferendumFormatter, private val governanceDAppsInteractor: GovernanceDAppsInteractor, - private val referendaSummaryInteractor: ReferendaSummaryInteractor -) : BaseViewModel(), WithAssetSelector { + private val referendaSummaryInteractor: ReferendaSummaryInteractor, + private val tinderGovInteractor: TinderGovInteractor, + private val selectedMetaAccountUseCase: SelectedAccountUseCase, + private val validationExecutor: ValidationExecutor, +) : BaseViewModel(), + WithAssetSelector, + Validatable by validationExecutor { override val assetSelectorMixin = assetSelectorFactory.create( scope = this, @@ -148,8 +158,25 @@ class ReferendaListViewModel( governanceRouter.openReferendum(payload) } - fun openTinderGovCards() { - governanceRouter.openTinderGovCards() + fun openTinderGovCards() = launch { + val payload = StartSwipeGovValidationPayload( + chain = selectedAssetSharedState.chain(), + metaAccount = selectedMetaAccountUseCase.getSelectedMetaAccount() + ) + + validationExecutor.requireValid( + validationSystem = tinderGovInteractor.startSwipeGovValidationSystem(), + payload = payload, + validationFailureTransformerCustom = { validationFailure, _ -> + handleStartSwipeGovValidationFailure( + resourceManager, + validationFailure, + governanceRouter + ) + } + ) { + governanceRouter.openTinderGovCards() + } } private fun mapLocksOverviewToUi(locksOverview: GovernanceLocksOverview?, asset: Asset): GovernanceLocksModel? { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/di/ReferendaListModule.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/di/ReferendaListModule.kt index 38f39b5a48..4dcb779f0e 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/di/ReferendaListModule.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/di/ReferendaListModule.kt @@ -9,6 +9,7 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.core.updater.UpdateSystem import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendaListInteractor @@ -16,6 +17,7 @@ import io.novafoundation.nova.feature_governance_impl.domain.summary.ReferendaSu import io.novafoundation.nova.feature_governance_impl.data.GovernanceSharedState import io.novafoundation.nova.feature_governance_impl.domain.dapp.GovernanceDAppsInteractor import io.novafoundation.nova.feature_governance_impl.domain.filters.ReferendaFiltersInteractor +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.TinderGovInteractor import io.novafoundation.nova.feature_governance_impl.presentation.GovernanceRouter import io.novafoundation.nova.feature_governance_impl.presentation.referenda.common.ReferendumFormatter import io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.ReferendaListViewModel @@ -38,7 +40,10 @@ class ReferendaListModule { governanceRouter: GovernanceRouter, referendumFormatter: ReferendumFormatter, governanceDAppsInteractor: GovernanceDAppsInteractor, - summaryInteractor: ReferendaSummaryInteractor + summaryInteractor: ReferendaSummaryInteractor, + tinderGovInteractor: TinderGovInteractor, + selectedMetaAccountUseCase: SelectedAccountUseCase, + validationExecutor: ValidationExecutor, ): ViewModel { return ReferendaListViewModel( assetSelectorFactory = assetSelectorFactory, @@ -51,7 +56,10 @@ class ReferendaListModule { governanceRouter = governanceRouter, referendumFormatter = referendumFormatter, governanceDAppsInteractor = governanceDAppsInteractor, - referendaSummaryInteractor = summaryInteractor + referendaSummaryInteractor = summaryInteractor, + tinderGovInteractor = tinderGovInteractor, + selectedMetaAccountUseCase = selectedMetaAccountUseCase, + validationExecutor = validationExecutor ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/validation/StartSwipeGovValidationUi.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/validation/StartSwipeGovValidationUi.kt new file mode 100644 index 0000000000..71309d053d --- /dev/null +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/list/validation/StartSwipeGovValidationUi.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_governance_impl.presentation.referenda.list.validation + +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.validation.TransformedFailure +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.feature_account_api.domain.validation.handleChainAccountNotFound +import io.novafoundation.nova.feature_governance_impl.R +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.validation.StartSwipeGovValidationFailure +import io.novafoundation.nova.feature_governance_impl.presentation.GovernanceRouter + +fun handleStartSwipeGovValidationFailure( + resourceManager: ResourceManager, + validationStatus: ValidationStatus.NotValid, + router: GovernanceRouter +): TransformedFailure { + return when (val reason = validationStatus.reason) { + is StartSwipeGovValidationFailure.NoChainAccountFound -> handleChainAccountNotFound( + failure = reason, + addAccountDescriptionRes = R.string.common_network_not_supported, + resourceManager = resourceManager, + goToWalletDetails = { router.openWalletDetails(reason.account.id) } + ) + } +} diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/basket/TinderGovBasketFragment.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/basket/TinderGovBasketFragment.kt index 44af75d766..5520a5a3ae 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/basket/TinderGovBasketFragment.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/basket/TinderGovBasketFragment.kt @@ -74,7 +74,7 @@ class TinderGovBasketFragment : BaseFragment(), Tinder onNegativeClick = { event.onSuccess(false) }, positiveTextRes = R.string.common_remove, negativeTextRes = R.string.common_cancel, - styleRes = R.style.AccentNegativeAlertDialogTheme, + styleRes = R.style.AccentNegativeAlertDialogTheme_Reversed, ) { setTitle(event.payload) setMessage(R.string.swipe_gov_basket_remove_item_confirm_message) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsFragment.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsFragment.kt index 24afb4eaaf..a678bcc80d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsFragment.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsFragment.kt @@ -122,8 +122,6 @@ class TinderGovCardsFragment : BaseFragment(), TinderGo } } - viewModel.manageVotingPowerAvailable.observe(tinderGovCardsSettings::setVisible) - viewModel.basketModelFlow.observe { tinderGovCardsBasketItems.text = it.items.toString() tinderGovCardsBasketItems.setTextColorRes(it.textColorRes) @@ -164,6 +162,7 @@ class TinderGovCardsFragment : BaseFragment(), TinderGo } viewModel.hasReferendaToVote.observe { + tinderGovCardsSettings.isVisible = it tinderGovCardsControlView.setVisible(it, falseState = View.INVISIBLE) // To avoid click if referenda cards is empty diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsViewModel.kt index 434a2c3ded..437a4c59aa 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/cards/TinderGovCardsViewModel.kt @@ -99,8 +99,6 @@ class TinderGovCardsViewModel( private var isVotingInProgress = MutableStateFlow(false) - val manageVotingPowerAvailable = topCardFlow.map { it != null } - val isCardDraggingAvailable = isVotingInProgress.map { !it } val basketModelFlow = basketFlow diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/di/SettingsFeatureModule.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/di/SettingsFeatureModule.kt index eef8f65b2a..d13c099193 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/di/SettingsFeatureModule.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/di/SettingsFeatureModule.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory import io.novafoundation.nova.feature_settings_impl.data.NodeChainIdRepositoryFactory import io.novafoundation.nova.feature_settings_impl.domain.AddNetworkInteractor @@ -70,9 +71,10 @@ class SettingsFeatureModule { fun provideNetworkManagementChainInteractor( chainRegistry: ChainRegistry, nodeHealthStateTesterFactory: NodeHealthStateTesterFactory, - chainRepository: ChainRepository + chainRepository: ChainRepository, + accountInteractor: AccountInteractor ): NetworkManagementChainInteractor { - return RealNetworkManagementChainInteractor(chainRegistry, nodeHealthStateTesterFactory, chainRepository) + return RealNetworkManagementChainInteractor(chainRegistry, nodeHealthStateTesterFactory, chainRepository, accountInteractor) } @Provides diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt index 57664ff4d8..7826013109 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt @@ -2,25 +2,28 @@ package io.novafoundation.nova.feature_settings_impl.domain import io.novafoundation.nova.common.utils.combine import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.runtime.ext.Geneses -import io.novafoundation.nova.runtime.ext.autoBalanceDisabled import io.novafoundation.nova.runtime.ext.genesisHash import io.novafoundation.nova.runtime.ext.isCustomNetwork import io.novafoundation.nova.runtime.ext.isDisabled import io.novafoundation.nova.runtime.ext.isEnabled -import io.novafoundation.nova.runtime.ext.selectedNodeUrlOrNull +import io.novafoundation.nova.runtime.ext.selectedUnformattedWssNodeUrlOrNull import io.novafoundation.nova.runtime.ext.wssNodes import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory import io.novafoundation.nova.runtime.repository.ChainRepository import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.withContext class ChainNetworkState( val chain: Chain, @@ -50,19 +53,20 @@ interface NetworkManagementChainInteractor { suspend fun toggleAutoBalance(chainId: String) - suspend fun selectNode(chainId: String, nodeUrl: String) + suspend fun selectNode(chainId: String, unformattedNodeUrl: String) suspend fun toggleChainEnableState(chainId: String) suspend fun deleteNetwork(chainId: String) - suspend fun deleteNode(chainId: String, nodeUrl: String) + suspend fun deleteNode(chainId: String, unformattedNodeUrl: String) } class RealNetworkManagementChainInteractor( private val chainRegistry: ChainRegistry, private val nodeHealthStateTesterFactory: NodeHealthStateTesterFactory, - private val chainRepository: ChainRepository + private val chainRepository: ChainRepository, + private val accountInteractor: AccountInteractor ) : NetworkManagementChainInteractor { override fun chainStateFlow(chainId: String, coroutineScope: CoroutineScope): Flow { @@ -77,11 +81,23 @@ class RealNetworkManagementChainInteractor( override suspend fun toggleAutoBalance(chainId: String) { val chain = chainRegistry.getChain(chainId) - chainRegistry.setAutoBalanceEnabled(chainId, chain.autoBalanceDisabled) + chainRegistry.setWssNodeSelectionStrategy(chainId, chain.nodes.strategyForToggledWssAutoBalance()) } - override suspend fun selectNode(chainId: String, nodeUrl: String) { - chainRegistry.setDefaultNode(chainId, nodeUrl) + private fun Chain.Nodes.strategyForToggledWssAutoBalance(): NodeSelectionStrategy { + return when (wssNodeSelectionStrategy) { + NodeSelectionStrategy.AutoBalance -> { + val firstNode = wssNodes().first() + NodeSelectionStrategy.SelectedNode(firstNode.unformattedUrl) + } + + is NodeSelectionStrategy.SelectedNode -> NodeSelectionStrategy.AutoBalance + } + } + + override suspend fun selectNode(chainId: String, unformattedNodeUrl: String) { + val strategy = NodeSelectionStrategy.SelectedNode(unformattedNodeUrl) + chainRegistry.setWssNodeSelectionStrategy(chainId, strategy) } override suspend fun toggleChainEnableState(chainId: String) { @@ -94,18 +110,20 @@ class RealNetworkManagementChainInteractor( val chain = chainRegistry.getChain(chainId) require(chain.isCustomNetwork) + + withContext(Dispatchers.Default) { accountInteractor.deleteProxiedMetaAccountsByChain(chainId) } // Delete proxied meta accounts manually chainRepository.deleteNetwork(chainId) } - override suspend fun deleteNode(chainId: String, nodeUrl: String) { + override suspend fun deleteNode(chainId: String, unformattedNodeUrl: String) { val chain = chainRegistry.getChain(chainId) require(chain.nodes.nodes.size > 1) - chainRepository.deleteNode(chainId, nodeUrl) + chainRepository.deleteNode(chainId, unformattedNodeUrl) - if (chain.selectedNodeUrlOrNull == nodeUrl) { - chainRegistry.setAutoBalanceEnabled(chain.id, true) + if (chain.selectedUnformattedWssNodeUrlOrNull == unformattedNodeUrl) { + chainRegistry.setWssNodeSelectionStrategy(chainId, NodeSelectionStrategy.AutoBalance) } } diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt index 1ec3eaa953..2a6ec8f606 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/utils/CustomChainFactory.kt @@ -15,7 +15,7 @@ import io.novafoundation.nova.runtime.ext.EVM_DEFAULT_TOKEN_DECIMALS import io.novafoundation.nova.runtime.ext.evmChainIdFrom import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy.AutoBalance +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnection import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory import io.novafoundation.nova.runtime.network.rpc.systemProperties @@ -102,23 +102,21 @@ class CustomChainFactory( source = Chain.Asset.Source.MANUAL, ) - val node = Chain.Node( - chainId = chainId, - unformattedUrl = payload.nodeUrl, - name = payload.nodeName, - orderId = 0, - isCustom = true, - ) - val explorer = getChainExplorer(payload.blockExplorer, chainId) + val nodes = Chain.Nodes( + autoBalanceStrategy = prefilledChain?.nodes?.autoBalanceStrategy ?: Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN, + wssNodeSelectionStrategy = NodeSelectionStrategy.AutoBalance, + nodes = createNodeList(chainId, prefilledChain, payload) + ) + return Chain( id = chainId, parentId = prefilledChain?.parentId, name = payload.chainName, assets = listOf(asset), - nodes = Chain.Nodes(prefilledChain?.nodes?.nodeSelectionStrategy ?: AutoBalance.ROUND_ROBIN, listOf(node)), - explorers = explorer?.let { listOf(it) } ?: prefilledChain?.explorers.orEmpty(), + nodes = nodes, + explorers = explorer?.let(::listOf) ?: prefilledChain?.explorers.orEmpty(), externalApis = prefilledChain?.externalApis.orEmpty(), icon = prefilledChain?.icon, addressPrefix = addressPrefix, @@ -138,6 +136,37 @@ class CustomChainFactory( ) } + private fun createNodeList( + chainId: String, + prefilledChain: Chain?, + input: CustomNetworkPayload + ): List { + val inputNode = Chain.Node( + chainId = chainId, + unformattedUrl = input.nodeUrl, + name = input.nodeName, + orderId = 0, + isCustom = true, + ) + + val prefilledNodes = prefilledChain?.nodes?.nodes.orEmpty() + val prefilledExceptInput = prefilledNodes.mapNotNull { + val differentFromInput = it.unformattedUrl != inputNode.unformattedUrl + + if (differentFromInput) { + // Consider prefilled nodes as custom + it.copy(isCustom = true) + } else { + null + } + } + + return buildList { + add(inputNode) + addAll(prefilledExceptInput) + } + } + fun getChainExplorer(blockExplorer: CustomNetworkPayload.BlockExplorer?, chainId: String): Chain.Explorer? { return blockExplorer?.let { val links = blockExplorerLinkFormatter.format(it.url) diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/ChainNetworkManagementViewModel.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/ChainNetworkManagementViewModel.kt index 807bbdc41c..61ae27ee64 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/ChainNetworkManagementViewModel.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/ChainNetworkManagementViewModel.kt @@ -83,7 +83,7 @@ class ChainNetworkManagementViewModel( fun selectNode(item: NetworkNodeRvItem) { launch { - networkManagementChainInteractor.selectNode(payload.chainId, item.url) + networkManagementChainInteractor.selectNode(payload.chainId, item.unformattedUrl) } } @@ -96,12 +96,12 @@ class ChainNetworkManagementViewModel( R.string.manage_node_actions_title, subtitle = item.name, listOf( - editItem(R.string.manage_node_action_edit) { editNode(item.url) }, + editItem(R.string.manage_node_action_edit) { editNode(item.unformattedUrl) }, deleteItem(R.string.manage_node_action_delete) { deleteNode(item) } ) ) } else { - editNode(item.url) + editNode(item.unformattedUrl) } } } @@ -151,7 +151,7 @@ class ChainNetworkManagementViewModel( ) ) - networkManagementChainInteractor.deleteNode(chainId = payload.chainId, item.url) + networkManagementChainInteractor.deleteNode(chainId = payload.chainId, unformattedNodeUrl = item.unformattedUrl) } } @@ -181,7 +181,7 @@ class ChainNetworkManagementViewModel( return NetworkNodeRvItem( id = nodeHealthState.node.unformattedUrl, name = nodeHealthState.node.name, - url = nodeHealthState.node.unformattedUrl, + unformattedUrl = nodeHealthState.node.unformattedUrl, isEditable = nodeHealthState.node.isCustom, isDeletable = networkState.nodeHealthStates.size > 1 && nodeHealthState.node.isCustom, isSelected = nodeHealthState.node.unformattedUrl == networkState.connectingNode?.unformattedUrl, diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/ChainNetworkManagementNodesAdapter.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/ChainNetworkManagementNodesAdapter.kt index 8e568d358a..a2bf7dfef1 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/ChainNetworkManagementNodesAdapter.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/ChainNetworkManagementNodesAdapter.kt @@ -100,7 +100,7 @@ class ChainNetworkManagementNodeViewHolder( chainNodeRadioButton.isEnabled = item.isSelectable chainNodeName.text = item.name chainNodeName.setTextColorRes(item.nameColorRes) - chainNodeSocketAddress.text = item.url + chainNodeSocketAddress.text = item.unformattedUrl chainNodeConnectionStatusShimmering.setShimmerShown(item.connectionState.showShimmering) chainNodeConnectionState.setText(item.connectionState.name) item.connectionState.chainStatusColor?.let { chainNodeConnectionState.setTextColor(it) } diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/items/NetworkNodeRvItem.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/items/NetworkNodeRvItem.kt index dbefb08349..e07fa0cd5c 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/items/NetworkNodeRvItem.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/networkManagement/chain/nodeAdapter/items/NetworkNodeRvItem.kt @@ -7,7 +7,7 @@ data class NetworkNodeRvItem( val id: String, val name: String, @ColorRes val nameColorRes: Int, - val url: String, + val unformattedUrl: String, val isEditable: Boolean, val isDeletable: Boolean, val isSelected: Boolean, diff --git a/feature-settings-impl/src/main/res/layout/fragment_settings.xml b/feature-settings-impl/src/main/res/layout/fragment_settings.xml index 25ece4f973..110b3b98b1 100644 --- a/feature-settings-impl/src/main/res/layout/fragment_settings.xml +++ b/feature-settings-impl/src/main/res/layout/fragment_settings.xml @@ -161,66 +161,66 @@ + android:text="@string/settings_support" /> + app:icon="@drawable/ic_mail_outline" + app:title="@string/settings_email" /> + app:icon="@drawable/ic_nova_wiki" + app:title="@string/settings_wiki" /> + app:icon="@drawable/ic_rate_us" + app:title="@string/about_rate_app" /> + + android:text="@string/settings_community" /> + app:icon="@drawable/ic_tg" + app:title="@string/about_telegram_v2_2_0" /> + app:icon="@drawable/ic_twitter" + app:title="@string/settings_twitter" /> + app:icon="@drawable/ic_youtube" + app:title="@string/settings_youtube" /> - , externalRequirementsFlow: MutableStateFlow, nodeAutobalancer: NodeAutobalancer, - connectionSecrets: ConnectionSecrets ) = ChainConnectionFactory( externalRequirementsFlow, nodeAutobalancer, socketProvider, - connectionSecrets ) @Provides @@ -195,7 +194,6 @@ class ChainRegistryModule { @Provides @ApplicationScope fun provideWeb3ApiFactory( - connectionSecrets: ConnectionSecrets, strategyProvider: NodeSelectionStrategyProvider, ): Web3ApiFactory { val builder = HttpService.getOkHttpClientBuilder() @@ -207,7 +205,7 @@ class ChainRegistryModule { val okHttpClient = builder.build() - return Web3ApiFactory(connectionSecrets = connectionSecrets, strategyProvider = strategyProvider, httpClient = okHttpClient) + return Web3ApiFactory(strategyProvider = strategyProvider, httpClient = okHttpClient) } @Provides diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/BalancingHttpWeb3jService.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/BalancingHttpWeb3jService.kt index 4a8cab4569..eb62359037 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/BalancingHttpWeb3jService.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/BalancingHttpWeb3jService.kt @@ -6,15 +6,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode import io.novafoundation.nova.common.utils.requireException import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl import io.novafoundation.nova.runtime.multiNetwork.connection.UpdatableNodes -import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionSequenceStrategy import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider +import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSequenceGenerator import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.generateNodeIterator -import io.novafoundation.nova.runtime.multiNetwork.connection.saturateNodeUrls -import io.reactivex.Flowable import io.novasama.substrate_sdk_android.extensions.tryFindNonNull +import io.reactivex.Flowable import okhttp3.Call import okhttp3.Callback import okhttp3.OkHttpClient @@ -31,27 +29,21 @@ import org.web3j.protocol.http.HttpService import org.web3j.protocol.websocket.events.Notification import java.io.IOException import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService class BalancingHttpWeb3jService( initialNodes: Chain.Nodes, - connectionSecrets: ConnectionSecrets, private val httpClient: OkHttpClient, private val strategyProvider: NodeSelectionStrategyProvider, private val objectMapper: ObjectMapper = ObjectMapperFactory.getObjectMapper(), - private val executorService: ExecutorService, ) : Web3jService, UpdatableNodes { private val nodeSwitcher = NodeSwitcher( - initialNodes = initialNodes.nodes, - initialStrategy = strategyProvider.strategyFor(initialNodes.nodeSelectionStrategy), - connectionSecrets = connectionSecrets + initialStrategy = strategyProvider.createHttp(initialNodes), ) override fun updateNodes(nodes: Chain.Nodes) { - val autoBalanceStrategy = strategyProvider.strategyFor(nodes.nodeSelectionStrategy) - - nodeSwitcher.updateNodes(nodes.nodes, autoBalanceStrategy) + val strategy = strategyProvider.createHttp(nodes) + nodeSwitcher.updateNodes(strategy) } override fun > send(request: Request<*, out Response<*>>, responseType: Class): T { @@ -245,16 +237,11 @@ class BalancingHttpWeb3jService( } private class NodeSwitcher( - initialNodes: List, - initialStrategy: NodeSelectionSequenceStrategy, - private val connectionSecrets: ConnectionSecrets, + initialStrategy: NodeSequenceGenerator, ) { @Volatile - private var availableNodes: List = initialNodes - - @Volatile - private var balanceStrategy: NodeSelectionSequenceStrategy = initialStrategy + private var balanceStrategy: NodeSequenceGenerator = initialStrategy @Volatile private var nodeIterator: Iterator? = null @@ -263,18 +250,14 @@ private class NodeSwitcher( private var currentNodeUrl: String? = null init { - updateNodes(initialNodes, initialStrategy) + updateNodes(initialStrategy) } @Synchronized - fun updateNodes(nodes: List, strategy: NodeSelectionSequenceStrategy) { - val saturatedNodes = nodes.saturateNodeUrls(connectionSecrets) - if (saturatedNodes.isEmpty()) return - - availableNodes = nodes + fun updateNodes(strategy: NodeSequenceGenerator) { balanceStrategy = strategy - nodeIterator = balanceStrategy.generateNodeIterator(saturatedNodes) + nodeIterator = balanceStrategy.generateNodeIterator() selectNextNode() } @@ -289,7 +272,10 @@ private class NodeSwitcher( } private fun selectNextNode() { - currentNodeUrl = nodeIterator?.next()?.saturatedUrl + val iterator = nodeIterator ?: return + if (iterator.hasNext()) { + currentNodeUrl = iterator.next().saturatedUrl + } } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/Web3Api.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/Web3Api.kt index 3b4c2ae6b1..9d646e3f22 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/Web3Api.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/Web3Api.kt @@ -3,7 +3,6 @@ package io.novafoundation.nova.runtime.ethereum import io.novafoundation.nova.core.ethereum.Web3Api import io.novafoundation.nova.core.ethereum.log.Topic import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets import io.novafoundation.nova.runtime.multiNetwork.connection.UpdatableNodes import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider import io.novasama.substrate_sdk_android.extensions.requireHexPrefix @@ -23,7 +22,6 @@ import java.util.concurrent.ScheduledExecutorService class Web3ApiFactory( private val requestExecutorService: ScheduledExecutorService = Async.defaultExecutorService(), - private val connectionSecrets: ConnectionSecrets, private val httpClient: OkHttpClient, private val strategyProvider: NodeSelectionStrategyProvider, ) { @@ -37,17 +35,21 @@ class Web3ApiFactory( ) } - fun createHttps(chainNodes: Chain.Node): Pair { - return createHttps(Chain.Nodes(Chain.Nodes.NodeSelectionStrategy.AutoBalance.ROUND_ROBIN, listOf(chainNodes))) + fun createHttps(chainNode: Chain.Node): Pair { + val nodes = Chain.Nodes( + autoBalanceStrategy = Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN, + wssNodeSelectionStrategy = Chain.Nodes.NodeSelectionStrategy.AutoBalance, + nodes = listOf(chainNode) + ) + + return createHttps(nodes) } fun createHttps(chainNodes: Chain.Nodes): Pair { val service = BalancingHttpWeb3jService( initialNodes = chainNodes, - connectionSecrets = connectionSecrets, httpClient = httpClient, strategyProvider = strategyProvider, - executorService = requestExecutorService ) val api = RealWeb3Api( diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt index a9fbd253ec..8d084be570 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt @@ -49,14 +49,11 @@ const val EVM_DEFAULT_TOKEN_DECIMALS = 18 private const val EIP_155_PREFIX = "eip155" val Chain.autoBalanceEnabled: Boolean - get() = nodes.nodeSelectionStrategy is Chain.Nodes.NodeSelectionStrategy.AutoBalance + get() = nodes.wssNodeSelectionStrategy is Chain.Nodes.NodeSelectionStrategy.AutoBalance -val Chain.autoBalanceDisabled: Boolean - get() = !autoBalanceEnabled - -val Chain.selectedNodeUrlOrNull: String? - get() = if (nodes.nodeSelectionStrategy is Chain.Nodes.NodeSelectionStrategy.SelectedNode) { - nodes.nodeSelectionStrategy.nodeUrl +val Chain.selectedUnformattedWssNodeUrlOrNull: String? + get() = if (nodes.wssNodeSelectionStrategy is Chain.Nodes.NodeSelectionStrategy.SelectedNode) { + nodes.wssNodeSelectionStrategy.unformattedNodeUrl } else { null } @@ -215,8 +212,12 @@ fun Chain.Nodes.wssNodes(): List { return nodes.filter { it.isWss } } -fun Chain.Nodes.httpNodes(): Chain.Nodes { - return copy(nodes = nodes.filter { it.isHttps }) +fun Chain.Nodes.httpNodes(): List { + return nodes.filter { it.isHttps } +} + +fun Chain.Nodes.hasHttpNodes(): Boolean { + return nodes.any { it.isHttps } } val Chain.Asset.disabled: Boolean diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/ExtrinsicBuilderFactory.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/ExtrinsicBuilderFactory.kt index 0b2860d074..3d1c1bcefa 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/ExtrinsicBuilderFactory.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/ExtrinsicBuilderFactory.kt @@ -3,9 +3,9 @@ package io.novafoundation.nova.runtime.extrinsic import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.requireGenesisHash -import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataProof import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner +import io.novafoundation.nova.runtime.extrinsic.signer.generateMetadataProofWithSignerRestrictions import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getRuntime @@ -87,15 +87,4 @@ class ExtrinsicBuilderFactory( newElement } } - - private suspend fun MetadataShortenerService.generateMetadataProofWithSignerRestrictions( - chain: Chain, - signer: NovaSigner, - ): MetadataProof { - return if (signer.supportsCheckMetadataHash(chain)) { - generateMetadataProof(chain.id) - } else { - generateDisabledMetadataProof(chain.id) - } - } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt index 7c11f5c515..9703246a7d 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt @@ -108,6 +108,7 @@ internal class RealExtrinsicSplitter( accountId = signer.signerAccountId(chain) ) .call(call) - .build() + .buildExtrinsic() + .extrinsicHex } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/MetadataHashSigning.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/MetadataHashSigning.kt new file mode 100644 index 0000000000..2cc1a26eab --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/MetadataHashSigning.kt @@ -0,0 +1,16 @@ +package io.novafoundation.nova.runtime.extrinsic.signer + +import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataProof +import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +suspend fun MetadataShortenerService.generateMetadataProofWithSignerRestrictions( + chain: Chain, + signer: NovaSigner, +): MetadataProof { + return if (signer.supportsCheckMetadataHash(chain)) { + generateMetadataProof(chain.id) + } else { + generateDisabledMetadataProof(chain.id) + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt index 8329e72331..f8c0879a0a 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt @@ -134,17 +134,24 @@ class ChainRegistry( chainDao.setConnectionState(chainId, connectionState) } - suspend fun setAutoBalanceEnabled(chainId: ChainId, enabled: Boolean) { - chainDao.setNodePreferences(NodeSelectionPreferencesLocal(chainId, enabled, null)) + suspend fun setWssNodeSelectionStrategy(chainId: String, strategy: Chain.Nodes.NodeSelectionStrategy) { + return when (strategy) { + Chain.Nodes.NodeSelectionStrategy.AutoBalance -> enableAutoBalance(chainId) + is Chain.Nodes.NodeSelectionStrategy.SelectedNode -> setSelectedNode(chainId, strategy.unformattedNodeUrl) + } + } + + private suspend fun enableAutoBalance(chainId: ChainId) { + chainDao.setNodePreferences(NodeSelectionPreferencesLocal(chainId, autoBalanceEnabled = false, null)) } - suspend fun setDefaultNode(chainId: ChainId, nodeUrl: String) { + private suspend fun setSelectedNode(chainId: ChainId, unformattedNodeUrl: String) { val chain = getChain(chainId) - val chainSupportsNode = chain.nodes.nodes.any { it.unformattedUrl == nodeUrl } - require(chainSupportsNode) { "Node with url $nodeUrl is not found for chain $chainId" } + val chainSupportsNode = chain.nodes.nodes.any { it.unformattedUrl == unformattedNodeUrl } + require(chainSupportsNode) { "Node with url $unformattedNodeUrl is not found for chain $chainId" } - chainDao.setNodePreferences(NodeSelectionPreferencesLocal(chainId, false, nodeUrl)) + chainDao.setNodePreferences(NodeSelectionPreferencesLocal(chainId, false, unformattedNodeUrl)) } private suspend fun requireConnectionStateAtLeast(chainId: ChainId, state: ConnectionState) { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt index 8048c72cf3..fb90529a77 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt @@ -13,7 +13,7 @@ import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLoca import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal import io.novafoundation.nova.runtime.ext.autoBalanceEnabled -import io.novafoundation.nova.runtime.ext.selectedNodeUrlOrNull +import io.novafoundation.nova.runtime.ext.selectedUnformattedWssNodeUrlOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.EVM_TRANSFER_PARAMETER import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.GovernanceReferendaParameters import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.SUBSTRATE_TRANSFER_PARAMETER @@ -157,19 +157,14 @@ fun mapNodeSelectionPreferencesToLocal(chain: Chain): NodeSelectionPreferencesLo return NodeSelectionPreferencesLocal( chainId = chain.id, autoBalanceEnabled = chain.autoBalanceEnabled, - selectedNodeUrl = chain.selectedNodeUrlOrNull + selectedNodeUrl = chain.selectedUnformattedWssNodeUrlOrNull ) } -fun mapNodeSelectionStrategyToLocal(domain: Chain): ChainLocal.NodeSelectionStrategyLocal { - val autobalanceStrategy = when (val strategy = domain.nodes.nodeSelectionStrategy) { - is Chain.Nodes.NodeSelectionStrategy.SelectedNode -> strategy.autoBalanceStrategy - is Chain.Nodes.NodeSelectionStrategy.AutoBalance -> strategy - } - - return when (autobalanceStrategy) { - Chain.Nodes.NodeSelectionStrategy.AutoBalance.ROUND_ROBIN -> ChainLocal.NodeSelectionStrategyLocal.ROUND_ROBIN - Chain.Nodes.NodeSelectionStrategy.AutoBalance.UNIFORM -> ChainLocal.NodeSelectionStrategyLocal.UNIFORM +fun mapNodeSelectionStrategyToLocal(domain: Chain): ChainLocal.AutoBalanceStrategyLocal { + return when (domain.nodes.autoBalanceStrategy) { + Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN -> ChainLocal.AutoBalanceStrategyLocal.ROUND_ROBIN + Chain.Nodes.AutoBalanceStrategy.UNIFORM -> ChainLocal.AutoBalanceStrategyLocal.UNIFORM } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt index aa89dd769a..81ce937317 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt @@ -16,8 +16,8 @@ import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.ApiType import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.SourceType import io.novafoundation.nova.core_db.model.chain.ChainLocal +import io.novafoundation.nova.core_db.model.chain.ChainLocal.AutoBalanceStrategyLocal import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLocal -import io.novafoundation.nova.core_db.model.chain.ChainLocal.NodeSelectionStrategyLocal import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal import io.novafoundation.nova.core_db.model.chain.JoinedChainInfo import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal @@ -30,6 +30,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.BuyProviderId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.ConnectionState import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.ExternalApi +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.AutoBalanceStrategy import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId @@ -161,17 +162,23 @@ private fun mapExternalApiLocalToExternalApi(externalApiLocal: ChainExternalApiL } }.getOrNull() -private fun mapNodeSelectionFromLocal(chainLocal: ChainLocal, nodeSelectionPreferencesLocal: NodeSelectionPreferencesLocal?): NodeSelectionStrategy { - val autoBalanceStrategy = when (chainLocal.nodeSelectionStrategy) { - NodeSelectionStrategyLocal.ROUND_ROBIN -> NodeSelectionStrategy.AutoBalance.ROUND_ROBIN - NodeSelectionStrategyLocal.UNIFORM -> NodeSelectionStrategy.AutoBalance.UNIFORM - NodeSelectionStrategyLocal.UNKNOWN -> NodeSelectionStrategy.AutoBalance.ROUND_ROBIN +private fun mapAutoBalanceStrategyFromLocal(local: AutoBalanceStrategyLocal): AutoBalanceStrategy { + return when (local) { + AutoBalanceStrategyLocal.ROUND_ROBIN -> AutoBalanceStrategy.ROUND_ROBIN + AutoBalanceStrategyLocal.UNIFORM -> AutoBalanceStrategy.UNIFORM + AutoBalanceStrategyLocal.UNKNOWN -> AutoBalanceStrategy.ROUND_ROBIN } +} + +private fun mapNodeSelectionFromLocal(nodeSelectionPreferencesLocal: NodeSelectionPreferencesLocal?): NodeSelectionStrategy { + if (nodeSelectionPreferencesLocal == null) return NodeSelectionStrategy.AutoBalance + + val selectedUnformattedWssUrl = nodeSelectionPreferencesLocal.selectedUnformattedWssNodeUrl - return if (nodeSelectionPreferencesLocal?.autoBalanceEnabled == true) { - autoBalanceStrategy + return if (selectedUnformattedWssUrl != null && !nodeSelectionPreferencesLocal.autoBalanceEnabled) { + NodeSelectionStrategy.SelectedNode(selectedUnformattedWssUrl) } else { - NodeSelectionStrategy.SelectedNode(nodeSelectionPreferencesLocal?.selectedNodeUrl, autoBalanceStrategy) + NodeSelectionStrategy.AutoBalance } } @@ -207,7 +214,8 @@ fun mapChainLocalToChain( } val nodesConfig = Chain.Nodes( - nodeSelectionStrategy = mapNodeSelectionFromLocal(chainLocal, nodeSelectionPreferences), + autoBalanceStrategy = mapAutoBalanceStrategyFromLocal(chainLocal.autoBalanceStrategy), + wssNodeSelectionStrategy = mapNodeSelectionFromLocal(nodeSelectionPreferences), nodes = nodes ) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt index eb4fa5cd0e..0652fef8bd 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt @@ -12,7 +12,7 @@ import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.SourceTy import io.novafoundation.nova.core_db.model.chain.ChainLocal import io.novafoundation.nova.core_db.model.chain.ChainLocal.Companion.EMPTY_CHAIN_ICON import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLocal -import io.novafoundation.nova.core_db.model.chain.ChainLocal.NodeSelectionStrategyLocal +import io.novafoundation.nova.core_db.model.chain.ChainLocal.AutoBalanceStrategyLocal import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId @@ -99,11 +99,11 @@ fun mapRemoteChainToLocal( return chainLocal } -private fun mapNodeSelectionStrategyToLocal(remote: String?): NodeSelectionStrategyLocal { +private fun mapNodeSelectionStrategyToLocal(remote: String?): AutoBalanceStrategyLocal { return when (remote) { - null, "roundRobin" -> NodeSelectionStrategyLocal.ROUND_ROBIN - "uniform" -> NodeSelectionStrategyLocal.UNIFORM - else -> NodeSelectionStrategyLocal.UNKNOWN + null, "roundRobin" -> AutoBalanceStrategyLocal.ROUND_ROBIN + "uniform" -> AutoBalanceStrategyLocal.UNIFORM + else -> AutoBalanceStrategyLocal.UNKNOWN } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt index cdaf7acf91..76b61f9ae2 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt @@ -122,17 +122,20 @@ data class Chain( } data class Nodes( - val nodeSelectionStrategy: NodeSelectionStrategy, + val autoBalanceStrategy: AutoBalanceStrategy, + val wssNodeSelectionStrategy: NodeSelectionStrategy, val nodes: List, ) { - sealed interface NodeSelectionStrategy { + enum class AutoBalanceStrategy { + ROUND_ROBIN, UNIFORM + } - enum class AutoBalance : NodeSelectionStrategy { - ROUND_ROBIN, UNIFORM - } + sealed class NodeSelectionStrategy { + + object AutoBalance : NodeSelectionStrategy() - class SelectedNode(val nodeUrl: String?, val autoBalanceStrategy: AutoBalance) : NodeSelectionStrategy + class SelectedNode(val unformattedNodeUrl: String) : NodeSelectionStrategy() } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt index 3aad16a16f..23db36343b 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.runtime.multiNetwork.connection import android.util.Log import io.novafoundation.nova.common.utils.LOG_TAG +import io.novafoundation.nova.common.utils.share import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.NodeAutobalancer import io.novasama.substrate_sdk_android.wsrpc.SocketService @@ -17,23 +18,22 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import javax.inject.Provider -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map class ChainConnectionFactory( private val externalRequirementFlow: Flow, private val nodeAutobalancer: NodeAutobalancer, private val socketServiceProvider: Provider, - private val connectionSecrets: ConnectionSecrets ) { suspend fun create(chain: Chain): ChainConnection { @@ -41,7 +41,6 @@ class ChainConnectionFactory( socketService = socketServiceProvider.get(), externalRequirementFlow = externalRequirementFlow, nodeAutobalancer = nodeAutobalancer, - connectionSecrets = connectionSecrets, initialChain = chain ) @@ -68,7 +67,6 @@ class ChainConnection internal constructor( val socketService: SocketService, private val externalRequirementFlow: Flow, private val nodeAutobalancer: NodeAutobalancer, - private val connectionSecrets: ConnectionSecrets, initialChain: Chain, ) : CoroutineScope by CoroutineScope(Dispatchers.Default), WebSocketResponseInterceptor { @@ -88,16 +86,21 @@ class ChainConnection internal constructor( ).shareIn(scope = this, started = SharingStarted.Eagerly) private val chain = MutableStateFlow(initialChain) + private val availableNodes = chain.map { it.nodes } - .shareIn(scope = this, started = SharingStarted.Eagerly, replay = 1) + .distinctUntilChanged() + .share(SharingStarted.Eagerly) - val currentUrl = chain.flatMapLatest { getNodeUrlFlow(it) } - .shareIn(scope = this, started = SharingStarted.Eagerly, replay = 1) + val currentUrl = nodeAutobalancer.connectionUrlFlow( + chainId = initialChain.id, + changeConnectionEventFlow = nodeChangeSignal, + availableNodesFlow = availableNodes, + ).share(SharingStarted.Eagerly) suspend fun setup() { socketService.setInterceptor(this) - observeCurrentNode(chain.value) + observeCurrentNode() externalRequirementFlow.onEach { if (it == ExternalRequirement.ALLOWED) { @@ -109,27 +112,16 @@ class ChainConnection internal constructor( .launchIn(this) } - private fun getNodeUrlFlow(chain: Chain): Flow { - return getAutobalancedNodeUrlFlow(chain) - } - - private fun getAutobalancedNodeUrlFlow(chain: Chain): Flow { - return nodeAutobalancer.connectionUrlFlow( - chainId = chain.id, - changeConnectionEventFlow = nodeChangeSignal, - availableNodesFlow = availableNodes, - ) - } - - private suspend fun observeCurrentNode(chain: Chain) { + private suspend fun observeCurrentNode() { + // Important - this should be awaited first before setting up externalRequirementFlow subscription + // Otherwise there might be a race between both of them val firstNodeUrl = currentUrl.first()?.saturatedUrl ?: return socketService.start(firstNodeUrl, remainPaused = true) - currentUrl - .mapNotNull { it?.saturatedUrl } + currentUrl.mapNotNull { it?.saturatedUrl } .filter { nodeUrl -> actualUrl() != nodeUrl } .onEach { nodeUrl -> socketService.switchUrl(nodeUrl) } - .onEach { nodeUrl -> Log.d(this@ChainConnection.LOG_TAG, "Switching node in ${chain.name} to $nodeUrl") } + .onEach { nodeUrl -> Log.d(this@ChainConnection.LOG_TAG, "Switching node in ${chain.value.name} to $nodeUrl") } .launchIn(this) } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/Web3ApiPool.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/Web3ApiPool.kt index 75db664f28..af9a5c7dd3 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/Web3ApiPool.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/Web3ApiPool.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.runtime.multiNetwork.connection import io.novafoundation.nova.core.ethereum.Web3Api import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory -import io.novafoundation.nova.runtime.ext.httpNodes +import io.novafoundation.nova.runtime.ext.hasHttpNodes import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Node.ConnectionType import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId @@ -25,19 +25,19 @@ class Web3ApiPool(private val web3ApiFactory: Web3ApiFactory) { } fun setupHttpsApi(chain: Chain): Web3Api? { - val httpNodes = chain.nodes.httpNodes() + val chainNodes = chain.nodes - if (httpNodes.nodes.isEmpty()) { + if (!chainNodes.hasHttpNodes()) { removeApi(chain.id, ConnectionType.HTTPS) return null } val (web3Api, updatableNodes) = pool.getOrPut(chain.id to ConnectionType.HTTPS) { - web3ApiFactory.createHttps(httpNodes) + web3ApiFactory.createHttps(chainNodes) } - updatableNodes?.updateNodes(httpNodes) + updatableNodes?.updateNodes(chainNodes) return web3Api } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancer.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancer.kt index 763462829b..9b92e9c457 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancer.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancer.kt @@ -2,49 +2,47 @@ package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance import android.util.Log import io.novafoundation.nova.common.utils.LOG_TAG -import io.novafoundation.nova.runtime.ext.wssNodes import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider -import io.novafoundation.nova.runtime.multiNetwork.connection.saturateNodeUrls +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.transformLatest class NodeAutobalancer( private val autobalanceStrategyProvider: NodeSelectionStrategyProvider, - private val connectionSecrets: ConnectionSecrets, ) { + @OptIn(ExperimentalCoroutinesApi::class) fun connectionUrlFlow( chainId: ChainId, changeConnectionEventFlow: Flow, availableNodesFlow: Flow, ): Flow { - return availableNodesFlow.flatMapLatest { nodesConfig -> - autobalanceStrategyProvider.strategyFlowFor(chainId, nodesConfig.nodeSelectionStrategy).transform { strategy -> - Log.d(this@NodeAutobalancer.LOG_TAG, "Using ${nodesConfig.nodeSelectionStrategy} strategy for switching nodes in $chainId") + return availableNodesFlow.transformLatest { nodesConfig -> + Log.d(this@NodeAutobalancer.LOG_TAG, "Using ${nodesConfig.wssNodeSelectionStrategy} strategy for switching nodes in $chainId") - val wssNodes = nodesConfig.wssNodes().saturateNodeUrls(connectionSecrets) + val strategy = autobalanceStrategyProvider.createWss(nodesConfig) - if (wssNodes.isEmpty()) { - Log.w(this@NodeAutobalancer.LOG_TAG, "No wss nodes available for chain $chainId") - - emit(null) - return@transform - } - - val nodeIterator = strategy.generateNodeSequence(wssNodes).iterator() + val nodeIterator = strategy.generateNodeSequence().iterator() + if (!nodeIterator.hasNext()) { + Log.w(this@NodeAutobalancer.LOG_TAG, "No wss nodes available for chain $chainId using strategy $strategy") + return@transformLatest + } - emit(nodeIterator.next()) + emit(nodeIterator.next()) - val updates = changeConnectionEventFlow.map { nodeIterator.next() } - emitAll(updates) + val updates = changeConnectionEventFlow.mapNotNull { + if (nodeIterator.hasNext()) { + nodeIterator.next() + } else { + null + } } + emitAll(updates) } } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionSequenceStrategy.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionSequenceStrategy.kt deleted file mode 100644 index 1c5ff107b1..0000000000 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionSequenceStrategy.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy - -import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl - -interface NodeSelectionSequenceStrategy { - - fun generateNodeSequence(defaultNodes: List): Sequence -} - -fun NodeSelectionSequenceStrategy.generateNodeIterator(defaultNodes: List): Iterator { - return generateNodeSequence(defaultNodes).iterator() -} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionStrategyProvider.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionStrategyProvider.kt index 5fa99e5538..b4c72e34ca 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionStrategyProvider.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSelectionStrategyProvider.kt @@ -1,30 +1,67 @@ package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy -import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.runtime.ext.httpNodes +import io.novafoundation.nova.runtime.ext.wssNodes +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy -import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -import kotlinx.coroutines.flow.Flow +import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets +import io.novafoundation.nova.runtime.multiNetwork.connection.saturateNodeUrl +import io.novafoundation.nova.runtime.multiNetwork.connection.saturateNodeUrls -class NodeSelectionStrategyProvider { +class NodeSelectionStrategyProvider( + private val connectionSecrets: ConnectionSecrets, +) { - private val roundRobin = RoundRobinStrategy() - private val uniform = UniformStrategy() + fun createWss(config: Chain.Nodes): NodeSequenceGenerator { + return createNodeSequenceGenerator( + availableNodes = config.wssNodes(), + autobalanceStrategy = config.autoBalanceStrategy, + nodeSelectionStrategy = config.wssNodeSelectionStrategy + ) + } - fun strategyFlowFor(chainId: ChainId, default: NodeSelectionStrategy): Flow { - return flowOf { strategyFor(default) } + fun createHttp(config: Chain.Nodes): NodeSequenceGenerator { + return createNodeSequenceGenerator( + availableNodes = config.httpNodes(), + autobalanceStrategy = config.autoBalanceStrategy, + // Http nodes disregard selected wss strategy and always use auto balance + nodeSelectionStrategy = NodeSelectionStrategy.AutoBalance + ) } - fun strategyFor(config: NodeSelectionStrategy): NodeSelectionSequenceStrategy { - return when (config) { - is NodeSelectionStrategy.AutoBalance -> autobalanceStrategyFor(config) - is NodeSelectionStrategy.SelectedNode -> SelectedNodeStrategy(config.nodeUrl, autobalanceStrategyFor(config.autoBalanceStrategy)) + private fun createNodeSequenceGenerator( + availableNodes: List, + autobalanceStrategy: Chain.Nodes.AutoBalanceStrategy, + nodeSelectionStrategy: NodeSelectionStrategy, + ): NodeSequenceGenerator { + return when (nodeSelectionStrategy) { + NodeSelectionStrategy.AutoBalance -> createAutoBalanceGenerator(autobalanceStrategy, availableNodes) + is NodeSelectionStrategy.SelectedNode -> { + createSelectedNodeGenerator(nodeSelectionStrategy.unformattedNodeUrl, availableNodes) + // Fallback to auto balance in case we failed to setup a selected node strategy + ?: createAutoBalanceGenerator(autobalanceStrategy, availableNodes) + } } } - private fun autobalanceStrategyFor(config: NodeSelectionStrategy.AutoBalance): NodeSelectionSequenceStrategy { - return when (config) { - NodeSelectionStrategy.AutoBalance.ROUND_ROBIN -> roundRobin - NodeSelectionStrategy.AutoBalance.UNIFORM -> uniform + private fun createSelectedNodeGenerator( + selectedUnformattedNodeUrl: String, + availableNodes: List, + ): SelectedNodeGenerator? { + val node = availableNodes.find { it.unformattedUrl == selectedUnformattedNodeUrl } ?: return null + val saturatedNode = node.saturateNodeUrl(connectionSecrets) ?: return null + return SelectedNodeGenerator(saturatedNode) + } + + private fun createAutoBalanceGenerator( + autoBalanceStrategy: Chain.Nodes.AutoBalanceStrategy, + availableNodes: List, + ): NodeSequenceGenerator { + val saturatedUrls = availableNodes.saturateNodeUrls(connectionSecrets) + + return when (autoBalanceStrategy) { + Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN -> RoundRobinGenerator(saturatedUrls) + Chain.Nodes.AutoBalanceStrategy.UNIFORM -> UniformGenerator(saturatedUrls) } } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSequenceGenerator.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSequenceGenerator.kt new file mode 100644 index 0000000000..6c1a5ce30b --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/NodeSequenceGenerator.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy + +import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl + +interface NodeSequenceGenerator { + + fun generateNodeSequence(): Sequence +} + +fun NodeSequenceGenerator.generateNodeIterator(): Iterator { + return generateNodeSequence().iterator() +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategy.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinGenerator.kt similarity index 50% rename from runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategy.kt rename to runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinGenerator.kt index a5379f22c3..f36521febd 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategy.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinGenerator.kt @@ -3,9 +3,11 @@ package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strat import io.novafoundation.nova.common.utils.cycle import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl -class RoundRobinStrategy : NodeSelectionSequenceStrategy { +class RoundRobinGenerator( + private val availableNodes: List, +) : NodeSequenceGenerator { - override fun generateNodeSequence(defaultNodes: List): Sequence { - return defaultNodes.cycle() + override fun generateNodeSequence(): Sequence { + return availableNodes.cycle() } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/UniformStrategy.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/SelectedNodeGenerator.kt similarity index 50% rename from runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/UniformStrategy.kt rename to runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/SelectedNodeGenerator.kt index 8f92592096..959d2139ba 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/UniformStrategy.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/SelectedNodeGenerator.kt @@ -3,9 +3,11 @@ package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strat import io.novafoundation.nova.common.utils.cycle import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl -class UniformStrategy : NodeSelectionSequenceStrategy { +class SelectedNodeGenerator( + private val selectedNode: NodeWithSaturatedUrl, +) : NodeSequenceGenerator { - override fun generateNodeSequence(defaultNodes: List): Sequence { - return defaultNodes.shuffled().cycle() + override fun generateNodeSequence(): Sequence { + return listOf(selectedNode).cycle() } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/SelectedNodeStrategy.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/SelectedNodeStrategy.kt deleted file mode 100644 index c322fb8473..0000000000 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/SelectedNodeStrategy.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy - -import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl - -class SelectedNodeStrategy( - private val selectedUrl: String?, - private val fallbackStrategy: NodeSelectionSequenceStrategy -) : NodeSelectionSequenceStrategy { - - override fun generateNodeSequence(defaultNodes: List): Sequence { - if (selectedUrl == null) { - return fallbackStrategy.generateNodeSequence(defaultNodes) - } - - val selectedNode = defaultNodes.find { it.node.unformattedUrl == selectedUrl } - - return if (selectedNode == null) { - fallbackStrategy.generateNodeSequence(defaultNodes) - } else { - sequenceOf(selectedNode) - } - } -} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/UniformGenerator.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/UniformGenerator.kt new file mode 100644 index 0000000000..0fdad74534 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/UniformGenerator.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy + +import io.novafoundation.nova.common.utils.cycle +import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl + +class UniformGenerator( + private val availabelNodes: List, +) : NodeSequenceGenerator { + + override fun generateNodeSequence(): Sequence { + return availabelNodes.shuffled().cycle() + } +} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancerTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancerTest.kt deleted file mode 100644 index 1bc599fe7d..0000000000 --- a/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/NodeAutobalancerTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance - -import io.novafoundation.nova.common.utils.second -import io.novafoundation.nova.common.utils.singleReplaySharedFlow -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets -import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider -import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.RoundRobinStrategy -import io.novafoundation.nova.test_shared.CoroutineTest -import io.novafoundation.nova.test_shared.any -import io.novafoundation.nova.test_shared.whenever -import io.novasama.substrate_sdk_android.wsrpc.state.SocketStateMachine -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnitRunner - -@RunWith(MockitoJUnitRunner::class) -@Ignore("TODO: fix test") -// TODO valentun: New coroutine test API had some changes which broke those tests. The problem is caused by the implementation of the -// balancingNodeFlow, which use SharedFlow + emitting coroutine, which is not a quite good way to do so. I figured a way to rewrite it in cold way using runningReduce -// but I do not want to make such complex changes right before release. Gonna fix after 3.7.0 -class NodeAutobalancerTest : CoroutineTest() { - - @Mock - lateinit var strategyProvider: NodeSelectionStrategyProvider - - lateinit var autobalancer: NodeAutobalancer - - private val nodes = generateNodes() - private val nodeSelectionStrategy = Chain.Nodes.NodeSelectionStrategy.AutoBalance.ROUND_ROBIN - - private val nodesFlow = MutableStateFlow(Chain.Nodes(nodeSelectionStrategy, nodes)) - private val stateFlow = singleReplaySharedFlow() - private val connectionSecrets = ConnectionSecrets(emptyMap()) - - @Before - fun setup() { - autobalancer = NodeAutobalancer(strategyProvider, connectionSecrets) - whenever(strategyProvider.strategyFlowFor(any(), nodeSelectionStrategy)) - .thenReturn(flowOf(RoundRobinStrategy())) - } - - @Test - fun shouldSelectInitialNode() = runCoroutineTest { - val nodeFlow = nodeFlow() - - val initial = nodeFlow.first() - - assertEquals(nodes.first(), initial) - } - - @Test - fun shouldSelectNodeOnReconnectState() = runCoroutineTest { - val nodeFlow = nodeFlow() - stateFlow.emit(Unit) - - assertEquals(nodes.second(), nodeFlow.first()) - } - - @Test - fun shouldNotAutobalanceIfNotEnoughAttempts() = runCoroutineTest { - val nodeFlow = nodeFlow() - stateFlow.emit(Unit) - - assertEquals(nodes.first(), nodeFlow.first()) - } - - private fun generateNodes() = (1..10).map { - Chain.Node(unformattedUrl = it.toString(), name = it.toString(), chainId = "test", orderId = 0, isCustom = false) - } - - private fun nodeFlow() = autobalancer.connectionUrlFlow( - chainId = "test", - changeConnectionEventFlow = stateFlow, - availableNodesFlow = nodesFlow - ) - - private fun triggerState(attempt: Int) = SocketStateMachine.State.WaitingForReconnect( - url = "test", - attempt = attempt, - pendingSendables = emptySet() - ) -} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategyTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategyTest.kt index 4a211bda77..3c21fcfe1a 100644 --- a/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategyTest.kt +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/autobalance/strategy/RoundRobinStrategyTest.kt @@ -7,7 +7,6 @@ import org.junit.Test class RoundRobinStrategyTest { - private val strategy = RoundRobinStrategy() private val nodes = listOf( createFakeNode("1"), @@ -15,9 +14,11 @@ class RoundRobinStrategyTest { createFakeNode("3") ) + private val strategy = RoundRobinGenerator(nodes) + @Test fun `collections should have the same sequence`() { - val iterator = strategy.generateNodeSequence(nodes) + val iterator = strategy.generateNodeSequence() .iterator() nodes.forEach { assertEquals(it, iterator.next()) } @@ -25,7 +26,7 @@ class RoundRobinStrategyTest { @Test fun `sequence should be looped`() { - val iterator = strategy.generateNodeSequence(nodes) + val iterator = strategy.generateNodeSequence() .iterator() repeat(nodes.size) { iterator.next() }