From f8bf93dad11c52052a7336896a99beacdfdfb233 Mon Sep 17 00:00:00 2001 From: valentunn <70131744+valentunn@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:12:26 +0300 Subject: [PATCH] Rc/7.7.0 (#1331) * Refactor local account directory. Add proxy account in db * Add migration for proxy accounts * Minor changes * Proxies syncing in progress * Implemented proxies sync * Run ktlint * Update RealProxySyncService.kt * Minor changes * Minor changes * Clean code * Select wallet screen + logic * Fixed migration * Run ktlint * Delegated accounts updates * Update MetaAccountWithBalanceListingMixin.kt * Update MetaAccountGroupingInteractorImpl.kt * Update MetaAccount.kt * Added sort for delegated accounts * Increase susbtrate library version * Fixed pr notes * Fixed pr notes * Added run catching * Add failure handling * Add run catching * Proxied signer implementation + fee signer * Run ktlint * Clean code * Update AccountFeatureModule.kt * Fixed bugs * Fixed pr notes * Fixed pr notes * Update ProxiedSigner.kt * Fixed dependencies * Save proxieds in transaction * Filter delayed proxies * Feature/proxied signer (#1270) * Proxied signer implementation + fee signer * Run ktlint * Clean code * Update AccountFeatureModule.kt * Fixed bugs * Fixed pr notes * Update ProxiedSigner.kt * Fixed dependencies * Fix merge conflicts * Fixed delegated updates item subtitle * Fix signing * Fix - exotic nonce structures * Code style * Fix/staking paged exposures (#1267) * Paged exposures * Payouts * Fixes * Fixes * Update tags * Code style * Remove unused class * Remove unused logs * Fix tests * Update NftUniquesIntegrationTest.kt (#1278) * Wallet details refactoring and Proxied wallet details implementation * Clean code * Fixed wildcard import * Realtime proxy updates * Fixes * Code style * Fixes * Implemented proxy fee validation * Code style * Refactor extrinsic service * Fix - send title is not visible until xcm config is synced (#1281) * Code style * Fixes * Fixed proxied deactivated state * Removed Parity sugner wallet details mixin * Fixed imports * Update ProxiedWalletDetailsMixin.kt * Add proxy fee validation * Update MetaAccountDao.kt * Run ktlint * Remove dependency in account build module * Disable removing proxieds * Fixed pr notes * Update ProxyHaveEnoughFeeValidation.kt * Update ProxyExtrinsicValidationRequestBusHandler.kt * Update ProxyExtrinsicValidationRequestBusHandler.kt * Add dwellir secret (#1295) * Remove redundant code * Fix/sync added accounts (#1291) * Refactoring adding meta accounts * Fix DI * Refactor proxySyncService * Removing deactivated proxieds * Run ktlint * Clean code * Merge conflicts --------- Co-authored-by: Valentun * Proxy/fee validation (#1294) * Add support for non-requested account fee payment in validations * Code style * Remove debug logs * Improve comments * Fix conflicts * per chain proxy sync (#1303) * Fix/setup staking state transitions (#1302) * Fix bugs in start staking state transitions * Code style * Fix - crash for proxied receive (#1304) * Fix - proxy icon opacity (#1305) * Fixed proxy types matching * Fixed pr notes * Selecting first meta account in case when selected proxied is removed due to deactivation * Use on chain balance for proxy fee validation (#1310) * Use on chain balance for proxy fee validation * Rename variables * Proxy bugs and improvements Update proxy wiki link Replace text in Settings account title Update proxy type title formatting Fixed bug when after revoke access no wallet is selected in bottom-sheet * Update ProxyFormatter.kt * Update ProxyFormatter.kt * Improve account switching logic * Fix/proxy ledger (#1313) * Fix proxy signing with ledger * Code style * Do not sync WO proxies in production (#1315) * Localization (#1316) * Localization * Fixes * Fix - robonomics demoracy unlock call encoding (#1317) * Revisit usages of all accounts (#1318) * Fixes (#1324) * Fix - first proxy sync after update does not detect proxy chains in config * Fix - not enough permissions with no proxies case * Non proxy fixes (#1325) * Update github update link * Fix crash when failed to retrieve metadata * Fix tests * Fix/staking reactive updates for relaychain staking (#1320) * Fix reactive updates for relaychain unbondings * Fix reactive updates for relaychain stake summary * Fix/tests (#1319) * Fix integration tests * Increase timeout back * Use wrapped payload for fee validation (#1327) * Fixed res names and values: stacking to staking and unstacking to unstaking (#1328) * Apply localization changes to localize * Bump version --------- Co-authored-by: antonijzelinskij Co-authored-by: antonijzelinskij <107959809+antonijzelinskij@users.noreply.github.com> --- .github/workflows/android_build.yml | 2 +- app/build.gradle | 2 +- .../nova/NftUniquesIntegrationTest.kt | 101 ++-- .../nova/balances/BalancesIntegrationTest.kt | 24 +- .../nova/app/root/di/RootDependencies.kt | 9 + .../nova/app/root/di/RootFeatureModule.kt | 10 +- .../di/busHandler/RequestBusHandlerModule.kt | 42 ++ .../nova/app/root/domain/RootInteractor.kt | 11 +- .../nova/app/root/navigation/Navigator.kt | 12 +- .../governance/GovernanceNavigator.kt | 4 +- .../navigation/settings/SettingsNavigator.kt | 4 +- .../staking/StartMultiStakingNavigator.kt | 2 +- .../parachain/ParachainStakingNavigator.kt | 2 +- .../relaychain/RelayStakingNavigator.kt | 2 +- .../app/root/presentation/RootViewModel.kt | 19 +- .../handlers/ImportMnemonicDeepLinkHandler.kt | 6 +- .../presentation/di/RootActivityModule.kt | 7 +- .../CompoundRequestBusHandler.kt | 9 + ...oxyExtrinsicValidationRequestBusHandler.kt | 55 ++ .../requestBusHandler/RequestBusHandler.kt | 5 + .../main/res/navigation/main_nav_graph.xml | 30 +- build.gradle | 6 +- common/build.gradle | 1 + .../common/address/AddressIconGenerator.kt | 4 +- .../common/data/network/AppLinksProvider.kt | 1 + .../network/runtime/binding/AccountInfo.kt | 24 +- .../network/runtime/binding/GenericCall.kt | 7 + .../network/runtime/calls/GetStorageSize.kt | 8 + .../subquery/EraValidatorInfoQueryResponse.kt | 20 +- .../nova/common/di/modules/NetworkModule.kt | 1 + .../nova/common/utils/AlphaColorFilter.kt | 12 + .../nova/common/utils/AlphaDrawable.kt | 55 ++ .../nova/common/utils/FearlessLibExt.kt | 42 ++ .../nova/common/utils/KotlinExt.kt | 43 ++ .../nova/common/utils/SpannableExt.kt | 24 +- .../nova/common/utils/bus/BaseRequestBus.kt | 24 + .../nova/common/utils/bus/RequestBus.kt | 26 + .../nova/common/view/AlertView.kt | 22 +- .../nova/common/view/ChipLabelView.kt | 2 +- .../ActionNotAllowedBottomSheet.kt | 12 +- .../src/main/res/drawable/bg_chain_wallet.xml | 8 + common/src/main/res/drawable/ic_proxy.xml | 10 + .../shape_account_updated_indicator.xml | 11 + .../res/drawable/shape_updates_indicator.xml | 7 + .../bottom_sheet_action_not_allowed.xml | 3 +- common/src/main/res/layout/layout_puller.xml | 6 + common/src/main/res/layout/view_alert.xml | 27 +- common/src/main/res/values-ru/strings.xml | 31 +- common/src/main/res/values/strings.xml | 44 +- .../nova/common/utils/PartitionTest.kt | 27 + .../nova/core_db/dao/Helpers.kt | 4 +- .../migrations/BetterChainDiffingTest_8_9.kt | 2 +- .../nova/core_db/AppDatabase.kt | 13 +- .../converters/MetaAccountTypeConverters.kt | 2 +- .../converters/ProxyAccountConverters.kt | 16 + .../nova/core_db/dao/MetaAccountDao.kt | 84 ++- .../migrations/14_15_AddMetaAccountType.kt | 2 +- .../migrations/53_54_AddProxyAccount.kt | 27 + .../nova/core_db/model/BalanceLockLocal.kt | 2 +- .../core_db/model/ExternalBalanceLocal.kt | 2 +- .../model/StakingDashboardItemLocal.kt | 2 +- .../model/WalletConnectPairingLocal.kt | 2 +- .../nova/core_db/model/chain/ChainLocal.kt | 2 + .../model/chain/account/ChainAccountLocal.kt | 34 ++ .../chain/account/JoinedMetaAccountInfo.kt | 24 + .../chain/{ => account}/MetaAccountLocal.kt | 58 +- .../model/chain/account/ProxyAccountLocal.kt | 48 ++ .../transaction/EvmTransactionService.kt | 3 +- .../ethereum/transaction/TransactionOrigin.kt | 11 + .../data/extrinsic/ExtrinsicService.kt | 80 +-- .../data/extrinsic/ExtrinsicServiceExt.kt | 11 +- .../feature_account_api/data/model/Fee.kt | 28 +- .../data/model/ProxiedWithProxy.kt | 20 + .../data/proxy/MetaAccountsUpdatesRegistry.kt | 20 + .../data/proxy/ProxySyncService.kt | 12 + .../ProxiedExtrinsicValidationSystem.kt | 31 + .../ProxyExtrinsicValidationRequestBus.kt | 14 + .../data/repository/ProxyRepository.kt | 14 + .../addAccount/AddAccountRepository.kt | 6 + .../addAccount/BaseAddAccountRepository.kt | 16 + .../ledger/LedgerAddAccountRepository.kt | 24 + .../data/signer/SeparateFlowSignerState.kt | 9 + .../data/signer/SignerProvider.kt | 9 +- .../di/AccountFeatureApi.kt | 16 +- .../domain/interfaces/AccountInteractor.kt | 6 +- .../domain/interfaces/AccountRepository.kt | 20 +- .../domain/interfaces/AccountRepositoryExt.kt | 13 +- .../MetaAccountGroupingInteractor.kt | 3 + .../interfaces/SelectedAccountUseCase.kt | 18 +- .../domain/model/MetaAccount.kt | 28 +- .../domain/model/MetaAccountAssetBalance.kt | 4 + .../domain/model/MetaAccountId.kt | 5 + .../model/ProxiedAndProxyMetaAccount.kt | 9 + .../domain/model/ProxyAccount.kt | 32 ++ .../account/AddressDisplayUseCase.kt | 13 +- .../account/icon/AddressIconGeneratorExt.kt | 2 +- .../account/listing/AccountListAdapter.kt | 218 +------ .../account/listing/AccountUi.kt | 13 - .../account/listing/CommonAccountsAdapter.kt | 100 ++++ .../listing/MetaAccountPayloadGenerator.kt | 10 + .../listing/holders/AccountChipHolder.kt | 25 + .../account/listing/holders/AccountHolder.kt | 134 +++++ .../listing/holders/AccountTitleHolder.kt | 13 + .../listing/items/AccountChipGroupRvItem.kt | 12 + .../listing/items/AccountTitleGroupRvItem.kt | 11 + .../account/listing/items/AccountUi.kt | 17 + .../account/proxy/ProxySigningPresenter.kt | 18 + .../account/wallet/WalletUiUseCase.kt | 3 +- .../view/SelectedWalletView.kt | 10 +- .../src/main/res/layout/item_account.xml | 21 +- .../layout/item_delegated_account_group.xml | 10 + .../main/res/layout/view_selected_wallet.xml | 19 +- feature-account-impl/build.gradle | 1 + .../transaction/RealEvmTransactionService.kt | 39 +- .../data/extrinsic/RealExtrinsicService.kt | 185 +++--- .../data/mappers/Mappers.kt | 75 ++- .../proxy/RealMetaAccountsUpdatesRegistry.kt | 63 ++ .../data/proxy/RealProxySyncService.kt | 178 ++++++ .../data/repository/AccountRepositoryImpl.kt | 53 +- .../data/repository/AddAccountRepository.kt | 146 ----- .../data/repository/ParitySignerRepository.kt | 49 -- .../data/repository/RealProxyRepository.kt | 133 +++++ .../data/repository/WatchOnlyRepository.kt | 52 -- .../ledger/RealLedgerAddAccountRepository.kt | 83 +++ .../ParitySignerAddAccountRepository.kt | 49 ++ .../proxied/ProxiedAddAccountRepository.kt | 87 +++ .../secrets/JsonAddAccountRepository.kt | 54 ++ .../secrets/MnemonicAddAccountRepository.kt | 38 ++ .../secrets/SecretsAddAccountRepository.kt | 111 ++++ .../secrets/SeedAddAccountRepository.kt | 38 ++ .../WatchOnlyAddAccountRepository.kt | 68 +++ .../datasource/AccountDataSource.kt | 20 +- .../datasource/AccountDataSourceImpl.kt | 43 +- .../data/signer/DefaultFeeSigner.kt | 41 +- .../data/signer/LeafSigner.kt | 16 + .../data/signer/RealSignerProvider.kt | 55 +- .../data/signer/SeparateFlowSigner.kt | 23 +- .../data/signer/ledger/LedgerSigner.kt | 31 +- .../paritySigner/PolkadotVaultSigner.kt | 56 +- .../data/signer/proxy/ProxiedFeeSigner.kt | 99 ++++ .../data/signer/proxy/ProxiedSigner.kt | 179 ++++++ .../signer/proxy/ProxyCallFilterFactory.kt | 95 +++ .../signer/proxy/SignerPayloadModifierExt.kt | 51 ++ .../proxy/callFilter/AnyOfCallFilter.kt | 14 + .../signer/proxy/callFilter/CallFilter.kt | 7 + .../proxy/callFilter/EverythingFilter.kt | 10 + .../proxy/callFilter/WhiteListFilter.kt | 20 + .../data/signer/secrets/SecretsSigner.kt | 20 +- .../data/signer/watchOnly/WatchOnlySigner.kt | 24 +- .../di/AccountFeatureComponent.kt | 3 + .../di/AccountFeatureDependencies.kt | 3 + .../di/AccountFeatureModule.kt | 141 ++++- .../di/AddAccountsModule.kt | 114 ++++ .../di/modules/ParitySignerModule.kt | 26 +- .../di/modules/ProxySigningModule.kt | 24 + .../di/modules/signers/ProxiedSignerModule.kt | 45 ++ .../di/modules/{ => signers}/SignersModule.kt | 71 ++- .../domain/AccountInteractorImpl.kt | 28 +- .../MetaAccountGroupingInteractorImpl.kt | 83 ++- .../account/add/AddAccountInteractor.kt | 50 +- ...teractor.kt => WalletDetailsInteractor.kt} | 75 +-- .../account/identity/LocalIdentityProvider.kt | 2 +- .../FinishImportParitySignerInteractor.kt | 12 +- .../change/ChangeWatchAccountInteractor.kt | 16 +- .../create/CreateWatchWalletInteractor.kt | 10 +- .../presentation/AccountRouter.kt | 4 +- ...DelegatedMetaAccountUpdatesListingMixin.kt | 104 ++++ .../MetaAccountTypePresentationMapper.kt | 14 +- ...aAccountValidForTransactionListingMixin.kt | 7 +- .../MetaAccountWithBalanceListingMixin.kt | 70 ++- .../account/common/listing/ProxyFormatter.kt | 63 ++ .../details/AccountDetailsViewModel.kt | 237 -------- ...lsFragment.kt => WalletDetailsFragment.kt} | 17 +- .../account/details/WalletDetailsViewModel.kt | 116 ++++ .../details/di/AccountDetailsComponent.kt | 4 +- .../details/di/AccountDetailsModule.kt | 50 +- .../details/mixin/LedgerWalletDetailsMixin.kt | 69 +++ .../mixin/PolkadotVaultWalletDetailsMixin.kt | 59 ++ .../mixin/ProxiedWalletDetailsMixin.kt | 73 +++ .../mixin/SecretsWalletDetailsMixin.kt | 50 ++ .../details/mixin/WalletDetailsMixin.kt | 38 ++ .../mixin/WalletDetailsMixinFactory.kt | 39 ++ .../mixin/WatchOnlyWalletDetailsMixin.kt | 60 ++ .../details/mixin/common/AccountFormatter.kt | 66 +++ .../details/mixin/common/WalletMixinCommon.kt | 58 ++ .../account/details/model/AccountTypeAlert.kt | 3 +- .../account/list/WalletListFragment.kt | 12 +- .../account/list/WalletListViewModel.kt | 4 +- .../DelegatedAccountUpdatesBottomSheet.kt | 54 ++ .../DelegatedAccountUpdatesViewModel.kt | 40 ++ .../DelegatedAccountsAdapter.kt | 37 ++ .../di/DelegatedAccountUpdatesComponent.kt | 26 + .../di/DelegatedAccountUpdatesModule.kt | 44 ++ .../selectAddress/SelectAddressViewModel.kt | 6 +- .../list/switching/SwitchWalletFragment.kt | 5 + .../list/switching/SwitchWalletViewModel.kt | 24 +- .../list/switching/di/SwitchWalletModule.kt | 6 + .../management/WalletManagmentFragment.kt | 17 +- .../management/WalletManagmentViewModel.kt | 14 +- .../management/di/WalletManagmentModule.kt | 8 +- .../account/wallet/WalletUiUseCaseImpl.kt | 14 +- .../AddAccountLauncherProvider.kt | 4 +- ...knowledgeSigningNotSupportedBottomSheet.kt | 4 +- .../selectWallet/SelectWalletViewModel.kt | 6 +- .../di/FinishImportParitySignerModule.kt | 6 +- .../scan/ScanSignParitySignerViewModel.kt | 7 +- .../scan/di/ScanSignParitySignerModule.kt | 5 +- .../show/ShowSignParitySignerViewModel.kt | 12 +- .../show/di/ShowSignParitySignerModule.kt | 4 +- ...ProxySignNotEnoughPermissionBottomSheet.kt | 23 + .../proxy/sign/ProxySignWarningBottomSheet.kt | 44 ++ .../proxy/sign/RealProxySigningPresenter.kt | 143 +++++ .../change/di/ChangeWatchAccountModule.kt | 8 +- .../create/di/CreateWatchWalletModule.kt | 4 +- .../sign/WatchOnlySignBottomSheet.kt | 4 +- ...bottom_sheet_delegated_account_updates.xml | 58 ++ .../res/layout/bottom_sheet_proxy_warning.xml | 77 +++ ...etails.xml => fragment_wallet_details.xml} | 0 .../domain/send/SendInteractor.kt | 24 +- .../LedgerNotSupportedWarningBottomSheet.kt | 4 +- .../presentation/send/TransferDraft.kt | 2 +- .../send/amount/SelectSendViewModel.kt | 8 +- .../send/confirm/ConfirmSendViewModel.kt | 15 +- .../ContributeValidationsModule.kt | 2 +- .../MoonbeamTermsValidationsModule.kt | 2 +- .../CrowdloanContributeInteractor.kt | 14 +- .../custom/acala/AcalaContributeInteractor.kt | 2 +- .../moonbeam/MoonbeamCrowdloanInteractor.kt | 12 +- .../ContributeValidationPayload.kt | 3 +- .../custom/moonbeam/MoonbeamTermsPayload.kt | 4 +- .../presentation/CrowdloanRouter.kt | 2 +- .../confirm/ConfirmContributeViewModel.kt | 7 +- .../parcel/ConfirmContributePayload.kt | 3 +- .../terms/MoonbeamCrowdloanTermsViewModel.kt | 18 +- .../select/CrowdloanContributeViewModel.kt | 74 ++- .../presentation/main/CrowdloanViewModel.kt | 2 +- .../sheets/AcknowledgePhishingBottomSheet.kt | 4 +- .../data/evmApi/EvmApi.kt | 10 +- .../di/ExternalSignFeatureDependencies.kt | 3 + .../domain/sign/BaseExternalSignInteractor.kt | 6 +- .../domain/sign/evm/EvmSignInteractor.kt | 19 +- .../PolkadotExternalSignInteractor.kt | 29 +- .../signExtrinsic/ExternaSignViewModel.kt | 4 +- .../NewDelegationChooseAmountInteractor.kt | 3 +- .../removeVotes/RemoveTrackVotesInteractor.kt | 4 +- .../vote/VoteReferendumInteractor.kt | 6 +- .../extrinsic/ExtrinsicBuilderExt.kt | 8 +- ...RealNewDelegationChooseAmountInteractor.kt | 19 +- ...ChooseDelegationAmountValidationPayload.kt | 3 +- .../ChooseDelegationAmountValidationSystem.kt | 8 +- .../RealRemoveTrackVotesInteractor.kt | 19 +- .../RemoveVotesValidationPayload.kt | 4 +- .../RemoveVotesValidationSystem.kt | 8 +- .../revoke/RevokeDelegationsInteractor.kt | 11 +- .../RevokeDelegationValidationPayload.kt | 4 +- .../RevokeDelegationValidationSystem.kt | 8 +- .../unlock/GovernanceUnlockInteractor.kt | 24 +- .../UnlockReferendumValidationPayload.kt | 4 +- .../VoteReferendumValidationSystem.kt | 8 +- .../vote/RealVoteReferendumInteractor.kt | 11 +- .../VoteReferendumValidationPayload.kt | 3 +- .../VoteReferendumValidationSystem.kt | 8 +- .../presentation/GovernanceRouter.kt | 2 +- .../detail/main/DelegateDetailsViewModel.kt | 2 +- .../NewDelegationChooseAmountViewModel.kt | 8 +- .../confirm/NewDelegationConfirmPayload.kt | 3 +- .../confirm/NewDelegationConfirmViewModel.kt | 7 +- .../removeVotes/RemoveVotesViewModel.kt | 29 +- .../RevokeDelegationConfirmViewModel.kt | 9 +- .../details/ReferendumDetailsViewModel.kt | 2 +- .../confirm/ConfirmReferendumVoteViewModel.kt | 7 +- .../confirm/ConfirmVoteReferendumPayload.kt | 3 +- .../setup/SetupVoteReferendumViewModel.kt | 7 +- .../ConfirmGovernanceUnlockViewModel.kt | 38 +- .../data/repository/LedgerDerivationPath.kt | 12 + .../data/repository/LedgerRepository.kt | 11 + .../data/repository/LedgerRepository.kt | 104 ---- .../data/repository/RealLedgerRepository.kt | 18 + .../di/LedgerFeatureDependencies.kt | 8 +- .../di/LedgerFeatureModule.kt | 8 +- .../AddLedgerChainAccountInteractor.kt | 12 +- .../finish/FinishImportLedgerInteractor.kt | 11 +- .../account/sign/SignLedgerInteractor.kt | 22 +- ...ddLedgerChainAccountSelectAddressModule.kt | 6 +- .../SelectAddressLedgerFragment.kt | 18 +- .../SelectAddressLedgerViewModel.kt | 7 +- .../finish/di/FinishImportLedgerModule.kt | 6 +- .../account/sign/SignLedgerViewModel.kt | 18 +- .../account/sign/di/SignLedgerModule.kt | 18 +- .../RealSubstrateLedgerApplication.kt | 2 +- .../providers/uniques/UniquesNftProvider.kt | 61 +- .../feature_settings_impl/SettingsRouter.kt | 2 +- .../settings/SettingsViewModel.kt | 2 +- .../domain/api/StakingRepository.kt | 2 +- .../domain/model/Exposure.kt | 9 + .../domain/model/relaychain/StakingState.kt | 5 + .../feature_staking_impl/data/model/Payout.kt | 6 +- .../blockhain/bindings/ClaimedRewardPages.kt | 15 + .../blockhain/bindings/EraRewardPoints.kt | 20 +- .../network/blockhain/bindings/Exposure.kt | 46 +- .../blockhain/bindings/StakingLedger.kt | 18 +- .../blockhain/bindings/ValidatorPrefs.kt | 14 - .../blockhain/calls/ExtrinsicBuilderExt.kt | 11 - .../data/network/blockhain/updaters/Common.kt | 21 +- .../updaters/ValidatorExposureUpdater.kt | 183 +++++- .../HistoricalTotalValidatorRewardUpdater.kt | 5 - .../historical/HistoricalUpdateMediator.kt | 45 +- .../HistoricalValidatorRewardPointsUpdater.kt | 5 - .../data/network/subquery/StakingApi.kt | 13 +- .../subquery/SubQueryValidatorSetFetcher.kt | 72 ++- ....kt => StakingNominatorEraInfosRequest.kt} | 6 +- .../StakingValidatorEraInfosRequest.kt | 25 + .../data/repository/PayoutRepository.kt | 544 +++++++++++++----- .../repository/StakingConstantsRepository.kt | 7 +- .../data/repository/StakingRepositoryImpl.kt | 81 ++- .../di/StakingFeatureDependencies.kt | 3 + .../di/StakingFeatureModule.kt | 19 +- .../RelaychainStakingUpdatersModule.kt | 13 +- .../MakePayoutValidationsModule.kt | 4 +- .../di/validations/RebondValidationsModule.kt | 2 +- .../di/validations/RedeemValidationsModule.kt | 2 +- .../RewardDestinationValidationsModule.kt | 2 +- .../SetControllerValidationsModule.kt | 2 +- .../di/validations/UnbondValidationsModule.kt | 2 +- .../domain/StakingInteractor.kt | 51 +- .../domain/StakingInteractorExt.kt | 7 +- .../domain/alerts/AlertsInteractor.kt | 2 +- .../domain/bagList/rebag/RebagInteractor.kt | 16 +- .../validations/RebagValidationPayload.kt | 4 +- .../validations/RebagValidationSystem.kt | 8 +- .../domain/common/StakingSharedComputation.kt | 2 - .../validation/ProfitableActionValidation.kt | 9 +- .../domain/model/PendingPayoutsStatistics.kt | 1 + .../NominationPoolsBondMoreInteractor.kt | 15 +- .../bondMore/validations/Declarations.kt | 2 +- .../NominationPoolsClaimRewardsInteractor.kt | 15 +- .../claimRewards/validations/Declarations.kt | 10 +- ...ationPoolsClaimRewardsValidationPayload.kt | 3 +- .../PoolAvailableBalanceValidation.kt | 9 +- .../redeem/NominationPoolsRedeemInteractor.kt | 11 +- .../redeem/validations/Declarations.kt | 8 +- .../NominationPoolsRedeemValidationPayload.kt | 4 +- .../unbond/NominationPoolsUnbondInteractor.kt | 15 +- .../unbond/validations/Declarations.kt | 10 +- .../NominationPoolsUnbondValidationPayload.kt | 3 +- .../ParachainStakingRebondInteractor.kt | 14 +- ...ParachainStakingRebondValidationPayload.kt | 4 +- .../ParachainStakingUnbondValidationSystem.kt | 2 +- .../ParachainStakingRedeemInteractor.kt | 18 +- ...ParachainStakingRedeemValidationPayload.kt | 4 +- .../ParachainStakingUnbondValidationSystem.kt | 2 +- .../start/StartParachainStakingInteractor.kt | 24 +- .../StartParachainStakingValidationPayload.kt | 3 +- .../StartParachainStakingValidationSystem.kt | 4 +- .../ParachainStakingUnbondInteractor.kt | 14 +- ...ParachainStakingUnbondValidationPayload.kt | 3 +- .../ParachainStakingUnbondValidationSystem.kt | 2 +- .../yieldBoost/YieldBoostInteractor.kt | 15 +- .../validations/FirstTaskCanExecute.kt | 6 +- .../validations/ValidationSystem.kt | 8 +- .../YieldBoostValidationPayload.kt | 4 +- .../domain/payout/PayoutInteractor.kt | 67 ++- .../RecommendationSettingsProvider.kt | 3 +- .../RecommendationSettingsProviderFactory.kt | 7 +- .../filters/NotOverSubscribedFilter.kt | 13 +- .../setup/ChangeValidatorsInteractor.kt | 17 +- .../domain/staking/bond/BondMoreInteractor.kt | 15 +- .../controller/ControllerInteractor.kt | 14 +- .../domain/staking/rebond/RebondInteractor.kt | 11 +- .../domain/staking/redeem/RedeemInteractor.kt | 8 +- .../ChangeRewardDestinationInteractor.kt | 12 +- .../common/StartMultiStakingInteractor.kt | 9 +- .../AvailableBalanceGapValidation.kt | 3 +- .../direct/DirectStakingProperties.kt | 10 +- .../pools/NominationPoolStakingProperties.kt | 2 +- .../ManualMultiStakingSelectionType.kt | 4 +- .../domain/staking/unbond/UnbondInteractor.kt | 43 +- .../validations/AccountRequiredValidation.kt | 2 +- .../bond/BondMoreValidationPayload.kt | 3 +- .../domain/validations/bond/Declarations.kt | 4 +- .../SetControllerValidationPayload.kt | 3 +- .../validations/payout/MakePayoutPayload.kt | 11 +- .../rebond/RebondValidationPayload.kt | 3 +- .../reedeem/RedeemValidationPayload.kt | 4 +- .../RewardDestinationValidationPayload.kt | 3 +- .../domain/validations/setup/Declarations.kt | 2 +- .../validations/setup/SetupStakingPayload.kt | 4 +- .../unbond/UnbondValidationPayload.kt | 3 +- .../domain/validators/ValidatorProvider.kt | 2 +- .../current/CurrentValidatorsInteractor.kt | 2 +- .../presentation/StakingRouter.kt | 2 +- .../bagList/rebag/RebagViewModel.kt | 9 +- .../common/SetupStakingSharedState.kt | 38 +- .../CurrentStakeTargetsFragment.kt | 2 +- .../presentation/mappers/Validator.kt | 4 +- ...NominationPoolsConfirmBondMoreViewModel.kt | 2 +- .../NominationPoolsClaimRewardsViewModel.kt | 5 +- .../redeem/NominationPoolsRedeemViewModel.kt | 7 +- .../NominationPoolsConfirmUnbondPayload.kt | 3 +- .../NominationPoolsConfirmUnbondViewModel.kt | 7 +- .../NominationPoolsSetupUnbondViewModel.kt | 6 +- .../rebond/ParachainStakingRebondViewModel.kt | 37 +- .../redeem/ParachainStakingRedeemViewModel.kt | 37 +- .../ConfirmStartParachainStakingViewModel.kt | 45 +- .../ConfirmStartParachainStakingPayload.kt | 3 +- .../setup/StartParachainStakingViewModel.kt | 52 +- .../ParachainStakingUnbondConfirmViewModel.kt | 7 +- .../ParachainStakingUnbondConfirmPayload.kt | 3 +- .../setup/ParachainStakingUnbondViewModel.kt | 48 +- .../confirm/YieldBoostConfirmViewModel.kt | 7 +- .../confirm/model/YieldBoostConfirmPayload.kt | 4 +- .../setup/SetupYieldBoostViewModel.kt | 48 +- .../payouts/confirm/ConfirmPayoutFragment.kt | 1 + .../payouts/confirm/ConfirmPayoutViewModel.kt | 73 +-- .../payouts/confirm/di/ConfirmPayoutModule.kt | 5 +- .../payouts/list/PayoutsListViewModel.kt | 13 +- .../payouts/model/PendingPayoutParcelable.kt | 15 + .../bond/confirm/ConfirmBondMorePayload.kt | 3 +- .../bond/confirm/ConfirmBondMoreViewModel.kt | 7 +- .../bond/select/SelectBondMoreViewModel.kt | 47 +- .../confirm/ConfirmSetControllerPayload.kt | 3 +- .../confirm/ConfirmSetControllerViewModel.kt | 7 +- .../controller/set/SetControllerViewModel.kt | 55 +- .../rebond/confirm/ConfirmRebondViewModel.kt | 36 +- .../rebond/custom/CustomRebondViewModel.kt | 36 +- .../staking/redeem/RedeemViewModel.kt | 39 +- .../ConfirmRewardDestinationViewModel.kt | 7 +- .../parcel/ConfirmRewardDestinationPayload.kt | 4 +- .../SelectRewardDestinationViewModel.kt | 56 +- .../confirm/ConfirmMultiStakingViewModel.kt | 2 +- .../types/DirectConfirmMultiStakingType.kt | 3 +- .../SetupAmountMultiStakingViewModel.kt | 4 +- .../SetupStakingTypeFlowExecutor.kt | 5 +- .../unbond/confirm/ConfirmUnbondPayload.kt | 3 +- .../unbond/confirm/ConfirmUnbondViewModel.kt | 7 +- .../unbond/select/SelectUnbondViewModel.kt | 51 +- .../ChangeStakingValidationFailureUI.kt | 4 +- .../validators/change/StateTransitions.kt | 46 +- .../ConfirmChangeValidatorsViewModel.kt | 50 +- .../ConfirmNominationsViewModel.kt | 2 +- .../review/ReviewCustomValidatorsViewModel.kt | 2 +- .../SetupStakingReviewValidatorsFlowAction.kt | 6 +- .../search/SearchCustomValidatorsViewModel.kt | 2 +- .../select/SelectCustomValidatorsViewModel.kt | 8 +- .../RecommendedValidatorsViewModel.kt | 13 +- .../start/StartChangeValidatorsViewModel.kt | 41 +- .../current/CurrentValidatorsViewModel.kt | 14 +- .../details/StakeTargetDetailsPayload.kt | 2 +- .../details/ValidatorDetailsFragment.kt | 2 +- .../domain/model/SwapQuote.kt | 3 +- .../AssetConversionExchange.kt | 23 +- .../SwapTransactionHistoryRepository.kt | 3 +- .../feature_swap_impl/di/SwapFeatureModule.kt | 9 +- .../domain/interactor/SwapInteractor.kt | 14 +- .../domain/swap/RealSwapService.kt | 10 +- ...onsideringNonSufficientAssetsValidation.kt | 3 +- .../validation/SwapPayloadValidation.kt | 7 +- .../domain/validation/SwapValidations.kt | 17 +- ...tBalanceToPayFeeConsideringEDValidation.kt | 3 +- .../SwapFeeSufficientBalanceValidation.kt | 8 +- .../SwapSmallRemainingBalanceValidation.kt | 6 +- .../confirmation/SwapConfirmationFragment.kt | 4 +- .../confirmation/SwapConfirmationViewModel.kt | 6 +- .../payload/SwapConfirmationPayload.kt | 5 +- .../SwapConfirmationPayloadFormatter.kt | 15 +- .../main/SwapMainSettingsViewModel.kt | 5 +- .../main/SwapValidationFailureUi.kt | 21 +- .../options/SwapOptionsFragment.kt | 4 +- .../data/cache/AssetCache.kt | 2 +- .../feature_wallet_api/data/mappers/Fee.kt | 14 - .../blockhain/assets/balances/AssetBalance.kt | 7 + .../SubstrateRealtimeOperationFetcher.kt | 4 +- .../tranfers/AssetTransferValidations.kt | 9 +- .../assets/tranfers/AssetTransfers.kt | 5 +- .../crosschain/CrossChainTransactor.kt | 3 +- .../feature_wallet_api/di/WalletFeatureApi.kt | 3 + .../EnoughAmountToTransferValidation.kt | 44 +- .../EnoughBalanceToStayAboveEDValidation.kt | 9 +- .../ExistentialDepositValidation.kt | 7 +- .../domain/validation/FeeChangeValidation.kt | 29 +- .../HasEnoughFreeBalanceValidation.kt | 7 +- .../domain/validation/Producers.kt | 7 +- .../ProxyHaveEnoughFeeValidation.kt | 101 ++++ .../mixin/amountChooser/AmountChooserMixin.kt | 3 +- .../presentation/mixin/fee/FeeLoaderMixin.kt | 111 +--- .../presentation/mixin/fee/FeeParcelModel.kt | 43 +- .../mixin/fee/GenericFeeLoaderProvider.kt | 46 +- .../presentation/model/FeeModel.kt | 17 +- .../RealWalletConnectSessionInteractor.kt | 2 +- .../WalletConnectApproveSessionFragment.kt | 6 +- .../balances/UnsupportedAssetBalance.kt | 2 + .../equilibrium/EquilibriumAssetBalance.kt | 17 +- .../balances/evmErc20/EvmErc20AssetBalance.kt | 15 +- .../evmNative/EvmNativeAssetBalance.kt | 14 +- .../assets/balances/orml/OrmlAssetBalance.kt | 21 +- .../{OrmlAssets.kt => OrmlBalanceBinding.kt} | 24 +- .../statemine/StatemineAssetBalance.kt | 19 +- .../balances/utility/NativeAssetBalance.kt | 9 +- .../equilibrium/EquilibriumAssetHistory.kt | 8 +- .../assets/history/orml/OrmlAssetHistory.kt | 8 +- .../substrate/AssetConversionSwapExtractor.kt | 50 +- .../history/realtime/substrate/Common.kt | 10 +- .../RealSubstrateRealtimeOperationFetcher.kt | 39 +- .../statemine/StatemineAssetHistory.kt | 8 +- .../history/utility/NativeAssetHistory.kt | 8 +- .../assets/transfers/BaseAssetTransfers.kt | 14 +- .../transfers/UnsupportedAssetTransfers.kt | 3 +- .../evmErc20/EvmErc20AssetTransfers.kt | 5 +- .../evmNative/EvmNativeAssetTransfers.kt | 5 +- .../assets/transfers/validations/Common.kt | 15 +- .../crosschain/RealCrossChainTransactor.kt | 12 +- .../crosschain/RealCrossChainWeigher.kt | 3 +- .../validations/CrossChainFeeValidation.kt | 7 +- .../data/repository/WalletRepositoryImpl.kt | 4 +- .../di/WalletFeatureDependencies.kt | 3 + .../di/WalletFeatureModule.kt | 19 +- .../nova/runtime/di/RuntimeApi.kt | 3 + .../nova/runtime/di/RuntimeModule.kt | 8 + .../nova/runtime/ext/ChainExt.kt | 3 +- .../extrinsic/ExtrinsicBuilderFactory.kt | 18 +- .../extrinsic/multi/ExtrinsicSplitter.kt | 21 +- .../runtime/extrinsic/signer/NovaSigner.kt | 26 + .../extrinsic/visitor/api/ExtrinsicWalk.kt | 50 ++ .../extrinsic/visitor/api/ExtrinsicWalkExt.kt | 12 + .../visitor/impl/ExtrinsicVisitorLogger.kt | 26 + .../visitor/impl/MutableEventQueue.kt | 131 +++++ .../extrinsic/visitor/impl/NestedCallNode.kt | 56 ++ .../visitor/impl/RealExtrinsicWalk.kt | 136 +++++ .../visitor/impl/nodes/proxy/ProxyNode.kt | 103 ++++ .../chain/mappers/ChainMappers.kt | 1 + .../mappers/RemoteToLocalChainMappers.kt | 2 + .../runtime/multiNetwork/chain/model/Chain.kt | 1 + .../runtime/RuntimeSyncService.kt | 25 +- .../runtime/repository/ExtrinsicWithEvents.kt | 38 +- .../nova/runtime/network/rpc/RpcCalls.kt | 7 + .../updaters/SingleChainUpdateSystem.kt | 2 +- .../storage/source/multi/MultiQueryBuilder.kt | 33 +- .../source/multi/MultiQueryBuilderImpl.kt | 64 ++- .../source/query/BaseStorageQueryContext.kt | 52 +- .../source/query/StorageQueryContext.kt | 28 +- .../nova/runtime/util/AccountLookup.kt | 20 + .../extrinsic/visitor/impl/ProxyWalkTest.kt | 330 +++++++++++ 542 files changed, 9629 insertions(+), 3552 deletions(-) create mode 100644 app/src/main/java/io/novafoundation/nova/app/root/di/busHandler/RequestBusHandlerModule.kt create mode 100644 app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/CompoundRequestBusHandler.kt create mode 100644 app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/ProxyExtrinsicValidationRequestBusHandler.kt create mode 100644 app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/RequestBusHandler.kt create mode 100644 common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/GenericCall.kt create mode 100644 common/src/main/java/io/novafoundation/nova/common/data/network/runtime/calls/GetStorageSize.kt create mode 100644 common/src/main/java/io/novafoundation/nova/common/utils/AlphaColorFilter.kt create mode 100644 common/src/main/java/io/novafoundation/nova/common/utils/AlphaDrawable.kt create mode 100644 common/src/main/java/io/novafoundation/nova/common/utils/bus/BaseRequestBus.kt create mode 100644 common/src/main/java/io/novafoundation/nova/common/utils/bus/RequestBus.kt create mode 100644 common/src/main/res/drawable/bg_chain_wallet.xml create mode 100644 common/src/main/res/drawable/ic_proxy.xml create mode 100644 common/src/main/res/drawable/shape_account_updated_indicator.xml create mode 100644 common/src/main/res/drawable/shape_updates_indicator.xml create mode 100644 common/src/main/res/layout/layout_puller.xml create mode 100644 common/src/test/java/io/novafoundation/nova/common/utils/PartitionTest.kt create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/converters/ProxyAccountConverters.kt create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/migrations/53_54_AddProxyAccount.kt create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ChainAccountLocal.kt create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/JoinedMetaAccountInfo.kt rename core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/{ => account}/MetaAccountLocal.kt (51%) create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ProxyAccountLocal.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/ProxiedWithProxy.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/MetaAccountsUpdatesRegistry.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/ProxySyncService.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxiedExtrinsicValidationSystem.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxyExtrinsicValidationRequestBus.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/ProxyRepository.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/AddAccountRepository.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/BaseAddAccountRepository.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/ledger/LedgerAddAccountRepository.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SeparateFlowSignerState.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountId.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxiedAndProxyMetaAccount.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxyAccount.kt delete mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountUi.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/CommonAccountsAdapter.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/MetaAccountPayloadGenerator.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountChipHolder.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountHolder.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountTitleHolder.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountChipGroupRvItem.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountTitleGroupRvItem.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountUi.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/proxy/ProxySigningPresenter.kt create mode 100644 feature-account-api/src/main/res/layout/item_delegated_account_group.xml create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealMetaAccountsUpdatesRegistry.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealProxySyncService.kt delete mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AddAccountRepository.kt delete mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/ParitySignerRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/RealProxyRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/ledger/RealLedgerAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/paritySigner/ParitySignerAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/proxied/ProxiedAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/JsonAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/MnemonicAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SecretsAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SeedAddAccountRepository.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/watchOnly/WatchOnlyAddAccountRepository.kt rename runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/FeeSigner.kt => feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/DefaultFeeSigner.kt (55%) create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/LeafSigner.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedFeeSigner.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedSigner.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxyCallFilterFactory.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/SignerPayloadModifierExt.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/AnyOfCallFilter.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/CallFilter.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/EverythingFilter.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/WhiteListFilter.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AddAccountsModule.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ProxySigningModule.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/ProxiedSignerModule.kt rename feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/{ => signers}/SignersModule.kt (61%) rename feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/{AccountDetailsInteractor.kt => WalletDetailsInteractor.kt} (51%) create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/DelegatedMetaAccountUpdatesListingMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/ProxyFormatter.kt delete mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsViewModel.kt rename feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/{AccountDetailsFragment.kt => WalletDetailsFragment.kt} (84%) create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsViewModel.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/LedgerWalletDetailsMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/PolkadotVaultWalletDetailsMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/ProxiedWalletDetailsMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/SecretsWalletDetailsMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixinFactory.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WatchOnlyWalletDetailsMixin.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/AccountFormatter.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/WalletMixinCommon.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesBottomSheet.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesViewModel.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountsAdapter.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesComponent.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesModule.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignNotEnoughPermissionBottomSheet.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignWarningBottomSheet.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/RealProxySigningPresenter.kt create mode 100644 feature-account-impl/src/main/res/layout/bottom_sheet_delegated_account_updates.xml create mode 100644 feature-account-impl/src/main/res/layout/bottom_sheet_proxy_warning.xml rename feature-account-impl/src/main/res/layout/{fragment_account_details.xml => fragment_wallet_details.xml} (100%) create mode 100644 feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerDerivationPath.kt create mode 100644 feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerRepository.kt delete mode 100644 feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/LedgerRepository.kt create mode 100644 feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/RealLedgerRepository.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ClaimedRewardPages.kt rename feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/{StakingEraValidatorInfosRequest.kt => StakingNominatorEraInfosRequest.kt} (75%) create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingValidatorEraInfosRequest.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ProxyHaveEnoughFeeValidation.kt rename feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/{OrmlAssets.kt => OrmlBalanceBinding.kt} (72%) create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/NovaSigner.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalk.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalkExt.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ExtrinsicVisitorLogger.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/NestedCallNode.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/proxy/ProxyNode.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt create mode 100644 runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index aa31325167..9f99797ba2 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -126,7 +126,7 @@ jobs: fileName: ${{ inputs.keystore-file-name }} fileDir: './app/' encodedString: ${{ env.CI_GITHUB_KEYSTORE_KEY_FILE }} - + - name: 🔐 Getting market sign key if: ${{ startsWith(inputs.keystore-file-name, 'market_key.jks') }} uses: timheuer/base64-to-file@v1.1 diff --git a/app/build.gradle b/app/build.gradle index 6ca8ca4f40..2f888b3f8c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ android { versionNameSuffix '-github' applicationIdSuffix '.github' - buildConfigField "String", "APP_UPDATE_SOURCE_LINK", "\"https://github.com/novasamatech/nova-wallet-android-releases/releases\"" + buildConfigField "String", "APP_UPDATE_SOURCE_LINK", "\"https://github.com/novasamatech/nova-wallet-android/releases\"" } develop { signingConfig signingConfigs.dev diff --git a/app/src/androidTest/java/io/novafoundation/nova/NftUniquesIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/NftUniquesIntegrationTest.kt index c8bf0c8687..dfa7a1fd3f 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/NftUniquesIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/NftUniquesIntegrationTest.kt @@ -15,6 +15,8 @@ import io.novafoundation.nova.runtime.di.RuntimeApi import io.novafoundation.nova.runtime.di.RuntimeComponent import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection +import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder +import io.novafoundation.nova.runtime.storage.source.query.multi import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct import jp.co.soramitsu.fearless_utils.runtime.metadata.storage @@ -88,60 +90,71 @@ class NftUniquesIntegrationTest { val classesIds = classesWithInstances.map { (collection, _) -> collection }.distinct() - val classMetadataStorage = runtime.metadata.uniques().storage("ClassMetadataOf") - val classStorage = runtime.metadata.uniques().storage("Class") - val instanceMetadataStorage = runtime.metadata.uniques().storage("InstanceMetadataOf") - val instanceDetailsStorage = runtime.metadata.uniques().storage("Asset") + val classDetailsDescriptor: MultiQueryBuilder.Descriptor + val classMetadatasDescriptor: MultiQueryBuilder.Descriptor + val instancesDetailsDescriptor: MultiQueryBuilder.Descriptor, UniquesInstance.Details> + val instancesMetadataDescriptor: MultiQueryBuilder.Descriptor, UniquesInstance.Metadata?> val multiQueryResults = multi { - classMetadataStorage.querySingleArgKeys(classesIds) - classStorage.querySingleArgKeys(classesIds) - instanceMetadataStorage.queryKeys(classesWithInstances) - instanceDetailsStorage.queryKeys(classesWithInstances) - } + classDetailsDescriptor = runtime.metadata.uniques().storage("Class").querySingleArgKeys( + keysArgs = classesIds, + keyExtractor = { it.component1() }, + binding = { parsedValue -> + val classDetailsStruct = parsedValue.cast() + + UniquesClass.Details( + instances = classDetailsStruct.getTyped("instances"), + frozen = classDetailsStruct.getTyped("isFrozen") + ) + } + ) - val classDetails = multiQueryResults.getValue(classStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() } - .mapValues { (_, parsedValue) -> - val classDetailsStruct = parsedValue.cast() + classMetadatasDescriptor = runtime.metadata.uniques().storage("ClassMetadataOf").querySingleArgKeys( + keysArgs = classesIds, + keyExtractor = { it.component1() }, + binding = { parsedValue -> + parsedValue?.cast()?.let { classMetadataStruct -> + UniquesClass.Metadata( + deposit = classMetadataStruct.getTyped("deposit"), + data = bindString(classMetadataStruct["data"]) + ) + } + } + ) - UniquesClass.Details( - instances = classDetailsStruct.getTyped("instances"), - frozen = classDetailsStruct.getTyped("isFrozen") - ) - } + instancesDetailsDescriptor = runtime.metadata.uniques().storage("Asset").queryKeys( + keysArgs = classesWithInstances, + keyExtractor = { it.component1() to it.component2() }, + binding = { parsedValue -> + val instanceDetailsStruct = parsedValue.cast() - val classMetadatas = multiQueryResults.getValue(classMetadataStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() } - .mapValues { (_, parsedValue) -> - parsedValue?.cast()?.let { classMetadataStruct -> - UniquesClass.Metadata( - deposit = classMetadataStruct.getTyped("deposit"), - data = bindString(classMetadataStruct["data"]) + UniquesInstance.Details( + owner = chain.addressOf(bindAccountId(instanceDetailsStruct["owner"])), + frozen = bindBoolean(instanceDetailsStruct["isFrozen"]) ) } - } + ) - val instancesDetails = multiQueryResults.getValue(instanceDetailsStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() to keyComponents.component2() } - .mapValues { (_, parsedValue) -> - val instanceDetailsStruct = parsedValue.cast() + instancesMetadataDescriptor = runtime.metadata.uniques().storage("InstanceMetadataOf").queryKeys( + keysArgs = classesWithInstances, + keyExtractor = { it.component1() to it.component2() }, + binding = { parsedValue -> + parsedValue?.cast()?.let { + UniquesInstance.Metadata( + data = bindString(it["data"]) + ) + } + } + ) + } - UniquesInstance.Details( - owner = chain.addressOf(bindAccountId(instanceDetailsStruct["owner"])), - frozen = bindBoolean(instanceDetailsStruct["isFrozen"]) - ) - } + val classDetails = multiQueryResults[classDetailsDescriptor] - val instancesMetadatas = multiQueryResults.getValue(instanceMetadataStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() to keyComponents.component2() } - .mapValues { (_, parsedValue) -> - parsedValue?.cast()?.let { - UniquesInstance.Metadata( - data = bindString(it["data"]) - ) - } - } + val classMetadatas = multiQueryResults[classMetadatasDescriptor] + + val instancesDetails = multiQueryResults[instancesDetailsDescriptor] + + val instancesMetadatas = multiQueryResults[instancesMetadataDescriptor] val classes = classesIds.associateWith { classId -> UniquesClass( diff --git a/app/src/androidTest/java/io/novafoundation/nova/balances/BalancesIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/balances/BalancesIntegrationTest.kt index 3dbdbf33ee..b26e843128 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/balances/BalancesIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/balances/BalancesIntegrationTest.kt @@ -6,10 +6,15 @@ import com.google.gson.Gson import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountInfo import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.emptyEthereumAccountId +import io.novafoundation.nova.common.utils.emptySubstrateAccountId import io.novafoundation.nova.common.utils.fromJson import io.novafoundation.nova.common.utils.hasModule import io.novafoundation.nova.common.utils.system +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_impl.di.AccountFeatureComponent import io.novafoundation.nova.runtime.BuildConfig.TEST_CHAINS_URL import io.novafoundation.nova.runtime.di.RuntimeApi @@ -122,7 +127,7 @@ class BalancesIntegrationTest( private suspend fun testFeeLoadingAsync(chain: Chain) { return coroutineScope { withTimeout(80.seconds) { - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, testTransactionOrigin()) { systemRemark(byteArrayOf(0)) val haveBatch = runtime.metadata.hasModule("Utility") @@ -133,4 +138,21 @@ class BalancesIntegrationTest( } } } + + private fun testTransactionOrigin(): TransactionOrigin = TransactionOrigin.Wallet( + MetaAccount( + id = 0, + chainAccounts = emptyMap(), + proxy = null, + substratePublicKey = null, + substrateCryptoType = null, + substrateAccountId = emptySubstrateAccountId(), + ethereumAddress = emptyEthereumAccountId(), + ethereumPublicKey = null, + isSelected = true, + name = "Test", + type = LightMetaAccount.Type.WATCH_ONLY, + status = LightMetaAccount.Status.ACTIVE + ) + ) } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/di/RootDependencies.kt b/app/src/main/java/io/novafoundation/nova/app/root/di/RootDependencies.kt index 43bb8dc126..5eea5fa6ed 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/di/RootDependencies.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/di/RootDependencies.kt @@ -10,6 +10,8 @@ import io.novafoundation.nova.common.utils.coroutines.RootScope import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.sequrity.BackgroundAccessObserver import io.novafoundation.nova.common.utils.systemCall.SystemCallExecutor +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults import io.novafoundation.nova.feature_assets.data.network.BalancesUpdateSystem @@ -21,6 +23,7 @@ import io.novafoundation.nova.feature_governance_api.data.MutableGovernanceState import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_versions_api.domain.UpdateNotificationsInteractor import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -61,12 +64,18 @@ interface RootDependencies { fun rootScope(): RootScope + fun proxySyncService(): ProxySyncService + fun governanceStateUpdater(): MutableGovernanceState fun dappMetadataRepository(): DAppMetadataRepository fun encryptionDefaults(): EncryptionDefaults + fun proxyExtrinsicValidationRequestBus(): ProxyExtrinsicValidationRequestBus + + fun proxyHaveEnoughFeeValidationFactory(): ProxyHaveEnoughFeeValidationFactory + val systemCallExecutor: SystemCallExecutor val contextManager: ContextManager diff --git a/app/src/main/java/io/novafoundation/nova/app/root/di/RootFeatureModule.kt b/app/src/main/java/io/novafoundation/nova/app/root/di/RootFeatureModule.kt index 5c041c4417..acdb0015f2 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/di/RootFeatureModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/di/RootFeatureModule.kt @@ -2,13 +2,15 @@ package io.novafoundation.nova.app.root.di import dagger.Module import dagger.Provides +import io.novafoundation.nova.app.root.di.busHandler.RequestBusHandlerModule import io.novafoundation.nova.app.root.domain.RootInteractor import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_assets.data.network.BalancesUpdateSystem import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository -@Module +@Module(includes = [RequestBusHandlerModule::class]) class RootFeatureModule { @Provides @@ -16,12 +18,14 @@ class RootFeatureModule { fun provideRootInteractor( walletRepository: WalletRepository, accountRepository: AccountRepository, - balancesUpdateSystem: BalancesUpdateSystem + balancesUpdateSystem: BalancesUpdateSystem, + proxySyncService: ProxySyncService ): RootInteractor { return RootInteractor( updateSystem = balancesUpdateSystem, walletRepository = walletRepository, - accountRepository = accountRepository + accountRepository = accountRepository, + proxySyncService = proxySyncService ) } } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/di/busHandler/RequestBusHandlerModule.kt b/app/src/main/java/io/novafoundation/nova/app/root/di/busHandler/RequestBusHandlerModule.kt new file mode 100644 index 0000000000..16bef9e385 --- /dev/null +++ b/app/src/main/java/io/novafoundation/nova/app/root/di/busHandler/RequestBusHandlerModule.kt @@ -0,0 +1,42 @@ +package io.novafoundation.nova.app.root.di.busHandler + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import io.novafoundation.nova.app.root.presentation.requestBusHandler.CompoundRequestBusHandler +import io.novafoundation.nova.app.root.presentation.requestBusHandler.ProxyExtrinsicValidationRequestBusHandler +import io.novafoundation.nova.app.root.presentation.requestBusHandler.RequestBusHandler +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.coroutines.RootScope +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus +import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory + +@Module +class RequestBusHandlerModule { + + @Provides + @FeatureScope + @IntoSet + fun provideProxyExtrinsicValidationRequestBusHandler( + scope: RootScope, + proxyProxyExtrinsicValidationRequestBus: ProxyExtrinsicValidationRequestBus, + proxyHaveEnoughFeeValidationFactory: ProxyHaveEnoughFeeValidationFactory, + resourceManager: ResourceManager + ): RequestBusHandler { + return ProxyExtrinsicValidationRequestBusHandler( + scope, + proxyProxyExtrinsicValidationRequestBus, + proxyHaveEnoughFeeValidationFactory, + resourceManager + ) + } + + @Provides + @FeatureScope + fun provideCompoundRequestBusHandler( + handlers: Set<@JvmSuppressWildcards RequestBusHandler> + ): CompoundRequestBusHandler { + return CompoundRequestBusHandler(handlers) + } +} diff --git a/app/src/main/java/io/novafoundation/nova/app/root/domain/RootInteractor.kt b/app/src/main/java/io/novafoundation/nova/app/root/domain/RootInteractor.kt index 43a3942795..ebd4d50039 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/domain/RootInteractor.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/domain/RootInteractor.kt @@ -1,16 +1,19 @@ package io.novafoundation.nova.app.root.domain import io.novafoundation.nova.core.updater.Updater +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_assets.data.network.BalancesUpdateSystem import io.novafoundation.nova.feature_buy_impl.domain.providers.ExternalProvider import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest class RootInteractor( private val updateSystem: BalancesUpdateSystem, private val walletRepository: WalletRepository, - private val accountRepository: AccountRepository + private val accountRepository: AccountRepository, + private val proxySyncService: ProxySyncService ) { fun runBalancesUpdate(): Flow = updateSystem.start() @@ -30,4 +33,10 @@ class RootInteractor( suspend fun isPinCodeSet(): Boolean { return accountRepository.isCodeSet() } + + fun syncProxies(): Flow<*> { + return proxySyncService.proxySyncTrigger().mapLatest { + proxySyncService.startSyncingSuspend() + } + } } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt index 11573d44a6..ee52154cbb 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt @@ -17,7 +17,7 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.add.Impo import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.account.advancedEncryption.AdvancedEncryptionFragment import io.novafoundation.nova.feature_account_impl.presentation.account.advancedEncryption.AdvancedEncryptionModePayload -import io.novafoundation.nova.feature_account_impl.presentation.account.details.AccountDetailsFragment +import io.novafoundation.nova.feature_account_impl.presentation.account.details.WalletDetailsFragment import io.novafoundation.nova.feature_account_impl.presentation.exporting.ExportPayload import io.novafoundation.nova.feature_account_impl.presentation.exporting.json.confirm.ExportJsonConfirmFragment import io.novafoundation.nova.feature_account_impl.presentation.exporting.json.confirm.ExportJsonConfirmPayload @@ -179,7 +179,7 @@ class Navigator( val destination = when (val currentDestinationId = navController?.currentDestination?.id) { R.id.welcomeFragment -> R.id.action_welcomeFragment_to_mnemonic_nav_graph R.id.createAccountFragment -> R.id.action_createAccountFragment_to_mnemonic_nav_graph - R.id.accountDetailsFragment -> R.id.action_accountDetailsFragment_to_mnemonic_nav_graph + R.id.walletDetailsFragment -> R.id.action_accountDetailsFragment_to_mnemonic_nav_graph else -> throw IllegalArgumentException("Unknown current destination to open mnemonic screen: $currentDestinationId") } @@ -284,6 +284,10 @@ class Navigator( navController?.navigate(R.id.action_open_switch_wallet) } + override fun openDelegatedAccountsUpdates() { + navController?.navigate(R.id.action_switchWalletFragment_to_delegatedAccountUpdates) + } + override fun openSelectAddress(arguments: Bundle) { navController?.navigate(R.id.action_open_select_address, arguments) } @@ -405,8 +409,8 @@ class Navigator( } } - override fun openAccountDetails(metaId: Long) { - val extras = AccountDetailsFragment.getBundle(metaId) + override fun openWalletDetails(metaId: Long) { + val extras = WalletDetailsFragment.getBundle(metaId) navController?.navigate(R.id.action_open_account_details, extras) } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/governance/GovernanceNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/governance/GovernanceNavigator.kt index 16db060824..ef4fa33e2f 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/governance/GovernanceNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/governance/GovernanceNavigator.kt @@ -79,8 +79,8 @@ class GovernanceNavigator( } } - override fun openAccountDetails(id: Long) { - commonNavigator.openAccountDetails(id) + override fun openWalletDetails(id: Long) { + commonNavigator.openWalletDetails(id) } override fun openAddDelegation() { diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt index 541e747b7a..445fd90580 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/settings/SettingsNavigator.kt @@ -30,8 +30,8 @@ class SettingsNavigator( args = PincodeFragment.getPinCodeBundle(PinCodeAction.Change) ) - override fun openAccountDetails(metaId: Long) { - delegate.openAccountDetails(metaId) + override fun openWalletDetails(metaId: Long) { + delegate.openWalletDetails(metaId) } override fun openSwitchWallet() { diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/StartMultiStakingNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/StartMultiStakingNavigator.kt index 0b99a5d81f..8ebc2c4a98 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/StartMultiStakingNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/StartMultiStakingNavigator.kt @@ -58,6 +58,6 @@ class StartMultiStakingNavigator( } override fun goToWalletDetails(metaId: Long) { - commonNavigationHolder.openAccountDetails(metaId) + commonNavigationHolder.openWalletDetails(metaId) } } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/parachain/ParachainStakingNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/parachain/ParachainStakingNavigator.kt index 160034f372..cf99622a45 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/parachain/ParachainStakingNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/parachain/ParachainStakingNavigator.kt @@ -41,7 +41,7 @@ class ParachainStakingNavigator( ) override fun openWalletDetails(metaId: Long) { - commonNavigator.openAccountDetails(metaId) + commonNavigator.openWalletDetails(metaId) } override fun returnToStakingMain() = performNavigation(R.id.back_to_staking_main) diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/relaychain/RelayStakingNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/relaychain/RelayStakingNavigator.kt index 7f426bd48c..2c797cef2e 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/relaychain/RelayStakingNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/staking/relaychain/RelayStakingNavigator.kt @@ -48,7 +48,7 @@ class RelayStakingNavigator( override fun openSwitchWallet() = commonNavigator.openSwitchWallet() - override fun openAccountDetails(metaAccountId: Long) = commonNavigator.openAccountDetails(metaAccountId) + override fun openWalletDetails(metaAccountId: Long) = commonNavigator.openWalletDetails(metaAccountId) override fun openCustomRebond() { performNavigation(R.id.action_stakingFragment_to_customRebondFragment) diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt index 3eb2780af2..ba2c21d6a6 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt @@ -7,12 +7,14 @@ import io.novafoundation.nova.app.root.presentation.deepLinks.CallbackEvent import io.novafoundation.nova.app.root.presentation.deepLinks.DeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.common.DeepLinkHandlingException import io.novafoundation.nova.app.root.presentation.deepLinks.common.formatDeepLinkHandlingException +import io.novafoundation.nova.app.root.presentation.requestBusHandler.CompoundRequestBusHandler import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.NetworkStateMixin import io.novafoundation.nova.common.mixin.api.NetworkStateUi import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.sequrity.SafeModeService import io.novafoundation.nova.common.utils.coroutines.RootScope +import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.sequrity.BackgroundAccessObserver import io.novafoundation.nova.core.updater.Updater @@ -43,7 +45,8 @@ class RootViewModel( private val walletConnectSessionsUseCase: WalletConnectSessionsUseCase, private val deepLinkHandler: DeepLinkHandler, private val automaticInteractionGate: AutomaticInteractionGate, - private val rootScope: RootScope + private val rootScope: RootScope, + private val compoundRequestBusHandler: CompoundRequestBusHandler ) : BaseViewModel(), NetworkStateUi by networkStateMixin { private var willBeClearedForLanguageChange = false @@ -62,12 +65,16 @@ class RootViewModel( checkForUpdates() + syncProxies() + syncCurrencies() syncWalletConnectSessions() updatePhishingAddresses() + obserBusEvents() + walletConnectService.onPairErrorLiveData.observeForever { showError(it.peekContent()) } @@ -75,6 +82,10 @@ class RootViewModel( subscribeDeepLinkCallback() } + private fun obserBusEvents() { + compoundRequestBusHandler.observe() + } + private fun subscribeDeepLinkCallback() { deepLinkHandler.callbackFlow .onEach { handleDeepLinkCallbackEvent(it) } @@ -107,6 +118,12 @@ class RootViewModel( launch { currencyInteractor.syncCurrencies() } } + private fun syncProxies() { + interactor.syncProxies() + .inBackground() + .launchIn(rootScope) + } + private fun handleUpdatesSideEffect(sideEffect: Updater.SideEffect) { // pass } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ImportMnemonicDeepLinkHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ImportMnemonicDeepLinkHandler.kt index 65134ab986..997320d502 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ImportMnemonicDeepLinkHandler.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ImportMnemonicDeepLinkHandler.kt @@ -10,12 +10,12 @@ import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed import io.novafoundation.nova.core.model.CryptoType import io.novafoundation.nova.feature_account_api.data.derivationPath.DerivationPathDecoder +import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload import io.novafoundation.nova.feature_account_api.presenatation.account.add.ImportAccountPayload import io.novafoundation.nova.feature_account_api.presenatation.account.add.ImportType import io.novafoundation.nova.feature_account_api.presenatation.account.common.model.AdvancedEncryptionModel -import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import jp.co.soramitsu.fearless_utils.encrypt.mnemonic.Mnemonic import jp.co.soramitsu.fearless_utils.encrypt.mnemonic.MnemonicCreator @@ -40,7 +40,7 @@ class ImportMnemonicDeepLinkHandler( } override suspend fun handleDeepLink(data: Uri) { - if (accountRepository.hasMetaAccounts()) { + if (accountRepository.hasActiveMetaAccounts()) { automaticInteractionGate.awaitInteractionAllowed() } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt index 5ac9495209..a86dc36655 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.app.root.domain.RootInteractor import io.novafoundation.nova.app.root.presentation.RootRouter import io.novafoundation.nova.app.root.presentation.RootViewModel import io.novafoundation.nova.app.root.presentation.deepLinks.RootDeepLinkHandler +import io.novafoundation.nova.app.root.presentation.requestBusHandler.CompoundRequestBusHandler import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.mixin.api.NetworkStateMixin @@ -52,7 +53,8 @@ class RootActivityModule { walletConnectSessionsUseCase: WalletConnectSessionsUseCase, deepLinkHandler: RootDeepLinkHandler, automaticInteractionGate: AutomaticInteractionGate, - rootScope: RootScope + rootScope: RootScope, + compoundRequestBusHandler: CompoundRequestBusHandler ): ViewModel { return RootViewModel( interactor, @@ -69,7 +71,8 @@ class RootActivityModule { walletConnectSessionsUseCase, deepLinkHandler, automaticInteractionGate, - rootScope + rootScope, + compoundRequestBusHandler ) } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/CompoundRequestBusHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/CompoundRequestBusHandler.kt new file mode 100644 index 0000000000..34e8ebf099 --- /dev/null +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/CompoundRequestBusHandler.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.app.root.presentation.requestBusHandler + +class CompoundRequestBusHandler( + private val handlers: Set +) : RequestBusHandler { + override fun observe() { + handlers.forEach { it.observe() } + } +} diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/ProxyExtrinsicValidationRequestBusHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/ProxyExtrinsicValidationRequestBusHandler.kt new file mode 100644 index 0000000000..0485b4ecd0 --- /dev/null +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/ProxyExtrinsicValidationRequestBusHandler.kt @@ -0,0 +1,55 @@ +package io.novafoundation.nova.app.root.presentation.requestBusHandler + +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.bus.observeBusEvent +import io.novafoundation.nova.common.utils.coroutines.RootScope +import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus.ValidationResponse +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxiedExtrinsicValidationFailure +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxiedExtrinsicValidationPayload +import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory +import io.novafoundation.nova.feature_wallet_api.domain.validation.proxyHasEnoughFeeValidation +import kotlinx.coroutines.flow.launchIn + +class ProxyExtrinsicValidationRequestBusHandler( + private val scope: RootScope, + private val proxyProxyExtrinsicValidationRequestBus: ProxyExtrinsicValidationRequestBus, + private val proxyHaveEnoughFeeValidationFactory: ProxyHaveEnoughFeeValidationFactory, + private val resourceManager: ResourceManager +) : RequestBusHandler { + + override fun observe() { + proxyProxyExtrinsicValidationRequestBus.observeEvent() + .observeBusEvent { request -> + val validationResult = createValidationSystem() + .validate(request.validationPayload) + ValidationResponse(validationResult) + }.launchIn(scope) + } + + private fun createValidationSystem(): ValidationSystem { + return ValidationSystem { + proxyHasEnoughFee() + } + } + + private fun ValidationSystemBuilder.proxyHasEnoughFee() { + proxyHasEnoughFeeValidation( + factory = proxyHaveEnoughFeeValidationFactory, + metaAccount = { it.proxyMetaAccount }, + proxyAccountId = { it.proxyAccountId }, + call = { it.call }, + chainWithAsset = { it.chainWithAsset }, + proxyNotEnoughFee = { payload, availableBalance, fee -> + ProxiedExtrinsicValidationFailure.ProxyNotEnoughFee( + payload.proxyMetaAccount, + payload.chainWithAsset.asset, + availableBalance, + fee + ) + } + ) + } +} diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/RequestBusHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/RequestBusHandler.kt new file mode 100644 index 0000000000..a4a1c67564 --- /dev/null +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/requestBusHandler/RequestBusHandler.kt @@ -0,0 +1,5 @@ +package io.novafoundation.nova.app.root.presentation.requestBusHandler + +interface RequestBusHandler { + fun observe() +} diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 4a9f47b34d..41cab41e5b 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -85,7 +85,7 @@ app:exitAnim="@anim/fragment_close_exit" app:popEnterAnim="@anim/fragment_open_enter" app:popExitAnim="@anim/fragment_open_exit" - app:popUpTo="@+id/accountDetailsFragment" /> + app:popUpTo="@+id/walletDetailsFragment" /> + + + tools:layout="@layout/bottom_sheet_dynamic_list"> + + + + + android:id="@+id/walletDetailsFragment" + android:name="io.novafoundation.nova.feature_account_impl.presentation.account.details.WalletDetailsFragment" + android:label="WalletDetailsFragment" + tools:layout="@layout/fragment_wallet_details"> ?) { - class Node( - val id: String, - val address: String, - val era: BigInteger, - val total: String, - val own: String, - ) - } - } +class EraValidatorInfoQueryResponse(val eraValidatorInfos: SubQueryNodes?) { + class EraValidatorInfo( + val id: String, + val address: String, + val era: BigInteger, + val total: String, + val own: String, + ) } diff --git a/common/src/main/java/io/novafoundation/nova/common/di/modules/NetworkModule.kt b/common/src/main/java/io/novafoundation/nova/common/di/modules/NetworkModule.kt index 8d91c07545..396690d256 100644 --- a/common/src/main/java/io/novafoundation/nova/common/di/modules/NetworkModule.kt +++ b/common/src/main/java/io/novafoundation/nova/common/di/modules/NetworkModule.kt @@ -59,6 +59,7 @@ class NetworkModule { rateApp = BuildConfig.RATE_URL, website = BuildConfig.WEBSITE_URL, wikiBase = BuildConfig.NOVA_WALLET_WIKI_BASE, + wikiProxy = BuildConfig.NOVA_WALLET_WIKI_PROXY, github = BuildConfig.GITHUB_URL, email = BuildConfig.EMAIL, youtube = BuildConfig.YOUTUBE_URL, diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/AlphaColorFilter.kt b/common/src/main/java/io/novafoundation/nova/common/utils/AlphaColorFilter.kt new file mode 100644 index 0000000000..06a57484e6 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/AlphaColorFilter.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.common.utils + +import android.graphics.ColorMatrixColorFilter + +class AlphaColorFilter(val alpha: Float) : ColorMatrixColorFilter( + floatArrayOf( + 1f, 0f, 0f, 0f, 0f, + 0f, 1f, 0f, 0f, 0f, + 0f, 0f, 1f, 0f, 0f, + 0f, 0f, 0f, alpha, 0f + ) +) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/AlphaDrawable.kt b/common/src/main/java/io/novafoundation/nova/common/utils/AlphaDrawable.kt new file mode 100644 index 0000000000..f34c63b91e --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/AlphaDrawable.kt @@ -0,0 +1,55 @@ +package io.novafoundation.nova.common.utils + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.Drawable +import androidx.core.graphics.minus +import androidx.core.graphics.toRectF + +/** + * Note: this implementation is very expensive (see [Canvas.saveLayerAlpha]). + * This is usefull for drawables without implemented alpha and color filter support, such as PictureDrawable. + * In other cases it's recommended to use [Drawable.setAlpha] or [Drawable.setColorFilter] instead of this class. + */ +class AlphaDrawable(private val nestedDrawable: Drawable, private var alpha: Float) : Drawable() { + + private val layerBounds: RectF = RectF() + + init { + bounds = Rect(nestedDrawable.bounds) + layerBounds.set(bounds.toRectF()) + } + + override fun draw(canvas: Canvas) { + canvas.saveLayerAlpha(layerBounds, (alpha * 255).toInt()) + nestedDrawable.draw(canvas) + canvas.restore() + } + + override fun setAlpha(alpha: Int) { + this.alpha = alpha.toFloat() / 255f + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + nestedDrawable.colorFilter = colorFilter + } + + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int { + return nestedDrawable.opacity + } + + override fun getIntrinsicWidth(): Int { + return nestedDrawable.intrinsicWidth + } + + override fun getIntrinsicHeight(): Int { + return nestedDrawable.intrinsicHeight + } +} + +fun Drawable.withAlphaDrawable(alpha: Float): Drawable { + return AlphaDrawable(this, alpha) +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt index ea6ad0c683..fdf5dca621 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt @@ -25,9 +25,11 @@ import jp.co.soramitsu.fearless_utils.runtime.definitions.types.bytesOrNull import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.DefaultSignedExtensions import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent import jp.co.soramitsu.fearless_utils.runtime.definitions.types.skipAliases +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.genesisHash import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata @@ -35,6 +37,7 @@ import jp.co.soramitsu.fearless_utils.runtime.metadata.callOrNull import jp.co.soramitsu.fearless_utils.runtime.metadata.fullName import jp.co.soramitsu.fearless_utils.runtime.metadata.module import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Constant +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event import jp.co.soramitsu.fearless_utils.runtime.metadata.module.FunctionArgument import jp.co.soramitsu.fearless_utils.runtime.metadata.module.MetadataFunction import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module @@ -49,6 +52,7 @@ import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.addressPrefix import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAddress +import org.web3j.crypto.Sign import java.io.ByteArrayOutputStream import java.math.BigInteger import java.nio.ByteBuffer @@ -73,6 +77,9 @@ fun String.isValidSS58Address() = runCatching { toAccountId() }.isSuccess fun String.removeHexPrefix() = removePrefix("0x") fun MetadataFunction.argument(name: String): FunctionArgument = arguments.first { it.name == name } + +fun MetadataFunction.argumentType(name: String): RuntimeType<*, *> = requireNotNull(argument(name).type) + fun FunctionArgument.requireActualType() = type?.skipAliases()!! fun Short.toByteArray(byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { @@ -228,6 +235,8 @@ fun RuntimeMetadata.assetConversionOrNull() = moduleOrNull(Modules.ASSET_CONVERS fun RuntimeMetadata.assetConversion() = module(Modules.ASSET_CONVERSION) +fun RuntimeMetadata.proxy() = module(Modules.PROXY) + fun RuntimeMetadata.firstExistingModuleName(vararg options: String): String { return options.first(::hasModule) } @@ -277,19 +286,37 @@ fun GenericCall.Instance.instanceOf(moduleName: String, vararg callNames: String fun GenericEvent.Instance.instanceOf(moduleName: String, eventName: String): Boolean = moduleName == module.name && eventName == event.name +fun GenericEvent.Instance.instanceOf(event: Event): Boolean = event.index == this.event.index + fun structOf(vararg pairs: Pair) = Struct.Instance(mapOf(*pairs)) +fun SignedRaw.toEcdsaSignatureData(): Sign.SignatureData { + return signatureWrapper.run { + require(this is SignatureWrapper.Ecdsa) + Sign.SignatureData(v, r, s) + } +} + +fun SignedRaw.asHexString() = signatureWrapper.asHexString() + fun SignatureWrapper.asHexString() = signature.toHexString(withPrefix = true) fun String.ethereumAddressToAccountId() = asEthereumAddress().toAccountId().value fun AccountId.ethereumAccountIdToAddress(withChecksum: Boolean = true) = asEthereumAccountId().toAddress(withChecksum).value fun emptyEthereumAccountId() = ByteArray(20) { 1 } + +fun emptySubstrateAccountId() = ByteArray(32) + fun emptyEthereumAddress() = emptyEthereumAccountId().ethereumAccountIdToAddress(withChecksum = false) val SignerPayloadExtrinsic.chainId: String get() = genesisHash.toHexString() +fun CallRepresentation.toCallInstance(): CallRepresentation.Instance? { + return (this as? CallRepresentation.Instance) +} + object Modules { const val VESTING: String = "Vesting" const val STAKING = "Staking" @@ -344,4 +371,19 @@ object Modules { const val ASSET_TX_PAYMENT = "AssetTxPayment" const val UTILITY = "Utility" + + const val PROXY = "Proxy" + + const val AUCTIONS = "auctions" + + const val INDICES = "Indices" + const val GRANDPA = "Grandpa" + const val IM_ONLINE = "ImOnline" + const val BOUNTIES = "Bounties" + const val CHILD_BOUNTIES = "ChildBounties" + const val WHITELIST = "Whitelist" + const val CLAIMS = "Claims" + const val MULTISIG = "Multisig" + const val REGISTRAR = "Registrar" + const val FAST_UNSTAKE = "FastUnstake" } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index e986a9897a..bbb69e4d84 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -170,6 +170,47 @@ fun Result.requireException() = exceptionOrNull()!! fun Result.requireValue() = getOrThrow()!! +/** + * Given a list finds a partition point in O(log2(N)) given that there is only a single partition point present. + * That is, there is only a single place in the whole array where the value of [partition] changes from false to true + * + * @return index of the first element where invocation of [partition] returns true. Returns null in case there is no such elements in the list + */ +inline fun List.findPartitionPoint(partition: (T) -> Boolean): Int? { + if (isEmpty()) return null + + var lowIdx = 0 + var highIdx = size - 1 + + while (highIdx - lowIdx > 1) { + val midIdx = (lowIdx + highIdx) / 2 + + val midValue = get(midIdx) + val isPartitionTrue = partition(midValue) + + if (isPartitionTrue) { + highIdx = midIdx + } else { + lowIdx = midIdx + 1 + } + } + + val isLowTrue = partition(get(lowIdx)) + if (isLowTrue) return lowIdx + + val isHighTrue = partition(get(highIdx)) + if (isHighTrue) return highIdx + + return null +} + +/** + * @see [findPartitionPoint] + */ +fun List.findPartitionPoint(): Int? { + return findPartitionPoint { it } +} + fun Result.mapFailure(transform: (Throwable) -> Throwable): Result { return when { isFailure -> Result.failure(transform(requireException())) @@ -357,6 +398,8 @@ inline fun Iterable.mapNotNullToSet(mapper: (T) -> R?): Set = fun List.indexOfFirstOrNull(predicate: (T) -> Boolean) = indexOfFirst(predicate).takeIf { it >= 0 } +fun List.indexOfOrNull(value: T) = indexOf(value).takeIf { it >= 0 } + @Suppress("IfThenToElvis") fun ByteArray?.optionalContentEquals(other: ByteArray?): Boolean { return if (this == null) { diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt index c26f57fbab..b0d9b86a2f 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt @@ -28,10 +28,26 @@ fun Spannable.setFullSpan(span: Any): Spannable { // This method is nice for ImageSpan fun Spannable.setEndSpan(span: Any): Spannable { - val spannable = SpannableStringBuilder(this) - spannable.append(" ") - .setSpan(span, length, length + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannable + return SpannableStringBuilder(this) + .appendEnd(span) +} + +fun SpannableStringBuilder.appendSpace(): SpannableStringBuilder { + append(" ") + return this +} + +fun SpannableStringBuilder.append(text: CharSequence?, span: Any): SpannableStringBuilder { + val startSpan = length + append(text) + .setSpan(span, startSpan, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return this +} + +fun SpannableStringBuilder.appendEnd(span: Any): SpannableStringBuilder { + appendSpace() + .setSpan(span, length - 1, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return this } fun clickableSpan(onClick: () -> Unit) = object : ClickableSpan() { diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/bus/BaseRequestBus.kt b/common/src/main/java/io/novafoundation/nova/common/utils/bus/BaseRequestBus.kt new file mode 100644 index 0000000000..8dd6201fa4 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/bus/BaseRequestBus.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.common.utils.bus + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.runBlocking +import kotlin.coroutines.Continuation +import kotlin.coroutines.suspendCoroutine + +abstract class BaseRequestBus : RequestBus { + + private val eventFlow = MutableSharedFlow, T>>() + + override suspend fun handle(request: T): R { + return suspendCoroutine { continuation -> + runBlocking { + eventFlow.emit(continuation to request) + } + } + } + + override fun observeEvent(): Flow, T>> { + return eventFlow + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/bus/RequestBus.kt b/common/src/main/java/io/novafoundation/nova/common/utils/bus/RequestBus.kt new file mode 100644 index 0000000000..b6f1965ce9 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/bus/RequestBus.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.common.utils.bus + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +interface RequestBus { + + interface Request + + interface Response + + suspend fun handle(request: T): R + + fun observeEvent(): Flow, T>> +} + +fun Flow, T>>.observeBusEvent( + action: suspend (T) -> R +): Flow, T>> { + return this.onEach { (continuation, request) -> + val response = action(request) + continuation.resume(response) + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/AlertView.kt b/common/src/main/java/io/novafoundation/nova/common/view/AlertView.kt index f8e020e21a..3201c9c34a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/AlertView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/AlertView.kt @@ -3,20 +3,22 @@ package io.novafoundation.nova.common.view import android.content.Context import android.util.AttributeSet import android.view.View -import android.widget.LinearLayout import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintLayout import io.novafoundation.nova.common.R import io.novafoundation.nova.common.utils.WithContextExtensions import io.novafoundation.nova.common.utils.getEnum import io.novafoundation.nova.common.utils.getResourceIdOrNull import io.novafoundation.nova.common.utils.letOrHide import io.novafoundation.nova.common.utils.setImageTintRes +import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.utils.updatePadding import io.novafoundation.nova.common.utils.useAttributes import kotlinx.android.synthetic.main.view_alert.view.alertIcon import kotlinx.android.synthetic.main.view_alert.view.alertMessage +import kotlinx.android.synthetic.main.view_alert.view.alertSubMessage typealias SimpleAlertModel = String @@ -24,7 +26,7 @@ class AlertView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0, -) : LinearLayout(context, attrs, defStyle), WithContextExtensions by WithContextExtensions(context) { +) : ConstraintLayout(context, attrs, defStyle), WithContextExtensions by WithContextExtensions(context) { enum class StylePreset { WARNING, ERROR, INFO @@ -35,8 +37,6 @@ class AlertView @JvmOverloads constructor( init { View.inflate(context, R.layout.view_alert, this) - orientation = HORIZONTAL - updatePadding(top = 10.dp, start = 16.dp, end = 16.dp, bottom = 10.dp) attrs?.let(::applyAttrs) @@ -51,16 +51,20 @@ class AlertView @JvmOverloads constructor( setStyle(styleFromPreset(preset)) } - fun setText(text: String) { + fun setMessage(text: String) { alertMessage.text = text } - fun setText(@StringRes textRes: Int) { + fun setMessage(@StringRes textRes: Int) { alertMessage.setText(textRes) } + fun setSubMessage(text: CharSequence?) { + alertSubMessage.setTextOrHide(text) + } + fun setModel(maybeModel: SimpleAlertModel?) = letOrHide(maybeModel) { model -> - setText(model) + setMessage(model) } private fun setStyleBackground(@ColorRes colorRes: Int) { @@ -83,7 +87,7 @@ class AlertView @JvmOverloads constructor( setStyle(Style(iconRes, backgroundColorRes, iconTintRes)) val text = it.getString(R.styleable.AlertView_android_text) - text?.let(::setText) + text?.let(::setMessage) } private fun styleFromPreset(preset: StylePreset) = when (preset) { @@ -93,4 +97,4 @@ class AlertView @JvmOverloads constructor( } } -fun AlertView.setTextOrHide(text: String?) = letOrHide(text, ::setText) +fun AlertView.setMessageOrHide(text: String?) = letOrHide(text, ::setMessage) diff --git a/common/src/main/java/io/novafoundation/nova/common/view/ChipLabelView.kt b/common/src/main/java/io/novafoundation/nova/common/view/ChipLabelView.kt index 1daedb1782..fa8e96f5ab 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/ChipLabelView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/ChipLabelView.kt @@ -17,7 +17,7 @@ import kotlin.math.roundToInt private const val BASE_ICON_PADDING_DP = 6 -class ChipLabelModel( +data class ChipLabelModel( @DrawableRes val iconRes: Int, val title: String ) diff --git a/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/ActionNotAllowedBottomSheet.kt b/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/ActionNotAllowedBottomSheet.kt index 4a5175efa2..edea3ba0e1 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/ActionNotAllowedBottomSheet.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/bottomSheet/ActionNotAllowedBottomSheet.kt @@ -19,16 +19,16 @@ open class ActionNotAllowedBottomSheet( private val onSuccess: () -> Unit, ) : BaseBottomSheet(context, R.style.BottomSheetDialog), WithContextExtensions by WithContextExtensions(context) { - val image: ImageView + val iconView: ImageView get() = actionNotAllowedImage - val title: TextView + val titleView: TextView get() = actionNotAllowedTitle - val subtitle: TextView + val subtitleView: TextView get() = actionNotAllowedSubtitle - val button: PrimaryButton + val buttonView: PrimaryButton get() = actionNotAllowedOk init { @@ -50,13 +50,13 @@ open class ActionNotAllowedBottomSheet( super.setContentView(layoutResId) } - protected fun applySolidIconStyle(@DrawableRes src: Int) = with(image) { + protected fun applySolidIconStyle(@DrawableRes src: Int) = with(iconView) { setPadding(12.dp) setBackgroundResource(R.drawable.bg_icon_big) setImageResource(src) } - protected fun applyDashedIconStyle(@DrawableRes src: Int) = with(image) { + protected fun applyDashedIconStyle(@DrawableRes src: Int) = with(iconView) { setPadding(12.dp) setBackgroundResource(R.drawable.bg_icon_big_dashed) setImageResource(src) diff --git a/common/src/main/res/drawable/bg_chain_wallet.xml b/common/src/main/res/drawable/bg_chain_wallet.xml new file mode 100644 index 0000000000..42d8f9feb1 --- /dev/null +++ b/common/src/main/res/drawable/bg_chain_wallet.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/ic_proxy.xml b/common/src/main/res/drawable/ic_proxy.xml new file mode 100644 index 0000000000..078d22f110 --- /dev/null +++ b/common/src/main/res/drawable/ic_proxy.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/src/main/res/drawable/shape_account_updated_indicator.xml b/common/src/main/res/drawable/shape_account_updated_indicator.xml new file mode 100644 index 0000000000..49913003e3 --- /dev/null +++ b/common/src/main/res/drawable/shape_account_updated_indicator.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/shape_updates_indicator.xml b/common/src/main/res/drawable/shape_updates_indicator.xml new file mode 100644 index 0000000000..73f40bd947 --- /dev/null +++ b/common/src/main/res/drawable/shape_updates_indicator.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/common/src/main/res/layout/bottom_sheet_action_not_allowed.xml b/common/src/main/res/layout/bottom_sheet_action_not_allowed.xml index f879107f9c..3b2a7610ca 100644 --- a/common/src/main/res/layout/bottom_sheet_action_not_allowed.xml +++ b/common/src/main/res/layout/bottom_sheet_action_not_allowed.xml @@ -12,11 +12,12 @@ style="@style/Widget.Nova.Puller" android:layout_marginTop="6dp" /> - + \ No newline at end of file diff --git a/common/src/main/res/layout/view_alert.xml b/common/src/main/res/layout/view_alert.xml index 6c45231649..23927fb082 100644 --- a/common/src/main/res/layout/view_alert.xml +++ b/common/src/main/res/layout/view_alert.xml @@ -1,5 +1,6 @@ + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + \ No newline at end of file diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 5dc69c1011..ad8dfc566e 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -109,6 +109,15 @@ Перейдите на вкладку «Наборы ключей». Выберите свой набор ключей, сеть, а затем учетную запись, которую вы хотите добавить в Nova Wallet Polkadot Vault предоставит вам QR-код для сканирования Приватный ключ + Делегировано вам (проксированные) + Любые действия + Аукционы + Отмена прокси + Демократия + Идентификация личности + Пулы номинаций + Не переводы + Стейкинг У меня уже есть аккаунт 64 шест. символа Выберите аппаратный кошелёк @@ -232,6 +241,7 @@ Отключен Отключить Готово + Больше не показывать это Редактировать %s (и еще %s) Выберите приложение для работы с почтой @@ -482,6 +492,8 @@ Поиск по адресу или имени Недопустимый формат адреса. Убедитесь, что адрес принадлежит правильной сети результаты поиска: %d + Nova Wallet автоматически добавляет делегированные аккаунты (Прокси) в отдельную категорию для вас. Вы всегда можете управлять кошельками в Настройках. + Обновление делегированных аккаунтов Добавить делегацию Голоса за все время Делегат @@ -526,6 +538,7 @@ ECDSA ed25519 (альтернативный) Edwards + Недостаточно токенов для оплаты комиссии Контракт Вызов контракта Функция @@ -536,7 +549,6 @@ Сеть: %s\nМнемоника: %s Подождите, пока будет рассчитана комиссия Расчет комиссии в процессе - Выберите токен для получения Детали обмена Макс: Вы платите @@ -597,6 +609,8 @@ Подключение… Коллекция Создатель + %s за %s + %s единиц из %s #%s Издание из %s Неограниченная серия Владелец @@ -653,6 +667,17 @@ Подтверждение с PIN Безопасный режим Настройки + Этот аккаунт предоставил доступ для выполнения транзакций следующему аккаунту: + Более не действительны + Что такое прокси? + Делегированный аккаунт %s не имеет достаточного баланса для оплаты сетевой комиссии %s. Доступный баланс для оплаты комиссии: %s + Проксированные кошельки не поддерживают подпись произвольных сообщений - только транзакций + %1$s не делегировал прав %2$s + %1$s делегировал %2$s только для %3$s + Упс! Недостаточно разрешений + Транзакция будет инициирована %s как делегированным аккаунтом. Сетевая комиссия будет оплачена делегированным аккаунтом. + Это делегирующий (проксированный) аккаунт + %s Вставьте json строку или загрузите файл… Загрузите файл JSON для восстановления @@ -827,8 +852,8 @@ Неподдерживаемый тип стейкинга sr25519 (рекомендованный) Schnorrkel - Добавьте учетную запись контроллера в устройство. - Нет доступа к учетной записи контроллера + Добавьте учетную запись контроллера в устройство. + Нет доступа к учетной записи контроллера Выбранный аккаунт уже используется в качестве контроллера. Активные делегаторы Чтобы выполнить это действие, добавьте контроллер аккаунт %s в приложение diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index a5d6d8b1f6..668c980cc4 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,6 +1,46 @@ + No access to controller account + Add your controller account in device. + + Staking is an option to earn passive income by locking your tokens in the network. Staking rewards are allocated every era (6 hours on Kusama and 24 hours on Polkadot). You can stake as long as you wish, and for unstaking your tokens you need to wait for the unstaking period to end, making your tokens available to be redeemed. + + %1$s has not delegated %2$s + + Delegated account %s doesn\'t have enough balance to pay the network fee of %s. Available balance to pay fee: %s + Not enough tokens to pay the fee + + This account granted access to perform transactions to the following account: + + Do not show this again + + This is Delegating (Proxied) account + Transaction will be initiated by %s as a delegated account. Network fee will be paid by delegated account. + + Oops! Not enough permission + %1$s delegated %2$s only for %3$s + + Proxied wallets do not support signing arbitrary messages — only transactions + + Delegated accounts update + Nova Wallet automatically adds delegated authorities (Proxy) to a separate category for you. You can always manage wallets in Settings. + What is a Proxy? + + No longer valid + + %s proxy + Any + Non Transfer + Governance + Staking + Identity Judgement + Cancel Proxy + Auction + Nomination Pools + + Delegated to you (Proxieds) + Chain is not found Governance type is not specified Governance type is not supported @@ -591,9 +631,6 @@ Safe mode - No access to controller account - Add your controller account in device. - Page settings Add to Favorites Remove from Favorites @@ -1599,7 +1636,6 @@ Rewards for staking are available to payout at the end of each era (6 hours in Kusama and 24 hours in Polkadot). Network stores pending rewards during 84 eras and in most cases validators are paying out the rewards for everyone. However, validators might forget or something might happen with them, so nominators can payout their rewards by themselves. Although rewards are usually distributed by validators, Nova Wallet helps by alerting if there are any unpaid rewards that are close to expiring. You will receive alerts about this and other activities on the staking screen. Receiving rewards - Staking is an option to earn passive income by locking your tokens in the network. Staking rewards are allocated every era (6 hours on Kusama and 24 hours on Polkadot). You can stake as long as you wish, and for unstaking your tokens you need to wait for the unstacking period to end, making your tokens available to be redeemed. Staking is an important part of network security and reliability. Anyone can run validator nodes, but only those who have enough tokens staked will be elected by the network to participate in composing new blocks and receive the rewards. Validators often do not have enough tokens by themselves, so nominators are helping them by locking their tokens for them to achieve the required amount of stake. What is staking? The validator runs a blockchain node 24/7 and is required to have enough stake locked (both owned and provided by nominators) to be elected by the network. Validators should maintain their nodes\' performance and reliability to be rewarded. Being a validator is almost a full-time job, there are companies that are focused to be validators on the blockchain networks. diff --git a/common/src/test/java/io/novafoundation/nova/common/utils/PartitionTest.kt b/common/src/test/java/io/novafoundation/nova/common/utils/PartitionTest.kt new file mode 100644 index 0000000000..c945ef7d98 --- /dev/null +++ b/common/src/test/java/io/novafoundation/nova/common/utils/PartitionTest.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.common.utils + +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class PartitionTest { + + @Test + fun testEveryCombinationBelowSize100() { + (1..100).map { size -> + (0 .. size).map { truePointIndex -> + val list = List(truePointIndex) { false } + List(size - truePointIndex) { true } + runTest(list, expectedResult = truePointIndex.takeIf { truePointIndex < size }) + } + } + } + + private fun runTest( + list: List, + expectedResult: Int? + ) { + var iterationCount = 0 + val actualResult = list.findPartitionPoint { iterationCount++; it } + + assertEquals("Expected: ${expectedResult}, Got: $actualResult in $list", expectedResult, actualResult) + } +} 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 1ba32b9d99..098334b9e0 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 @@ -4,12 +4,12 @@ import io.novafoundation.nova.common.utils.CollectionDiffer import io.novafoundation.nova.core.model.CryptoType import io.novafoundation.nova.core_db.model.CurrencyLocal import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal import io.novafoundation.nova.core_db.model.chain.ChainLocal 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.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal fun createTestChain( id: String, diff --git a/core-db/src/androidTest/java/io/novafoundation/nova/core_db/migrations/BetterChainDiffingTest_8_9.kt b/core-db/src/androidTest/java/io/novafoundation/nova/core_db/migrations/BetterChainDiffingTest_8_9.kt index cca240e69b..370dacd76b 100644 --- a/core-db/src/androidTest/java/io/novafoundation/nova/core_db/migrations/BetterChainDiffingTest_8_9.kt +++ b/core-db/src/androidTest/java/io/novafoundation/nova/core_db/migrations/BetterChainDiffingTest_8_9.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.core_db.converters.CryptoTypeConverters import io.novafoundation.nova.core_db.dao.assetOf import io.novafoundation.nova.core_db.dao.chainOf import io.novafoundation.nova.core_db.dao.testMetaAccount -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigInteger 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 74e9937001..ee400583e8 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 @@ -16,6 +16,7 @@ import io.novafoundation.nova.core_db.converters.MetaAccountTypeConverters import io.novafoundation.nova.core_db.converters.NetworkTypeConverters import io.novafoundation.nova.core_db.converters.NftTypeConverters import io.novafoundation.nova.core_db.converters.OperationConverters +import io.novafoundation.nova.core_db.converters.ProxyAccountConverters import io.novafoundation.nova.core_db.dao.AccountDao import io.novafoundation.nova.core_db.dao.AccountStakingDao import io.novafoundation.nova.core_db.dao.AssetDao @@ -65,6 +66,7 @@ import io.novafoundation.nova.core_db.migrations.AddMetaAccountType_14_15 import io.novafoundation.nova.core_db.migrations.AddNfts_5_6 import io.novafoundation.nova.core_db.migrations.AddNodeSelectionStrategyField_38_39 import io.novafoundation.nova.core_db.migrations.AddPoolIdToOperations_46_47 +import io.novafoundation.nova.core_db.migrations.AddProxyAccount_54_55 import io.novafoundation.nova.core_db.migrations.AddRewardAccountToStakingDashboard_43_44 import io.novafoundation.nova.core_db.migrations.AddRuntimeFlagToChains_36_37 import io.novafoundation.nova.core_db.migrations.AddSitePhishing_6_7 @@ -117,14 +119,15 @@ import io.novafoundation.nova.core_db.model.StorageEntryLocal import io.novafoundation.nova.core_db.model.TokenLocal import io.novafoundation.nova.core_db.model.TotalRewardLocal import io.novafoundation.nova.core_db.model.WalletConnectPairingLocal -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal import io.novafoundation.nova.core_db.model.chain.ChainExplorerLocal import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal import io.novafoundation.nova.core_db.model.chain.ChainLocal import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal import io.novafoundation.nova.core_db.model.chain.ChainRuntimeInfoLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ProxyAccountLocal import io.novafoundation.nova.core_db.model.operation.DirectRewardTypeLocal import io.novafoundation.nova.core_db.model.operation.ExtrinsicTypeLocal import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal @@ -133,7 +136,7 @@ import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal @Database( - version = 54, + version = 55, entities = [ AccountLocal::class, NodeLocal::class, @@ -171,6 +174,7 @@ import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal StakingDashboardItemLocal::class, StakingRewardPeriodLocal::class, ExternalBalanceLocal::class, + ProxyAccountLocal::class ], ) @TypeConverters( @@ -185,6 +189,7 @@ import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal ExternalApiConverters::class, ChainConverters::class, ExternalBalanceTypeConverters::class, + ProxyAccountConverters::class ) abstract class AppDatabase : RoomDatabase() { @@ -221,7 +226,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(AddRewardAccountToStakingDashboard_43_44, AddStakingTypeToTotalRewards_44_45, AddExternalBalances_45_46) .addMigrations(AddPoolIdToOperations_46_47, AddEventIdToOperation_47_48, AddSwapOption_48_49) .addMigrations(RefactorOperations_49_50, AddTransactionVersionToRuntime_50_51, AddBalanceModesToAssets_51_52) - .addMigrations(ChangeSessionTopicToParing_52_53, AddConnectionStateToChains_53_54) + .addMigrations(ChangeSessionTopicToParing_52_53, AddConnectionStateToChains_53_54, AddProxyAccount_54_55) .build() } return instance!! diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/MetaAccountTypeConverters.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/MetaAccountTypeConverters.kt index ee18fe7fe2..59ec7e0acc 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/MetaAccountTypeConverters.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/MetaAccountTypeConverters.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.core_db.converters import androidx.room.TypeConverter -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal class MetaAccountTypeConverters { diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ProxyAccountConverters.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ProxyAccountConverters.kt new file mode 100644 index 0000000000..8cfbd4a35f --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/ProxyAccountConverters.kt @@ -0,0 +1,16 @@ +package io.novafoundation.nova.core_db.converters + +import androidx.room.TypeConverter +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal + +class ProxyAccountConverters { + @TypeConverter + fun fromStatusType(type: MetaAccountLocal.Status): String { + return type.name + } + + @TypeConverter + fun toStatusType(name: String): MetaAccountLocal.Status { + return MetaAccountLocal.Status.valueOf(name) + } +} 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 6be18defdf..e9d613fea4 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 @@ -6,10 +6,11 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import androidx.room.Update -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountPositionUpdate -import io.novafoundation.nova.core_db.model.chain.RelationJoinedMetaAccountInfo +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountPositionUpdate +import io.novafoundation.nova.core_db.model.chain.account.ProxyAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.RelationJoinedMetaAccountInfo import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.flow.Flow import org.intellij.lang.annotations.Language @@ -17,32 +18,30 @@ import java.math.BigDecimal import java.math.BigInteger /** - * Fetch meta account where - * accountId = meta.substrateAccountId - * or hex(accountId) = meta.ethereumAddress - * or there is a child chain account which have child.accountId = accountId + * Fetch meta account where either + * 1. chain account for specified chain is present and its accountId matches + * 2. chain account for specified is missing but one of base accountIds matches + * + * Note that if both chain account and base accounts are present than we should filter out entries where chain account matches but base accounts does not */ @Language("RoomSql") private const val FIND_BY_ADDRESS_WHERE_CLAUSE = """ - WHERE substrateAccountId = :accountId - OR ethereumAddress = :accountId - OR id = ( - SELECT id FROM meta_accounts AS m - INNER JOIN chain_accounts as c ON m.id = c.metaId - WHERE c.accountId = :accountId - ) - ORDER BY (CASE WHEN isSelected THEN 0 ELSE 1 END) + LEFT JOIN chain_accounts as c ON m.id = c.metaId + WHERE + (c.chainId = :chainId AND c.accountId IS NOT NULL AND c.accountId = :accountId) + OR (c.accountId IS NULL AND (substrateAccountId = :accountId OR ethereumAddress = :accountId)) + ORDER BY (CASE WHEN isSelected THEN 0 ELSE 1 END) """ @Language("RoomSql") private const val FIND_ACCOUNT_BY_ADDRESS_QUERY = """ - SELECT * FROM meta_accounts + SELECT * FROM meta_accounts as m $FIND_BY_ADDRESS_WHERE_CLAUSE """ @Language("RoomSql") private const val FIND_NAME_BY_ADDRESS_QUERY = """ - SELECT name FROM meta_accounts + SELECT name FROM meta_accounts as m $FIND_BY_ADDRESS_WHERE_CLAUSE """ @@ -77,6 +76,19 @@ private const val META_ACCOUNT_WITH_BALANCE_QUERY = """ @Dao interface MetaAccountDao { + @Transaction + suspend fun insertProxiedMetaAccount( + metaAccount: MetaAccountLocal, + chainAccount: (metaId: Long) -> ChainAccountLocal, + proxyAccount: (metaId: Long) -> ProxyAccountLocal + ): Long { + val metaId = insertMetaAccount(metaAccount) + insertChainAccount(chainAccount(metaId)) + insertProxy(proxyAccount(metaId)) + + return metaId + } + @Insert suspend fun insertMetaAccount(metaAccount: MetaAccountLocal): Long @@ -86,12 +98,18 @@ interface MetaAccountDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertChainAccounts(chainAccounts: List) - @Query("SELECT * FROM meta_accounts") - fun getMetaAccounts(): List + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertProxy(proxyLocal: ProxyAccountLocal) @Query("SELECT * FROM meta_accounts") + suspend fun getMetaAccounts(): List + + @Query("SELECT * FROM meta_accounts WHERE status = :status") @Transaction - suspend fun getJoinedMetaAccountsInfo(): List + suspend fun getMetaAccountsInfoByStatus(status: MetaAccountLocal.Status): List + + @Query("SELECT id FROM meta_accounts WHERE status = :status") + suspend fun getMetaAccountIdsByStatus(status: MetaAccountLocal.Status): List @Query("SELECT * FROM meta_accounts") suspend fun getMetaAccountsInfo(): List @@ -99,12 +117,18 @@ interface MetaAccountDao { @Query("SELECT * FROM meta_accounts") fun getJoinedMetaAccountsInfoFlow(): Flow> + @Query("SELECT * FROM meta_accounts WHERE status = :status") + fun getJoinedMetaAccountsInfoByStatusFlow(status: MetaAccountLocal.Status): Flow> + @Query(META_ACCOUNTS_WITH_BALANCE_QUERY) fun metaAccountsWithBalanceFlow(): Flow> @Query(META_ACCOUNT_WITH_BALANCE_QUERY) fun metaAccountWithBalanceFlow(metaId: Long): Flow> + @Query("SELECT * FROM proxy_accounts WHERE chainId = :chainId") + suspend fun getProxyAccounts(chainId: String): List + @Query("UPDATE meta_accounts SET isSelected = (id = :metaId)") suspend fun selectMetaAccount(metaId: Long) @@ -124,19 +148,19 @@ interface MetaAccountDao { fun metaAccountInfoFlow(metaId: Long): Flow @Query("SELECT EXISTS ($FIND_ACCOUNT_BY_ADDRESS_QUERY)") - fun isMetaAccountExists(accountId: AccountId): Boolean + fun isMetaAccountExists(accountId: AccountId, chainId: String): Boolean @Query(FIND_ACCOUNT_BY_ADDRESS_QUERY) @Transaction - fun getMetaAccountInfo(accountId: AccountId): RelationJoinedMetaAccountInfo? + fun getMetaAccountInfo(accountId: AccountId, chainId: String): RelationJoinedMetaAccountInfo? @Query(FIND_NAME_BY_ADDRESS_QUERY) - fun metaAccountNameFor(accountId: AccountId): String? + fun metaAccountNameFor(accountId: AccountId, chainId: String): String? @Query("UPDATE meta_accounts SET name = :newName WHERE id = :metaId") suspend fun updateName(metaId: Long, newName: String) - @Query("DELETE FROM meta_accounts WHERE id = :metaId") + @Query("DELETE FROM meta_accounts WHERE id = :metaId OR parentMetaId = :metaId") suspend fun delete(metaId: Long) @Query("SELECT COALESCE(MAX(position), 0) + 1 FROM meta_accounts") @@ -157,8 +181,14 @@ interface MetaAccountDao { return metaId } - @Query("SELECT EXISTS(SELECT * FROM meta_accounts)") - suspend fun hasMetaAccounts(): Boolean + @Query("SELECT EXISTS(SELECT * FROM meta_accounts WHERE status = :status)") + suspend fun hasMetaAccountsByStatus(status: MetaAccountLocal.Status): Boolean + + @Query("UPDATE meta_accounts SET status = :status WHERE id IN (:metaIds)") + suspend fun changeAccountsStatus(metaIds: List, status: MetaAccountLocal.Status) + + @Query("DELETE FROM meta_accounts WHERE status = :status ") + fun removeMetaAccountsByStatus(status: MetaAccountLocal.Status) } class MetaAccountWithBalanceLocal( diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/14_15_AddMetaAccountType.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/14_15_AddMetaAccountType.kt index dbc4655f08..64aa2b0bd5 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/14_15_AddMetaAccountType.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/14_15_AddMetaAccountType.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.core_db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import io.novafoundation.nova.core_db.converters.MetaAccountTypeConverters -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal val AddMetaAccountType_14_15 = object : Migration(14, 15) { diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/53_54_AddProxyAccount.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/53_54_AddProxyAccount.kt new file mode 100644 index 0000000000..78474771ac --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/53_54_AddProxyAccount.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.core_db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val AddProxyAccount_54_55 = object : Migration(54, 55) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `proxy_accounts` ( + `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 ) + """.trimMargin() + ) + + database.execSQL("ALTER TABLE `meta_accounts` ADD COLUMN `status` TEXT NOT NULL DEFAULT 'ACTIVE'") + database.execSQL("ALTER TABLE `meta_accounts` ADD COLUMN `parentMetaId` INTEGER") + database.execSQL("ALTER TABLE `chains` ADD COLUMN `supportProxy` INTEGER NOT NULL DEFAULT 0") + } +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceLockLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceLockLocal.kt index 72e401f2b1..d3602fc26c 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceLockLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceLockLocal.kt @@ -4,7 +4,7 @@ import androidx.room.Entity import androidx.room.ForeignKey import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal import io.novafoundation.nova.core_db.model.chain.ChainLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal import java.math.BigInteger @Entity( diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/ExternalBalanceLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/ExternalBalanceLocal.kt index c2206d7d8d..1645496b03 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/ExternalBalanceLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/ExternalBalanceLocal.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.core_db.model import androidx.room.Entity import androidx.room.ForeignKey import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal import java.math.BigInteger @Entity( diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/StakingDashboardItemLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/StakingDashboardItemLocal.kt index a824d3cfcc..b07ba44341 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/StakingDashboardItemLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/StakingDashboardItemLocal.kt @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.ForeignKey import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal import io.novafoundation.nova.core_db.model.chain.ChainLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal import jp.co.soramitsu.fearless_utils.runtime.AccountId import java.math.BigInteger diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/WalletConnectPairingLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/WalletConnectPairingLocal.kt index e6f5723e5c..0035e68b16 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/WalletConnectPairingLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/WalletConnectPairingLocal.kt @@ -4,7 +4,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal @Entity( tableName = "wallet_connect_pairings", 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 55c3553884..e036ea5825 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 @@ -22,6 +22,8 @@ data class ChainLocal( @ColumnInfo(defaultValue = "1") val hasSubstrateRuntime: Boolean, val hasCrowdloans: Boolean, + @ColumnInfo(defaultValue = "0") + val supportProxy: Boolean, val swap: String, val governance: String, val additional: String?, diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ChainAccountLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ChainAccountLocal.kt new file mode 100644 index 0000000000..31abe03ef1 --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ChainAccountLocal.kt @@ -0,0 +1,34 @@ +package io.novafoundation.nova.core_db.model.chain.account + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import io.novafoundation.nova.core.model.CryptoType + +@Entity( + tableName = "chain_accounts", + foreignKeys = [ + // no foreign key for `chainId` since we do not want ChainAccounts to be deleted or modified when chain is deleted + // but rather keep it in db in case future UI will show them somehow + + ForeignKey( + parentColumns = ["id"], + childColumns = ["metaId"], + entity = MetaAccountLocal::class, + onDelete = ForeignKey.CASCADE + ), + ], + indices = [ + Index(value = ["chainId"]), + Index(value = ["metaId"]), + Index(value = ["accountId"]), + ], + primaryKeys = ["metaId", "chainId"] +) +class ChainAccountLocal( + val metaId: Long, + val chainId: String, + val publicKey: ByteArray?, + val accountId: ByteArray, + val cryptoType: CryptoType?, +) diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/JoinedMetaAccountInfo.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/JoinedMetaAccountInfo.kt new file mode 100644 index 0000000000..84ebe2286a --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/JoinedMetaAccountInfo.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.core_db.model.chain.account + +import androidx.room.Embedded +import androidx.room.Relation + +interface JoinedMetaAccountInfo { + + val metaAccount: MetaAccountLocal + + val chainAccounts: List + + val proxyAccountLocal: ProxyAccountLocal? +} + +class RelationJoinedMetaAccountInfo( + @Embedded + override val metaAccount: MetaAccountLocal, + + @Relation(parentColumn = "id", entityColumn = "metaId", entity = ChainAccountLocal::class) + override val chainAccounts: List, + + @Relation(parentColumn = "id", entityColumn = "proxiedMetaId", entity = ProxyAccountLocal::class) + override val proxyAccountLocal: ProxyAccountLocal?, +) : JoinedMetaAccountInfo diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/MetaAccountLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt similarity index 51% rename from core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/MetaAccountLocal.kt rename to core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt index b2cdbfe306..00720cc067 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/MetaAccountLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/MetaAccountLocal.kt @@ -1,11 +1,9 @@ -package io.novafoundation.nova.core_db.model.chain +package io.novafoundation.nova.core_db.model.chain.account -import androidx.room.Embedded +import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey -import androidx.room.Relation import io.novafoundation.nova.core.model.CryptoType @Entity( @@ -22,11 +20,18 @@ class MetaAccountLocal( val ethereumPublicKey: ByteArray?, val ethereumAddress: ByteArray?, val name: String, + val parentMetaId: Long?, val isSelected: Boolean, val position: Int, val type: Type, + @ColumnInfo(defaultValue = "ACTIVE") + val status: Status, ) { + enum class Status { + ACTIVE, DEACTIVATED + } + companion object Table { const val TABLE_NAME = "meta_accounts" @@ -49,53 +54,10 @@ class MetaAccountLocal( var id: Long = 0 enum class Type { - SECRETS, WATCH_ONLY, PARITY_SIGNER, LEDGER, POLKADOT_VAULT + SECRETS, WATCH_ONLY, PARITY_SIGNER, LEDGER, POLKADOT_VAULT, PROXIED } } -@Entity( - tableName = "chain_accounts", - foreignKeys = [ - // no foreign key for `chainId` since we do not want ChainAccounts to be deleted or modified when chain is deleted - // but rather keep it in db in case future UI will show them somehow - - ForeignKey( - parentColumns = ["id"], - childColumns = ["metaId"], - entity = MetaAccountLocal::class, - onDelete = ForeignKey.CASCADE - ), - ], - indices = [ - Index(value = ["chainId"]), - Index(value = ["metaId"]), - Index(value = ["accountId"]), - ], - primaryKeys = ["metaId", "chainId"] -) -class ChainAccountLocal( - val metaId: Long, - val chainId: String, - val publicKey: ByteArray?, - val accountId: ByteArray, - val cryptoType: CryptoType?, -) - -interface JoinedMetaAccountInfo { - - val metaAccount: MetaAccountLocal - - val chainAccounts: List -} - -class RelationJoinedMetaAccountInfo( - @Embedded - override val metaAccount: MetaAccountLocal, - - @Relation(parentColumn = "id", entityColumn = "metaId", entity = ChainAccountLocal::class) - override val chainAccounts: List, -) : JoinedMetaAccountInfo - class MetaAccountPositionUpdate( val id: Long, val position: Int 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 new file mode 100644 index 0000000000..18c62cad67 --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/chain/account/ProxyAccountLocal.kt @@ -0,0 +1,48 @@ +package io.novafoundation.nova.core_db.model.chain.account + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import io.novafoundation.nova.common.utils.Identifiable +import jp.co.soramitsu.fearless_utils.extensions.toHexString + +@Entity( + tableName = "proxy_accounts", + foreignKeys = [ + ForeignKey( + parentColumns = ["id"], + childColumns = ["proxiedMetaId"], + entity = MetaAccountLocal::class, + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + parentColumns = ["id"], + childColumns = ["proxyMetaId"], + entity = MetaAccountLocal::class, + onDelete = ForeignKey.CASCADE + ), + ], + primaryKeys = ["proxyMetaId", "proxiedAccountId", "chainId", "proxyType"] +) +data class ProxyAccountLocal( + val proxiedMetaId: Long, + val proxyMetaId: Long, + val chainId: String, + val proxiedAccountId: ByteArray, + val proxyType: String +) : Identifiable { + + @Ignore + override val identifier: String = makeIdentifier(proxyMetaId, chainId, proxiedAccountId, proxyType) + + companion object { + fun makeIdentifier( + proxyMetaId: Long, + chainId: String, + proxiedAccountId: ByteArray, + proxyType: String + ): String { + return "$proxyMetaId:$chainId:${proxiedAccountId.toHexString()}:$proxyType" + } + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt index 5186b21890..84f9be5fc1 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_account_api.data.ethereum.transaction +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId @@ -23,5 +24,5 @@ interface EvmTransactionService { origin: TransactionOrigin = TransactionOrigin.SelectedWallet, fallbackGasLimit: BigInteger = DefaultGasProvider.GAS_LIMIT, building: EvmTransactionBuilding, - ): Result + ): Result } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/TransactionOrigin.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/TransactionOrigin.kt index 8c03dd2b52..bf22ceebdf 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/TransactionOrigin.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/TransactionOrigin.kt @@ -1,6 +1,17 @@ package io.novafoundation.nova.feature_account_api.data.ethereum.transaction +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import jp.co.soramitsu.fearless_utils.runtime.AccountId + sealed class TransactionOrigin { object SelectedWallet : TransactionOrigin() + + class WalletWithAccount(val accountId: AccountId) : TransactionOrigin() + + class Wallet(val metaAccount: MetaAccount) : TransactionOrigin() } + +fun AccountId.intoOrigin(): TransactionOrigin = TransactionOrigin.WalletWithAccount(this) + +fun MetaAccount.intoOrigin(): TransactionOrigin = TransactionOrigin.Wallet(this) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt index 6077cb208d..c8597e5d99 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt @@ -2,80 +2,86 @@ package io.novafoundation.nova.feature_account_api.data.extrinsic import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder +import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer import kotlinx.coroutines.flow.Flow -import java.math.BigInteger -typealias FormExtrinsicWithOrigin = suspend ExtrinsicBuilder.(origin: AccountId) -> Unit +typealias FormExtrinsicWithOrigin = suspend ExtrinsicBuilder.(origin: SubmissionOrigin) -> Unit -typealias FormMultiExtrinsicWithOrigin = suspend CallBuilder.(origin: AccountId) -> Unit +typealias FormMultiExtrinsicWithOrigin = suspend CallBuilder.(origin: SubmissionOrigin) -> Unit typealias FormMultiExtrinsic = suspend CallBuilder.() -> Unit -typealias ExtrinsicHash = String +class SubmissionOrigin( + /** + * Origin that was originally requested to sign the transaction + */ + val requestedOrigin: AccountId, -class ExtrinsicSubmission(val hash: String, val origin: AccountId) + /** + * Origin that was actually used to sign the transaction. + * It might differ from [requestedOrigin] if [Signer] modified the origin, for example in the case of Proxied wallet + */ + val actualOrigin: AccountId +) { -interface ExtrinsicService { + companion object { - suspend fun submitMultiExtrinsicWithSelectedWalletAwaitingInclusion( - chain: Chain, - formExtrinsic: FormMultiExtrinsicWithOrigin, - ): RetriableMultiResult + fun singleOrigin(origin: AccountId) = SubmissionOrigin(origin, origin) + } +} - @Suppress("DeprecatedCallableAddReplaceWith") - @Deprecated("Use submitExtrinsicWithSelectedWalletV2 instead") - suspend fun submitExtrinsicWithSelectedWallet( - chain: Chain, - formExtrinsic: FormExtrinsicWithOrigin, - ): Result = submitExtrinsicWithSelectedWalletV2(chain, formExtrinsic) - .map { it.hash } +class ExtrinsicSubmission(val hash: String, val submissionOrigin: SubmissionOrigin) - suspend fun submitExtrinsicWithSelectedWalletV2( - chain: Chain, - formExtrinsic: FormExtrinsicWithOrigin, - ): Result +interface ExtrinsicService { - suspend fun submitAndWatchExtrinsicWithSelectedWallet( + suspend fun submitExtrinsic( chain: Chain, + origin: TransactionOrigin, formExtrinsic: FormExtrinsicWithOrigin, - ): Flow + ): Result - suspend fun submitExtrinsicWithAnySuitableWallet( + suspend fun submitAndWatchExtrinsic( chain: Chain, - accountId: ByteArray, + origin: TransactionOrigin, formExtrinsic: FormExtrinsicWithOrigin, - ): Result + ): Result> - suspend fun submitAndWatchExtrinsicAnySuitableWallet( + suspend fun submitMultiExtrinsicAwaitingInclusion( chain: Chain, - accountId: ByteArray, - formExtrinsic: FormExtrinsicWithOrigin, - ): Flow + origin: TransactionOrigin, + formExtrinsic: FormMultiExtrinsicWithOrigin, + ): RetriableMultiResult suspend fun paymentInfo( chain: Chain, + origin: TransactionOrigin, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): FeeResponse suspend fun estimateFee( chain: Chain, - formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, - ): BigInteger - - suspend fun estimateFeeV2( - chain: Chain, + origin: TransactionOrigin, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): Fee + suspend fun zeroFee(chain: Chain, origin: TransactionOrigin): Fee + suspend fun estimateMultiFee( chain: Chain, + origin: TransactionOrigin, formExtrinsic: FormMultiExtrinsic, - ): BigInteger + ): Fee - suspend fun estimateFee(chain: Chain, extrinsic: String): Fee + suspend fun estimateFee( + chain: Chain, + extrinsic: String, + usedSigner: FeeSigner + ): Fee } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt index fb23680fc6..9a7b95da70 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt @@ -1,15 +1,10 @@ package io.novafoundation.nova.feature_account_api.data.extrinsic import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first -suspend fun ExtrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion( - chain: Chain, - formExtrinsic: FormExtrinsicWithOrigin, -): Result = runCatching { - submitAndWatchExtrinsicWithSelectedWallet(chain, formExtrinsic) - .filterIsInstance() - .first() +suspend fun Result>.awaitInBlock(): Result = map { + it.filterIsInstance().first() } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt index 7e014a4a89..f818beb1e5 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_account_api.data.model +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import java.math.BigInteger interface Fee { @@ -7,13 +8,32 @@ interface Fee { companion object val amount: BigInteger + + /** + * Information about origin that is supposed to send the transaction fee was calculated against + */ + val submissionOrigin: SubmissionOrigin } -data class EvmFee(val gasLimit: BigInteger, val gasPrice: BigInteger) : Fee { +data class EvmFee( + val gasLimit: BigInteger, + val gasPrice: BigInteger, + override val submissionOrigin: SubmissionOrigin +) : Fee { override val amount = gasLimit * gasPrice } -@JvmInline -value class InlineFee(override val amount: BigInteger) : Fee +class SubstrateFee( + override val amount: BigInteger, + override val submissionOrigin: SubmissionOrigin +) : Fee + +val Fee.requestedAccountPaysFees: Boolean + get() = submissionOrigin.requestedOrigin.contentEquals(submissionOrigin.actualOrigin) -fun Fee.Companion.zero(): Fee = InlineFee(BigInteger.ZERO) +val Fee.amountByRequestedAccount: BigInteger + get() = if (requestedAccountPaysFees) { + amount + } else { + BigInteger.ZERO + } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/ProxiedWithProxy.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/ProxiedWithProxy.kt new file mode 100644 index 0000000000..887e97763c --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/ProxiedWithProxy.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.feature_account_api.data.model + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class ProxiedWithProxy( + val proxied: Proxied, + val proxy: Proxy +) { + class Proxied( + val accountId: AccountId, + val chainId: ChainId + ) + + class Proxy( + val accountId: AccountId, + val metaId: Long, + val proxyType: String + ) +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/MetaAccountsUpdatesRegistry.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/MetaAccountsUpdatesRegistry.kt new file mode 100644 index 0000000000..817a000c2a --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/MetaAccountsUpdatesRegistry.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.feature_account_api.data.proxy + +import kotlinx.coroutines.flow.Flow + +interface MetaAccountsUpdatesRegistry { + + fun addMetaIds(ids: List) + + fun observeUpdates(): Flow> + + fun getUpdates(): Set + + fun remove(ids: List) + + fun clear() + + fun hasUpdates(): Boolean + + fun observeUpdatesExist(): Flow +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/ProxySyncService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/ProxySyncService.kt new file mode 100644 index 0000000000..aff144b10d --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/ProxySyncService.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_account_api.data.proxy + +import kotlinx.coroutines.flow.Flow + +interface ProxySyncService { + + fun proxySyncTrigger(): Flow<*> + + fun startSyncing() + + suspend fun startSyncingSuspend() +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxiedExtrinsicValidationSystem.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxiedExtrinsicValidationSystem.kt new file mode 100644 index 0000000000..21d8b205d5 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxiedExtrinsicValidationSystem.kt @@ -0,0 +1,31 @@ +package io.novafoundation.nova.feature_account_api.data.proxy.validation + +import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import java.math.BigInteger +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +typealias ProxiedExtrinsicValidationSystem = ValidationSystem +typealias ProxiedExtrinsicValidationSystemBuilder = ValidationSystemBuilder + +class ProxiedExtrinsicValidationPayload( + val proxyMetaAccount: MetaAccount, + val proxyAccountId: AccountId, + val chainWithAsset: ChainWithAsset, + val call: GenericCall.Instance +) + +sealed interface ProxiedExtrinsicValidationFailure { + + class ProxyNotEnoughFee( + val metaAccount: MetaAccount, + val asset: Chain.Asset, + val availableBalance: BigInteger, + val fee: Fee + ) : ProxiedExtrinsicValidationFailure +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxyExtrinsicValidationRequestBus.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxyExtrinsicValidationRequestBus.kt new file mode 100644 index 0000000000..8829b6f2bf --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/proxy/validation/ProxyExtrinsicValidationRequestBus.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_account_api.data.proxy.validation + +import io.novafoundation.nova.common.utils.bus.BaseRequestBus +import io.novafoundation.nova.common.utils.bus.RequestBus +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus.Request +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus.ValidationResponse + +class ProxyExtrinsicValidationRequestBus : BaseRequestBus() { + + class Request(val validationPayload: ProxiedExtrinsicValidationPayload) : RequestBus.Request + + class ValidationResponse(val validationResult: Result>) : RequestBus.Response +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/ProxyRepository.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/ProxyRepository.kt new file mode 100644 index 0000000000..75a614bb0a --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/ProxyRepository.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_account_api.data.repository + +import io.novafoundation.nova.feature_account_api.data.model.ProxiedWithProxy +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountId +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +interface ProxyRepository { + + suspend fun getAllProxiesForMetaAccounts(chainId: ChainId, metaAccountIds: List): List + + suspend fun getDelegatedProxyTypes(chainId: ChainId, proxiedAccountId: AccountId, proxyAccountId: AccountId): List +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/AddAccountRepository.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/AddAccountRepository.kt new file mode 100644 index 0000000000..10ec7e29f4 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/AddAccountRepository.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_account_api.data.repository.addAccount + +interface AddAccountRepository { + + suspend fun addAccount(payload: T): Long +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/BaseAddAccountRepository.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/BaseAddAccountRepository.kt new file mode 100644 index 0000000000..0c3b6a7a6a --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/BaseAddAccountRepository.kt @@ -0,0 +1,16 @@ +package io.novafoundation.nova.feature_account_api.data.repository.addAccount + +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService + +abstract class BaseAddAccountRepository( + private val proxySyncService: ProxySyncService +) : AddAccountRepository { + + final override suspend fun addAccount(payload: T): Long { + val metaId = addAccountInternal(payload) + proxySyncService.startSyncing() + return metaId + } + + protected abstract suspend fun addAccountInternal(payload: T): Long +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/ledger/LedgerAddAccountRepository.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/ledger/LedgerAddAccountRepository.kt new file mode 100644 index 0000000000..db227fa813 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/repository/addAccount/ledger/LedgerAddAccountRepository.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger + +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.BaseAddAccountRepository +import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +abstract class LedgerAddAccountRepository( + private val proxySyncService: ProxySyncService, +) : BaseAddAccountRepository(proxySyncService) { + + sealed interface Payload { + class MetaAccount( + val name: String, + val ledgerChainAccounts: Map + ) : Payload + + class ChainAccount( + val metaId: Long, + val chainId: ChainId, + val ledgerChainAccount: LedgerSubstrateAccount + ) : Payload + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SeparateFlowSignerState.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SeparateFlowSignerState.kt new file mode 100644 index 0000000000..2f5928571e --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SeparateFlowSignerState.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_account_api.data.signer + +import io.novafoundation.nova.common.utils.MutableSharedState +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic + +typealias SigningSharedState = MutableSharedState + +class SeparateFlowSignerState(val extrinsic: SignerPayloadExtrinsic, val metaAccount: MetaAccount) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SignerProvider.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SignerProvider.kt index ce04078543..33d8d3daaf 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SignerProvider.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/signer/SignerProvider.kt @@ -1,12 +1,15 @@ package io.novafoundation.nova.feature_account_api.data.signer import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer interface SignerProvider { - fun signerFor(metaAccount: MetaAccount): Signer + fun rootSignerFor(metaAccount: MetaAccount): NovaSigner - fun feeSigner(chain: Chain): Signer + fun nestedSignerFor(metaAccount: MetaAccount): NovaSigner + + fun feeSigner(metaAccount: MetaAccount, chain: Chain): FeeSigner } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt index dbf88c2ec0..b23085de94 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt @@ -1,12 +1,15 @@ package io.novafoundation.nova.feature_account_api.di -import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory import io.novafoundation.nova.common.sequrity.TwoFactorVerificationExecutor -import io.novafoundation.nova.common.utils.MutableSharedState +import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity @@ -26,7 +29,6 @@ import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInp import io.novafoundation.nova.feature_account_api.presenatation.mixin.identity.IdentityMixin import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserMixin import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletMixin -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic interface AccountFeatureApi { @@ -54,6 +56,8 @@ interface AccountFeatureApi { fun encryptionDefaults(): EncryptionDefaults + fun proxyExtrinsicValidationRequestBus(): ProxyExtrinsicValidationRequestBus + val addressInputMixinFactory: AddressInputMixinFactory val walletUiUseCase: WalletUiUseCase @@ -62,7 +66,7 @@ interface AccountFeatureApi { val watchOnlyMissingKeysPresenter: WatchOnlyMissingKeysPresenter - val signSharedState: MutableSharedState + val signSharedState: SigningSharedState val onChainIdentityRepository: OnChainIdentityRepository @@ -72,6 +76,10 @@ interface AccountFeatureApi { @OnChainIdentity fun onChainIdentityProvider(): IdentityProvider + fun proxySyncService(): ProxySyncService + + fun ledgerAddAccountRepository(): LedgerAddAccountRepository + val evmTransactionService: EvmTransactionService val identityMixinFactory: IdentityMixin.Factory 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 38b45aa0b6..a0aa602edd 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 @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow interface AccountInteractor { - suspend fun getMetaAccounts(): List + suspend fun getActiveMetaAccounts(): List suspend fun generateMnemonic(): Mnemonic @@ -57,4 +57,8 @@ interface AccountInteractor { suspend fun deleteNode(nodeId: Int) suspend fun getChainAddress(metaId: Long, chainId: ChainId): String? + + suspend fun removeDeactivatedMetaAccounts() + + suspend fun switchToNotDeactivatedAccountIfNeeded() } 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 d1af37d507..3975ac8250 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 @@ -4,11 +4,11 @@ import io.novafoundation.nova.core.model.CryptoType import io.novafoundation.nova.core.model.Language import io.novafoundation.nova.core.model.Node import io.novafoundation.nova.feature_account_api.domain.model.Account -import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountAssetBalance import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountOrdering import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.fearless_utils.encrypt.mnemonic.Mnemonic import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.flow.Flow @@ -37,18 +37,16 @@ interface AccountRepository { fun selectedMetaAccountFlow(): Flow - suspend fun findMetaAccount(accountId: ByteArray): MetaAccount? + suspend fun findMetaAccount(accountId: ByteArray, chainId: ChainId): MetaAccount? - suspend fun accountNameFor(accountId: AccountId): String? + suspend fun accountNameFor(accountId: AccountId, chainId: ChainId): String? - suspend fun allMetaAccounts(): List - - suspend fun hasMetaAccounts(): Boolean - - suspend fun allLightMetaAccounts(): List + suspend fun hasActiveMetaAccounts(): Boolean fun allMetaAccountsFlow(): Flow> + fun activeMetaAccountsFlow(): Flow> + fun metaAccountBalancesFlow(): Flow> fun metaAccountBalancesFlow(metaId: Long): Flow> @@ -119,5 +117,9 @@ interface AccountRepository { password: String ): String - suspend fun isAccountExists(accountId: AccountId): Boolean + suspend fun isAccountExists(accountId: AccountId, chainId: ChainId): Boolean + + suspend fun removeDeactivatedMetaAccounts() + + suspend fun getActiveMetaAccounts(): List } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepositoryExt.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepositoryExt.kt index 09f0001dd7..a9c3196f6e 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepositoryExt.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/AccountRepositoryExt.kt @@ -1,12 +1,15 @@ package io.novafoundation.nova.feature_account_api.domain.interfaces +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.fearless_utils.extensions.toHexString import jp.co.soramitsu.fearless_utils.runtime.AccountId -suspend fun AccountRepository.findMetaAccountOrThrow(accountId: AccountId) = findMetaAccount(accountId) +suspend fun AccountRepository.findMetaAccountOrThrow(accountId: AccountId, chainId: ChainId) = findMetaAccount(accountId, chainId) ?: error("No meta account found for accountId: ${accountId.toHexString()}") suspend fun AccountRepository.requireIdOfSelectedMetaAccountIn(chain: Chain): AccountId { @@ -20,3 +23,11 @@ suspend fun AccountRepository.getIdOfSelectedMetaAccountIn(chain: Chain): Accoun return metaAccount.accountIdIn(chain) } + +suspend fun AccountRepository.requireMetaAccountFor(transactionOrigin: TransactionOrigin, chainId: ChainId): MetaAccount { + return when (transactionOrigin) { + TransactionOrigin.SelectedWallet -> getSelectedMetaAccount() + is TransactionOrigin.WalletWithAccount -> findMetaAccountOrThrow(transactionOrigin.accountId, chainId) + is TransactionOrigin.Wallet -> transactionOrigin.metaAccount + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/MetaAccountGroupingInteractor.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/MetaAccountGroupingInteractor.kt index 65acaa23cd..9ee001d004 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/MetaAccountGroupingInteractor.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/MetaAccountGroupingInteractor.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.list.GroupedList import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountWithTotalBalance +import io.novafoundation.nova.feature_account_api.domain.model.ProxiedAndProxyMetaAccount import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import kotlinx.coroutines.flow.Flow @@ -15,5 +16,7 @@ interface MetaAccountGroupingInteractor { fun getMetaAccountsForTransaction(fromId: ChainId, destinationId: ChainId): Flow> + fun updatedProxieds(): Flow> + suspend fun hasAvailableMetaAccountsForDestination(fromId: ChainId, destinationId: ChainId): Boolean } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/SelectedAccountUseCase.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/SelectedAccountUseCase.kt index cf0eb4b4d9..5ae85f9fbd 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/SelectedAccountUseCase.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/interfaces/SelectedAccountUseCase.kt @@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.feature_account_api.R +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.asPolkadotVaultVariantOrThrow @@ -12,12 +13,14 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.polkadot import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map class SelectedWalletModel( @DrawableRes val typeIcon: Int?, val walletIcon: Drawable, val name: String, + val hasUpdates: Boolean, ) class SelectedAccountUseCase( @@ -25,6 +28,7 @@ class SelectedAccountUseCase( private val walletUiUseCase: WalletUiUseCase, private val addressIconGenerator: AddressIconGenerator, private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + private val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry ) { fun selectedMetaAccountFlow(): Flow = accountRepository.selectedMetaAccountFlow() @@ -37,23 +41,29 @@ class SelectedAccountUseCase( ) } - fun selectedWalletModelFlow(): Flow = selectedMetaAccountFlow().map { - val icon = walletUiUseCase.walletIcon(it, transparentBackground = false) + fun selectedWalletModelFlow(): Flow = combine( + selectedMetaAccountFlow(), + metaAccountsUpdatesRegistry.observeUpdatesExist() + ) { metaAccount, hasMetaAccountsUpdates -> + val icon = walletUiUseCase.walletIcon(metaAccount, transparentBackground = false) - val typeIcon = when (val type = it.type) { + val typeIcon = when (val type = metaAccount.type) { LightMetaAccount.Type.SECRETS -> null // no icon for secrets account LightMetaAccount.Type.WATCH_ONLY -> R.drawable.ic_watch_only_filled LightMetaAccount.Type.PARITY_SIGNER, LightMetaAccount.Type.POLKADOT_VAULT -> { val config = polkadotVaultVariantConfigProvider.variantConfigFor(type.asPolkadotVaultVariantOrThrow()) config.common.iconRes } + LightMetaAccount.Type.LEDGER -> R.drawable.ic_ledger + LightMetaAccount.Type.PROXIED -> R.drawable.ic_proxy } SelectedWalletModel( typeIcon = typeIcon, walletIcon = icon, - name = it.name + name = metaAccount.name, + hasUpdates = hasMetaAccountsUpdates ) } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt index c69e19405c..9b22a3908d 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt @@ -29,9 +29,14 @@ interface LightMetaAccount { val isSelected: Boolean val name: String val type: Type + val status: LightMetaAccount.Status enum class Type { - SECRETS, WATCH_ONLY, PARITY_SIGNER, LEDGER, POLKADOT_VAULT + SECRETS, WATCH_ONLY, PARITY_SIGNER, LEDGER, POLKADOT_VAULT, PROXIED + } + + enum class Status { + ACTIVE, DEACTIVATED } } @@ -45,6 +50,7 @@ fun LightMetaAccount( isSelected: Boolean, name: String, type: LightMetaAccount.Type, + status: LightMetaAccount.Status ) = object : LightMetaAccount { override val id: Long = id override val substratePublicKey: ByteArray? = substratePublicKey @@ -55,11 +61,13 @@ fun LightMetaAccount( override val isSelected: Boolean = isSelected override val name: String = name override val type: LightMetaAccount.Type = type + override val status: LightMetaAccount.Status = status } class MetaAccount( override val id: Long, val chainAccounts: Map, + val proxy: ProxyAccount?, override val substratePublicKey: ByteArray?, override val substrateCryptoType: CryptoType?, override val substrateAccountId: ByteArray?, @@ -68,6 +76,7 @@ class MetaAccount( override val isSelected: Boolean, override val name: String, override val type: LightMetaAccount.Type, + override val status: LightMetaAccount.Status ) : LightMetaAccount { class ChainAccount( @@ -141,6 +150,7 @@ fun MetaAccount.multiChainEncryptionIn(chain: Chain): MultiChainEncryption? { MultiChainEncryption.substrateFrom(cryptoType) } } + chain.isEthereumBased -> MultiChainEncryption.Ethereum else -> substrateCryptoType?.let(MultiChainEncryption.Companion::substrateFrom) } @@ -175,10 +185,6 @@ private fun MultiChainEncryption.Companion.substrateFrom(cryptoType: CryptoType) fun MetaAccount.chainAccountFor(chainId: ChainId) = chainAccounts.getValue(chainId) -fun LightMetaAccount.Type.isPolkadotVaultLike(): Boolean { - return asPolkadotVaultVariantOrNull() != null -} - fun LightMetaAccount.Type.asPolkadotVaultVariantOrNull(): PolkadotVaultVariant? { return when (this) { LightMetaAccount.Type.PARITY_SIGNER -> PolkadotVaultVariant.PARITY_SIGNER @@ -192,3 +198,15 @@ fun LightMetaAccount.Type.asPolkadotVaultVariantOrThrow(): PolkadotVaultVariant "Not a Polkadot Vault compatible account type" } } + +fun LightMetaAccount.Type.requestedAccountPaysFees(): Boolean { + return when (this) { + LightMetaAccount.Type.SECRETS, + LightMetaAccount.Type.WATCH_ONLY, + LightMetaAccount.Type.PARITY_SIGNER, + LightMetaAccount.Type.LEDGER, + LightMetaAccount.Type.POLKADOT_VAULT -> true + + LightMetaAccount.Type.PROXIED -> false + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountAssetBalance.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountAssetBalance.kt index 60752bc1d6..e0c19daf65 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountAssetBalance.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountAssetBalance.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_account_api.domain.model import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal import java.math.BigInteger @@ -15,6 +16,9 @@ class MetaAccountAssetBalance( class MetaAccountWithTotalBalance( val metaAccount: MetaAccount, + val proxyMetaAccount: MetaAccount?, + val proxyChain: Chain?, val totalBalance: BigDecimal, val currency: Currency, + val hasUpdates: Boolean, ) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountId.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountId.kt new file mode 100644 index 0000000000..24fbb19f44 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccountId.kt @@ -0,0 +1,5 @@ +package io.novafoundation.nova.feature_account_api.domain.model + +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class MetaAccountId(val accountId: AccountId, val metaId: Long) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxiedAndProxyMetaAccount.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxiedAndProxyMetaAccount.kt new file mode 100644 index 0000000000..039c0369a7 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxiedAndProxyMetaAccount.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_account_api.domain.model + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class ProxiedAndProxyMetaAccount( + val proxied: MetaAccount, + val proxy: MetaAccount, + val chain: Chain +) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxyAccount.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxyAccount.kt new file mode 100644 index 0000000000..4eb5e0c4a6 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/ProxyAccount.kt @@ -0,0 +1,32 @@ +package io.novafoundation.nova.feature_account_api.domain.model + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +class ProxyAccount( + val metaId: Long, + val chainId: ChainId, + val proxiedAccountId: ByteArray, + val proxyType: ProxyType, +) { + + sealed class ProxyType(val name: String) { + + object Any : ProxyType("Any") + + object NonTransfer : ProxyType("NonTransfer") + + object Governance : ProxyType("Governance") + + object Staking : ProxyType("Staking") + + object IdentityJudgement : ProxyType("IdentityJudgement") + + object CancelProxy : ProxyType("CancelProxy") + + object Auction : ProxyType("Auction") + + object NominationPools : ProxyType("NominationPools") + + class Other(name: String) : ProxyType(name) + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/AddressDisplayUseCase.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/AddressDisplayUseCase.kt index 0b3fa03602..6d87f876db 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/AddressDisplayUseCase.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/AddressDisplayUseCase.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_account_api.presenatation.account import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -18,14 +19,8 @@ class AddressDisplayUseCase( } } - @Deprecated("Does not work with Meta Accounts. Use `invoke(chain: Chain, address: String)` instead") - // TODO remove - suspend operator fun invoke(address: String): String? = withContext(Dispatchers.Default) { - accountRepository.getAccountOrNull(address)?.name - } - - suspend operator fun invoke(accountId: AccountId): String? = withContext(Dispatchers.Default) { - accountRepository.findMetaAccount(accountId)?.name + suspend operator fun invoke(accountId: AccountId, chainId: ChainId): String? = withContext(Dispatchers.Default) { + accountRepository.findMetaAccount(accountId, chainId)?.name } suspend fun createIdentifier(): Identifier = withContext(Dispatchers.Default) { @@ -39,5 +34,5 @@ class AddressDisplayUseCase( } suspend operator fun AddressDisplayUseCase.invoke(chain: Chain, address: String): String? { - return runCatching { invoke(chain.accountIdOf(address)) }.getOrNull() + return runCatching { invoke(chain.accountIdOf(address), chain.id) }.getOrNull() } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/icon/AddressIconGeneratorExt.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/icon/AddressIconGeneratorExt.kt index 289c6ff373..76bd92e651 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/icon/AddressIconGeneratorExt.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/icon/AddressIconGeneratorExt.kt @@ -126,7 +126,7 @@ suspend fun AddressIconGenerator.createAccountAddressModel( chain: Chain, accountId: AccountId, addressDisplayUseCase: AddressDisplayUseCase, -) = createAccountAddressModel(chain, accountId, addressDisplayUseCase.invoke(accountId)) +) = createAccountAddressModel(chain, accountId, addressDisplayUseCase.invoke(accountId, chain.id)) suspend fun AddressIconGenerator.createIdentityAddressModel( chain: Chain, diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountListAdapter.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountListAdapter.kt index 2ab6f08369..a7b3f6c222 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountListAdapter.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountListAdapter.kt @@ -1,207 +1,23 @@ package io.novafoundation.nova.feature_account_api.presenatation.account.listing -import android.animation.LayoutTransition -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import androidx.core.view.isVisible -import io.novafoundation.nova.common.list.BaseGroupedDiffCallback -import io.novafoundation.nova.common.list.GroupedListAdapter -import io.novafoundation.nova.common.list.GroupedListHolder -import io.novafoundation.nova.common.list.PayloadGenerator -import io.novafoundation.nova.common.list.resolvePayload -import io.novafoundation.nova.common.utils.dp -import io.novafoundation.nova.common.utils.inflateChild -import io.novafoundation.nova.common.utils.setDrawableStart -import io.novafoundation.nova.common.view.ChipLabelModel +import androidx.annotation.ColorRes +import coil.ImageLoader import io.novafoundation.nova.common.view.ChipLabelView -import io.novafoundation.nova.feature_account_api.R -import kotlinx.android.synthetic.main.item_account.view.itemAccountArrow -import kotlinx.android.synthetic.main.item_account.view.itemAccountCheck -import kotlinx.android.synthetic.main.item_account.view.itemAccountContainer -import kotlinx.android.synthetic.main.item_account.view.itemAccountDelete -import kotlinx.android.synthetic.main.item_account.view.itemAccountIcon -import kotlinx.android.synthetic.main.item_account.view.itemAccountSubtitle -import kotlinx.android.synthetic.main.item_account.view.itemAccountTitle +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountChipHolder +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountChipGroupRvItem class AccountsAdapter( - private val accountItemHandler: AccountItemHandler, - initialMode: Mode -) : GroupedListAdapter(DiffCallback()) { - - private var mode: Mode = initialMode - - enum class Mode { - VIEW, EDIT, SWITCH - } - - interface AccountItemHandler { - - fun itemClicked(accountModel: AccountUi) - - fun deleteClicked(accountModel: AccountUi) { - // default no op - } - } - - fun setMode(mode: Mode) { - this.mode = mode - - notifyItemRangeChanged(0, itemCount, mode) - } - - override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder { - val view = ChipLabelView(parent.context) - - return AccountTypeHolder(view) - } - - override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder { - return AccountHolder(parent.inflateChild(R.layout.item_account)) - } - - override fun bindGroup(holder: GroupedListHolder, group: ChipLabelModel) { - (holder as AccountTypeHolder).bind(group) - } - - override fun bindChild(holder: GroupedListHolder, child: AccountUi) { - (holder as AccountHolder).bind(mode, child, accountItemHandler) - } - - override fun bindChild(holder: GroupedListHolder, position: Int, child: AccountUi, payloads: List) { - require(holder is AccountHolder) - - resolvePayload( - holder, - position, - payloads, - onUnknownPayload = { holder.bindMode(mode, child, accountItemHandler) }, - onDiffCheck = { - when (it) { - AccountUi::title -> holder.bindName(child) - AccountUi::subtitle -> holder.bindSubtitle(child) - AccountUi::isSelected -> holder.bindMode(mode, child, accountItemHandler) - } - } - ) - } -} - -class AccountTypeHolder(override val containerView: ChipLabelView) : GroupedListHolder(containerView) { - - init { - val context = containerView.context - - containerView.layoutParams = ViewGroup.MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(16.dp(context), 16.dp(context), 0, 8.dp(context)) - } - } - - fun bind(item: ChipLabelModel) { - containerView.setModel(item) - } -} - -class AccountHolder(view: View) : GroupedListHolder(view) { - - init { - val lt = LayoutTransition().apply { - disableTransitionType(LayoutTransition.DISAPPEARING) - disableTransitionType(LayoutTransition.APPEARING) - } - - containerView.itemAccountContainer.layoutTransition = lt - } - - fun bind( - mode: AccountsAdapter.Mode, - accountModel: AccountUi, - handler: AccountsAdapter.AccountItemHandler, - ) = with(containerView) { - bindName(accountModel) - bindSubtitle(accountModel) - bindMode(mode, accountModel, handler) - - itemAccountIcon.setImageDrawable(accountModel.picture) - } - - fun bindName(accountModel: AccountUi) { - containerView.itemAccountTitle.text = accountModel.title - } - - fun bindSubtitle(accountModel: AccountUi) { - containerView.itemAccountSubtitle.text = accountModel.subtitle - containerView.itemAccountSubtitle.setDrawableStart(accountModel.subtitleIconRes, paddingInDp = 4) - } - - fun bindMode( - mode: AccountsAdapter.Mode, - accountModel: AccountUi, - handler: AccountsAdapter.AccountItemHandler, - ) = with(containerView) { - when (mode) { - AccountsAdapter.Mode.VIEW -> { - itemAccountArrow.visibility = View.VISIBLE - - itemAccountDelete.visibility = View.GONE - itemAccountDelete.setOnClickListener(null) - - itemAccountCheck.visibility = View.GONE - - setOnClickListener { handler.itemClicked(accountModel) } - } - - AccountsAdapter.Mode.EDIT -> { - itemAccountArrow.visibility = View.INVISIBLE - - itemAccountDelete.visibility = View.VISIBLE - itemAccountDelete.setOnClickListener { handler.deleteClicked(accountModel) } - itemAccountDelete.setImageResource(R.drawable.ic_delete_symbol) - - itemAccountCheck.visibility = View.GONE - - setOnClickListener(null) - } - - AccountsAdapter.Mode.SWITCH -> { - itemAccountArrow.visibility = View.GONE - - itemAccountDelete.visibility = View.GONE - - itemAccountCheck.isVisible = accountModel.isClickable - - itemAccountCheck.isChecked = accountModel.isSelected - - setOnClickListener { handler.itemClicked(accountModel) } - } - } - } -} - -private object MetaAccountPayloadGenerator : PayloadGenerator( - AccountUi::title, - AccountUi::subtitle, - AccountUi::isSelected + private val accountItemHandler: AccountHolder.AccountItemHandler, + private val imageLoader: ImageLoader, + @ColorRes private val chainBorderColor: Int, + initialMode: AccountHolder.Mode +) : CommonAccountsAdapter( + accountItemHandler = accountItemHandler, + imageLoader = imageLoader, + diffCallback = AccountDiffCallback(AccountChipGroupRvItem::class.java), + groupFactory = { AccountChipHolder(ChipLabelView(it.context)) }, + groupBinder = { holder, item -> (holder as AccountChipHolder).bind(item) }, + chainBorderColor = chainBorderColor, + initialMode = initialMode ) - -private class DiffCallback : BaseGroupedDiffCallback(ChipLabelModel::class.java) { - override fun areGroupItemsTheSame(oldItem: ChipLabelModel, newItem: ChipLabelModel): Boolean { - return oldItem.title == newItem.title - } - - override fun areGroupContentsTheSame(oldItem: ChipLabelModel, newItem: ChipLabelModel): Boolean { - return oldItem.iconRes == newItem.iconRes - } - - override fun areChildItemsTheSame(oldItem: AccountUi, newItem: AccountUi): Boolean { - return oldItem.id == newItem.id - } - - override fun areChildContentsTheSame(oldItem: AccountUi, newItem: AccountUi): Boolean { - return oldItem.title == newItem.title && oldItem.subtitle == newItem.subtitle && oldItem.isSelected == newItem.isSelected - } - - override fun getChildChangePayload(oldItem: AccountUi, newItem: AccountUi): Any? { - return MetaAccountPayloadGenerator.diff(oldItem, newItem) - } -} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountUi.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountUi.kt deleted file mode 100644 index 7f3490f42c..0000000000 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/AccountUi.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.novafoundation.nova.feature_account_api.presenatation.account.listing - -import android.graphics.drawable.Drawable - -class AccountUi( - val id: Long, - val title: String, - val subtitle: String, - val isSelected: Boolean, - val isClickable: Boolean, - val picture: Drawable, - val subtitleIconRes: Int? -) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/CommonAccountsAdapter.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/CommonAccountsAdapter.kt new file mode 100644 index 0000000000..2c97d98b32 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/CommonAccountsAdapter.kt @@ -0,0 +1,100 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing + +import android.view.ViewGroup +import androidx.annotation.ColorRes +import coil.ImageLoader +import io.novafoundation.nova.common.list.BaseGroupedDiffCallback +import io.novafoundation.nova.common.list.GroupedListAdapter +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.list.resolvePayload +import io.novafoundation.nova.common.utils.inflateChild +import io.novafoundation.nova.feature_account_api.R +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi + +fun interface AccountGroupViewHolderFactory { + fun create(parent: ViewGroup): GroupedListHolder +} + +fun interface AccountGroupViewHolderBinder { + fun bind(holder: GroupedListHolder, item: Group) +} + +interface AccountGroupRvItem { + fun isItemTheSame(other: AccountGroupRvItem): Boolean +} + +abstract class CommonAccountsAdapter( + private val accountItemHandler: AccountHolder.AccountItemHandler?, + private val imageLoader: ImageLoader, + private val diffCallback: AccountDiffCallback, + private val groupFactory: AccountGroupViewHolderFactory, + private val groupBinder: AccountGroupViewHolderBinder, + @ColorRes private val chainBorderColor: Int, + initialMode: AccountHolder.Mode, +) : GroupedListAdapter(diffCallback) { + + private var mode: AccountHolder.Mode = initialMode + + fun setMode(mode: AccountHolder.Mode) { + this.mode = mode + + notifyItemRangeChanged(0, itemCount, mode) + } + + override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder { + return groupFactory.create(parent) + } + + override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder { + return AccountHolder(parent.inflateChild(R.layout.item_account), imageLoader, chainBorderColor) + } + + override fun bindGroup(holder: GroupedListHolder, group: Group) { + groupBinder.bind(holder, group) + } + + override fun bindChild(holder: GroupedListHolder, child: AccountUi) { + (holder as AccountHolder).bind(mode, child, accountItemHandler) + } + + override fun bindChild(holder: GroupedListHolder, position: Int, child: AccountUi, payloads: List) { + require(holder is AccountHolder) + + resolvePayload( + holder, + position, + payloads, + onUnknownPayload = { holder.bindMode(mode, child, accountItemHandler) }, + onDiffCheck = { + when (it) { + AccountUi::title -> holder.bindName(child) + AccountUi::subtitle -> holder.bindSubtitle(child) + AccountUi::isSelected -> holder.bindMode(mode, child, accountItemHandler) + } + } + ) + } +} + +class AccountDiffCallback(groupClass: Class) : BaseGroupedDiffCallback(groupClass) { + override fun areGroupItemsTheSame(oldItem: Group, newItem: Group): Boolean { + return oldItem.isItemTheSame(newItem) + } + + override fun areGroupContentsTheSame(oldItem: Group, newItem: Group): Boolean { + return oldItem == newItem + } + + override fun areChildItemsTheSame(oldItem: AccountUi, newItem: AccountUi): Boolean { + return oldItem.id == newItem.id + } + + override fun areChildContentsTheSame(oldItem: AccountUi, newItem: AccountUi): Boolean { + return oldItem.title == newItem.title && oldItem.subtitle == newItem.subtitle && oldItem.isSelected == newItem.isSelected + } + + override fun getChildChangePayload(oldItem: AccountUi, newItem: AccountUi): Any? { + return MetaAccountPayloadGenerator.diff(oldItem, newItem) + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/MetaAccountPayloadGenerator.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/MetaAccountPayloadGenerator.kt new file mode 100644 index 0000000000..0db9601a13 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/MetaAccountPayloadGenerator.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing + +import io.novafoundation.nova.common.list.PayloadGenerator +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi + +object MetaAccountPayloadGenerator : PayloadGenerator( + AccountUi::title, + AccountUi::subtitle, + AccountUi::isSelected +) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountChipHolder.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountChipHolder.kt new file mode 100644 index 0000000000..d7e9ddbef8 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountChipHolder.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders + +import android.view.ViewGroup +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.view.ChipLabelView +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountChipGroupRvItem + +class AccountChipHolder(override val containerView: ChipLabelView) : GroupedListHolder(containerView) { + + init { + val context = containerView.context + + containerView.layoutParams = ViewGroup.MarginLayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(16.dp(context), 16.dp(context), 0, 8.dp(context)) + } + } + + fun bind(item: AccountChipGroupRvItem) { + containerView.setModel(item.chipLabelModel) + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountHolder.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountHolder.kt new file mode 100644 index 0000000000..46f1f3dde1 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountHolder.kt @@ -0,0 +1,134 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders + +import android.animation.LayoutTransition +import android.view.View +import androidx.annotation.ColorRes +import androidx.core.view.isVisible +import coil.ImageLoader +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.AlphaColorFilter +import io.novafoundation.nova.common.utils.letOrHide +import io.novafoundation.nova.common.utils.setDrawableEnd +import io.novafoundation.nova.common.utils.setDrawableStart +import io.novafoundation.nova.feature_account_api.R +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon +import kotlinx.android.synthetic.main.item_account.view.itemAccountArrow +import kotlinx.android.synthetic.main.item_account.view.itemAccountCheck +import kotlinx.android.synthetic.main.item_account.view.itemAccountContainer +import kotlinx.android.synthetic.main.item_account.view.itemAccountDelete +import kotlinx.android.synthetic.main.item_account.view.itemAccountIcon +import kotlinx.android.synthetic.main.item_account.view.itemAccountSubtitle +import kotlinx.android.synthetic.main.item_account.view.itemAccountTitle +import kotlinx.android.synthetic.main.item_account.view.itemChainIcon + +class AccountHolder(view: View, private val imageLoader: ImageLoader, @ColorRes private val chainBorderColor: Int) : GroupedListHolder(view) { + + interface AccountItemHandler { + + fun itemClicked(accountModel: AccountUi) + + fun deleteClicked(accountModel: AccountUi) { + // default no op + } + } + + enum class Mode { + VIEW, SELECT, EDIT, SWITCH + } + + init { + val lt = LayoutTransition().apply { + disableTransitionType(LayoutTransition.DISAPPEARING) + disableTransitionType(LayoutTransition.APPEARING) + } + + containerView.itemAccountContainer.layoutTransition = lt + containerView.itemChainIcon.backgroundTintList = containerView.context.getColorStateList(chainBorderColor) + } + + fun bind( + mode: Mode, + accountModel: AccountUi, + handler: AccountItemHandler?, + ) = with(containerView) { + bindName(accountModel) + bindSubtitle(accountModel) + bindMode(mode, accountModel, handler) + + itemAccountIcon.setImageDrawable(accountModel.picture) + itemChainIcon.letOrHide(accountModel.chainIconUrl) { + itemChainIcon.colorFilter = AlphaColorFilter(accountModel.chainIconOpacity) + itemChainIcon.loadChainIcon(it, imageLoader = imageLoader) + } + + if (accountModel.updateIndicator) { + itemAccountTitle.setDrawableEnd(R.drawable.shape_account_updated_indicator, paddingInDp = 8) + } else { + itemAccountTitle.setDrawableEnd(null) + } + } + + fun bindName(accountModel: AccountUi) { + containerView.itemAccountTitle.text = accountModel.title + } + + fun bindSubtitle(accountModel: AccountUi) { + containerView.itemAccountSubtitle.text = accountModel.subtitle + containerView.itemAccountSubtitle.setDrawableStart(accountModel.subtitleIconRes, paddingInDp = 4) + } + + fun bindMode( + mode: Mode, + accountModel: AccountUi, + handler: AccountItemHandler?, + ) = with(containerView) { + when (mode) { + Mode.VIEW -> { + itemAccountArrow.visibility = View.GONE + itemAccountDelete.visibility = View.GONE + itemAccountCheck.visibility = View.GONE + + itemAccountDelete.setOnClickListener(null) + + setOnClickListener(null) + } + + Mode.SELECT -> { + itemAccountArrow.visibility = View.VISIBLE + + itemAccountDelete.visibility = View.GONE + itemAccountDelete.setOnClickListener(null) + + itemAccountCheck.visibility = View.GONE + + setOnClickListener { handler?.itemClicked(accountModel) } + } + + Mode.EDIT -> { + itemAccountArrow.visibility = View.INVISIBLE + + itemAccountDelete.isVisible = accountModel.isEditable + + itemAccountDelete.setOnClickListener { handler?.deleteClicked(accountModel) } + itemAccountDelete.setImageResource(R.drawable.ic_delete_symbol) + + itemAccountCheck.visibility = View.GONE + + setOnClickListener(null) + } + + Mode.SWITCH -> { + itemAccountArrow.visibility = View.GONE + + itemAccountDelete.visibility = View.GONE + + itemAccountCheck.isVisible = accountModel.isClickable + + itemAccountCheck.isChecked = accountModel.isSelected + + setOnClickListener { handler?.itemClicked(accountModel) } + } + } + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountTitleHolder.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountTitleHolder.kt new file mode 100644 index 0000000000..77eed94606 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/holders/AccountTitleHolder.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders + +import android.view.View +import io.novafoundation.nova.common.list.GroupedListHolder +import kotlinx.android.synthetic.main.item_delegated_account_group.view.delegatedAccountGroup +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountTitleGroupRvItem + +class AccountTitleHolder(override val containerView: View) : GroupedListHolder(containerView) { + + fun bind(item: AccountTitleGroupRvItem) { + containerView.delegatedAccountGroup.text = item.title + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountChipGroupRvItem.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountChipGroupRvItem.kt new file mode 100644 index 0000000000..124ffa50cb --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountChipGroupRvItem.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing.items + +import io.novafoundation.nova.common.view.ChipLabelModel +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountGroupRvItem + +data class AccountChipGroupRvItem( + val chipLabelModel: ChipLabelModel +) : AccountGroupRvItem { + override fun isItemTheSame(other: AccountGroupRvItem): Boolean { + return other is AccountChipGroupRvItem && chipLabelModel.title == other.chipLabelModel.title + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountTitleGroupRvItem.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountTitleGroupRvItem.kt new file mode 100644 index 0000000000..f6cae308c1 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountTitleGroupRvItem.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing.items + +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountGroupRvItem + +data class AccountTitleGroupRvItem( + val title: String +) : AccountGroupRvItem { + override fun isItemTheSame(other: AccountGroupRvItem): Boolean { + return other is AccountTitleGroupRvItem && title == other.title + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountUi.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountUi.kt new file mode 100644 index 0000000000..7f17c15877 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/listing/items/AccountUi.kt @@ -0,0 +1,17 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.listing.items + +import android.graphics.drawable.Drawable + +class AccountUi( + val id: Long, + val title: CharSequence, + val subtitle: CharSequence, + val isSelected: Boolean, + val isEditable: Boolean, + val isClickable: Boolean, + val picture: Drawable, + val chainIconUrl: String?, + val updateIndicator: Boolean, + val subtitleIconRes: Int?, + val chainIconOpacity: Float = 1f +) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/proxy/ProxySigningPresenter.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/proxy/ProxySigningPresenter.kt new file mode 100644 index 0000000000..98f8682d84 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/proxy/ProxySigningPresenter.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_account_api.presenatation.account.proxy + +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import java.math.BigInteger + +interface ProxySigningPresenter { + + suspend fun acknowledgeProxyOperation(proxiedMetaAccount: MetaAccount, proxyMetaAccount: MetaAccount): Boolean + + suspend fun notEnoughPermission(proxiedMetaAccount: MetaAccount, proxyMetaAccount: MetaAccount, proxyTypes: List) + + suspend fun signingIsNotSupported() + + suspend fun notEnoughFee(metaAccount: MetaAccount, chainAsset: Chain.Asset, availableBalance: BigInteger, fee: Fee) +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/wallet/WalletUiUseCase.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/wallet/WalletUiUseCase.kt index 3f32029636..946eaab081 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/wallet/WalletUiUseCase.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/account/wallet/WalletUiUseCase.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_account_api.presenatation.account.wallet import android.graphics.drawable.Drawable +import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import kotlinx.coroutines.flow.Flow @@ -19,7 +20,7 @@ interface WalletUiUseCase { suspend fun selectedWalletUi(): WalletModel - suspend fun walletIcon(metaAccount: MetaAccount, transparentBackground: Boolean = true): Drawable + suspend fun walletIcon(metaAccount: MetaAccount, iconSize: Int = AddressIconGenerator.SIZE_MEDIUM, transparentBackground: Boolean = true): Drawable suspend fun walletUiFor(metaAccount: MetaAccount): WalletModel } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/view/SelectedWalletView.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/view/SelectedWalletView.kt index 72a65df8cb..d09d81495f 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/view/SelectedWalletView.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/view/SelectedWalletView.kt @@ -3,30 +3,32 @@ package io.novafoundation.nova.feature_account_api.view import android.content.Context import android.util.AttributeSet import android.view.View -import android.widget.LinearLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible import io.novafoundation.nova.common.utils.makeGone import io.novafoundation.nova.common.utils.makeVisible import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable import io.novafoundation.nova.feature_account_api.R import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedWalletModel import kotlinx.android.synthetic.main.view_selected_wallet.view.viewSelectedWalletAccountIcon +import kotlinx.android.synthetic.main.view_selected_wallet.view.viewSelectedWalletAccountUpdateIndicator import kotlinx.android.synthetic.main.view_selected_wallet.view.viewSelectedWalletTypeIcon class SelectedWalletView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr) { init { View.inflate(context, R.layout.view_selected_wallet, this) - - orientation = HORIZONTAL } fun setModel(model: SelectedWalletModel) { viewSelectedWalletAccountIcon.setImageDrawable(model.walletIcon) + viewSelectedWalletAccountUpdateIndicator.isVisible = model.hasUpdates + if (model.typeIcon != null) { background = context.getRoundedCornerDrawable( fillColorRes = R.color.chips_background, diff --git a/feature-account-api/src/main/res/layout/item_account.xml b/feature-account-api/src/main/res/layout/item_account.xml index 7cf2e38d37..b79dd2a681 100644 --- a/feature-account-api/src/main/res/layout/item_account.xml +++ b/feature-account-api/src/main/res/layout/item_account.xml @@ -6,7 +6,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" - android:background="@drawable/bg_primary_list_item"> + android:background="@drawable/bg_primary_list_item" + tools:background="@color/secondary_screen_background"> + + + \ No newline at end of file diff --git a/feature-account-api/src/main/res/layout/view_selected_wallet.xml b/feature-account-api/src/main/res/layout/view_selected_wallet.xml index ca15dfce2b..f51aca4616 100644 --- a/feature-account-api/src/main/res/layout/view_selected_wallet.xml +++ b/feature-account-api/src/main/res/layout/view_selected_wallet.xml @@ -5,15 +5,16 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" tools:orientation="horizontal" - tools:parentTag="android.widget.LinearLayout"> + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> @@ -21,7 +22,17 @@ android:id="@+id/viewSelectedWalletAccountIcon" android:layout_width="40dp" android:layout_height="40dp" - android:layout_gravity="end" + android:layout_marginStart="8dp" + app:layout_constraintStart_toEndOf="@id/viewSelectedWalletTypeIcon" + app:layout_goneMarginStart="0dp" tools:src="@drawable/ic_polkadot_24" /> + \ No newline at end of file diff --git a/feature-account-impl/build.gradle b/feature-account-impl/build.gradle index 7d09470dce..5909d7a896 100644 --- a/feature-account-impl/build.gradle +++ b/feature-account-impl/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation project(':common') implementation project(':runtime') implementation project(':feature-account-api') + implementation project(':feature-wallet-api') implementation project(':feature-currency-api') implementation project(':feature-ledger-api') implementation project(':feature-versions-api') diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt index 06111aee8d..284abe21fe 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt @@ -1,15 +1,18 @@ package io.novafoundation.nova.feature_account_impl.data.ethereum.transaction import io.novafoundation.nova.common.utils.castOrNull +import io.novafoundation.nova.common.utils.toEcdsaSignatureData import io.novafoundation.nova.core.ethereum.Web3Api import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionBuilding import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService -import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionHash import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.EvmFee import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.interfaces.requireMetaAccountFor import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn @@ -18,10 +21,9 @@ import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.ethereum.sendSuspend import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper +import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow import jp.co.soramitsu.fearless_utils.extensions.toHexString import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw import org.web3j.crypto.RawTransaction @@ -49,7 +51,7 @@ internal class RealEvmTransactionService( val web3Api = chainRegistry.getCallEthereumApiOrThrow(chainId) val chain = chainRegistry.getChain(chainId) - val submittingMetaAccount = findMetaAccountFor(origin) + val submittingMetaAccount = accountRepository.requireMetaAccountFor(origin, chainId) val submittingAddress = submittingMetaAccount.requireAddressIn(chain) val txBuilder = EvmTransactionBuilder().apply(building) @@ -58,7 +60,7 @@ internal class RealEvmTransactionService( val gasPrice = gasPriceProviderFactory.createKnown(chainId).getGasPrice() val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit) - return EvmFee(gasLimit, gasPrice) + return EvmFee(gasLimit, gasPrice, SubmissionOrigin.singleOrigin(submittingMetaAccount.requireAccountIdIn(chain))) } override suspend fun transact( @@ -67,10 +69,11 @@ internal class RealEvmTransactionService( origin: TransactionOrigin, fallbackGasLimit: BigInteger, building: EvmTransactionBuilding - ): Result = runCatching { + ): Result = runCatching { val chain = chainRegistry.getChain(chainId) - val submittingMetaAccount = findMetaAccountFor(origin) + val submittingMetaAccount = accountRepository.requireMetaAccountFor(origin, chainId) val submittingAddress = submittingMetaAccount.requireAddressIn(chain) + val submittingAccountId = submittingMetaAccount.requireAccountIdIn(chain) val web3Api = chainRegistry.getCallEthereumApiOrThrow(chainId) val txBuilder = EvmTransactionBuilder().apply(building) @@ -80,7 +83,7 @@ internal class RealEvmTransactionService( val gasPrice = gasPriceProviderFactory.createKnown(chainId).getGasPrice() val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit) - EvmFee(gasLimit, gasPrice) + EvmFee(gasLimit, gasPrice, SubmissionOrigin.singleOrigin(submittingAccountId)) } val nonce = web3Api.getNonce(submittingAddress) @@ -88,30 +91,26 @@ internal class RealEvmTransactionService( val txForSign = txBuilder.buildForSign(nonce = nonce, gasPrice = evmFee.gasPrice, gasLimit = evmFee.gasLimit) val toSubmit = signTransaction(txForSign, submittingMetaAccount, chain) - web3Api.sendTransaction(toSubmit) + val txHash = web3Api.sendTransaction(toSubmit) + + ExtrinsicSubmission(hash = txHash, submissionOrigin = SubmissionOrigin.singleOrigin(submittingAccountId)) } private suspend fun signTransaction(txForSign: RawTransaction, metaAccount: MetaAccount, chain: Chain): String { val ethereumChainId = chain.addressPrefix.toLong() val encodedTx = TransactionEncoder.encode(txForSign, ethereumChainId) - val signer = signerProvider.signerFor(metaAccount) + val signer = signerProvider.rootSignerFor(metaAccount) val accountId = metaAccount.requireAccountIdIn(chain) val signerPayload = SignerPayloadRaw(encodedTx, accountId) - val signatureData = signer.signRaw(signerPayload).toSignatureData() + val signatureData = signer.signRaw(signerPayload).toEcdsaSignatureData() val eip155SignatureData: Sign.SignatureData = TransactionEncoder.createEip155SignatureData(signatureData, ethereumChainId) return txForSign.encodeWith(eip155SignatureData).toHexString(withPrefix = true) } - private suspend fun findMetaAccountFor(origin: TransactionOrigin): MetaAccount { - return when (origin) { - TransactionOrigin.SelectedWallet -> accountRepository.getSelectedMetaAccount() - } - } - private suspend fun Web3Api.getNonce(address: String): BigInteger { return ethGetTransactionCount(address, DefaultBlockParameterName.PENDING) .sendSuspend() @@ -124,12 +123,6 @@ internal class RealEvmTransactionService( default } - private fun SignatureWrapper.toSignatureData(): Sign.SignatureData { - require(this is SignatureWrapper.Ecdsa) - - return Sign.SignatureData(v, r, s) - } - private fun RawTransaction.encodeWith(signatureData: Sign.SignatureData): ByteArray { val values = TransactionEncoder.asRlpValues(this, signatureData) val rlpList = RlpList(values) 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 5b13960930..781c97c595 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 @@ -4,29 +4,32 @@ import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult import io.novafoundation.nova.common.utils.multiResult.runMultiCatching import io.novafoundation.nova.common.utils.orZero -import io.novafoundation.nova.common.utils.sum +import io.novafoundation.nova.common.utils.sumByBigInteger import io.novafoundation.nova.common.utils.takeWhileInclusive import io.novafoundation.nova.common.utils.tip +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.extrinsic.FormExtrinsicWithOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsic import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsicWithOrigin +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.InlineFee +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn +import io.novafoundation.nova.feature_account_api.domain.interfaces.requireMetaAccountFor +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.extrinsic.multi.SimpleCallBuilder +import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getRuntime import io.novafoundation.nova.runtime.network.rpc.RpcCalls -import jp.co.soramitsu.fearless_utils.extensions.toHexString import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHex import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder @@ -45,12 +48,25 @@ class RealExtrinsicService( private val extrinsicSplitter: ExtrinsicSplitter, ) : ExtrinsicService { - override suspend fun submitMultiExtrinsicWithSelectedWalletAwaitingInclusion( + override suspend fun submitExtrinsic( chain: Chain, + origin: TransactionOrigin, + formExtrinsic: FormExtrinsicWithOrigin + ): Result = runCatching { + val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) + val (extrinsic, submissionOrigin) = buildExtrinsic(chain, metaAccount, formExtrinsic) + val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic) + + ExtrinsicSubmission(hash, submissionOrigin) + } + + override suspend fun submitMultiExtrinsicAwaitingInclusion( + chain: Chain, + origin: TransactionOrigin, formExtrinsic: FormMultiExtrinsicWithOrigin ): RetriableMultiResult { return runMultiCatching( - intermediateListLoading = { constructSplitExtrinsicsForSubmission(chain, formExtrinsic) }, + intermediateListLoading = { constructSplitExtrinsicsForSubmission(chain, origin, formExtrinsic) }, listProcessing = { extrinsic -> rpcCalls.submitAndWatchExtrinsic(chain.id, extrinsic) .filterIsInstance() @@ -59,59 +75,24 @@ class RealExtrinsicService( ) } - override suspend fun submitExtrinsicWithSelectedWalletV2( - chain: Chain, - formExtrinsic: FormExtrinsicWithOrigin, - ): Result { - val account = accountRepository.getSelectedMetaAccount() - val accountId = account.requireAccountIdIn(chain) - - return submitExtrinsicWithAnySuitableWalletV2(chain, accountId, formExtrinsic) - } - - override suspend fun submitAndWatchExtrinsicWithSelectedWallet( - chain: Chain, - formExtrinsic: FormExtrinsicWithOrigin, - ): Flow { - val account = accountRepository.getSelectedMetaAccount() - val accountId = account.accountIdIn(chain)!! - - return submitAndWatchExtrinsicAnySuitableWallet(chain, accountId, formExtrinsic) - } - - override suspend fun submitExtrinsicWithAnySuitableWallet( - chain: Chain, - accountId: ByteArray, - formExtrinsic: FormExtrinsicWithOrigin, - ): Result = submitExtrinsicWithAnySuitableWalletV2(chain, accountId, formExtrinsic) - .map { it.hash } - - private suspend fun submitExtrinsicWithAnySuitableWalletV2( - chain: Chain, - accountId: ByteArray, - formExtrinsic: FormExtrinsicWithOrigin, - ): Result = runCatching { - val extrinsic = buildExtrinsic(chain, accountId, formExtrinsic) - val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic) - ExtrinsicSubmission(hash, accountId) - } - - override suspend fun submitAndWatchExtrinsicAnySuitableWallet( + override suspend fun submitAndWatchExtrinsic( chain: Chain, - accountId: ByteArray, - formExtrinsic: FormExtrinsicWithOrigin, - ): Flow { - val extrinsic = buildExtrinsic(chain, accountId, formExtrinsic) + origin: TransactionOrigin, + formExtrinsic: FormExtrinsicWithOrigin + ): Result> = runCatching { + val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) + val (extrinsic) = buildExtrinsic(chain, metaAccount, formExtrinsic) - return rpcCalls.submitAndWatchExtrinsic(chain.id, extrinsic) + rpcCalls.submitAndWatchExtrinsic(chain.id, extrinsic) .takeWhileInclusive { !it.terminal } } override suspend fun paymentInfo( chain: Chain, + origin: TransactionOrigin, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): FeeResponse { - val extrinsic = extrinsicBuilderFactory.createForFee(chain) + val extrinsic = extrinsicBuilderFactory.createForFee(getFeeSigner(chain, origin), chain) .also { it.formExtrinsic() } .build() @@ -120,20 +101,22 @@ class RealExtrinsicService( override suspend fun estimateFee( chain: Chain, - formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, - ): BigInteger { - val extrinsicBuilder = extrinsicBuilderFactory.createForFee(chain) + origin: TransactionOrigin, + formExtrinsic: suspend ExtrinsicBuilder.() -> Unit + ): Fee { + val signer = getFeeSigner(chain, origin) + val extrinsicBuilder = extrinsicBuilderFactory.createForFee(signer, chain) extrinsicBuilder.formExtrinsic() val extrinsic = extrinsicBuilder.build() - return estimateFee(chain, extrinsic).amount + return estimateFee(chain, extrinsic, signer) } - override suspend fun estimateFeeV2(chain: Chain, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit): Fee { - return InlineFee(estimateFee(chain, formExtrinsic)) - } - - override suspend fun estimateFee(chain: Chain, extrinsic: String): Fee { + override suspend fun estimateFee( + chain: Chain, + extrinsic: String, + usedSigner: FeeSigner + ): Fee { val chainId = chain.id val baseFee = rpcCalls.getExtrinsicFee(chain, extrinsic).partialFee @@ -143,43 +126,64 @@ class RealExtrinsicService( val tip = decodedExtrinsic.tip().orZero() - return InlineFee(tip + baseFee) + return SubstrateFee(amount = tip + baseFee, submissionOrigin = usedSigner.submissionOrigin(chain)) + } + + override suspend fun zeroFee(chain: Chain, origin: TransactionOrigin): Fee { + val signer = getFeeSigner(chain, origin) + return zeroFee(chain, signer) } - override suspend fun estimateMultiFee(chain: Chain, formExtrinsic: FormMultiExtrinsic): BigInteger { - val feeExtrinsicBuilderSequence = extrinsicBuilderFactory.createMultiForFee(chain) + override suspend fun estimateMultiFee( + chain: Chain, + origin: TransactionOrigin, + formExtrinsic: FormMultiExtrinsic + ): Fee { + val feeSigner = getFeeSigner(chain, origin) + val feeExtrinsicBuilderSequence = extrinsicBuilderFactory.createMultiForFee(feeSigner, chain) + + val extrinsics = constructSplitExtrinsics(chain, origin, formExtrinsic, feeExtrinsicBuilderSequence, alreadyComputedFeeSigner = feeSigner) + + if (extrinsics.isEmpty()) return zeroFee(chain, feeSigner) - val extrinsics = constructSplitExtrinsics(chain, formExtrinsic, feeExtrinsicBuilderSequence) + val fees = extrinsics.map { estimateFee(chain, it, feeSigner) } - val separateFees = extrinsics.map { estimateFee(chain, it).amount } + val totalFee = fees.sumByBigInteger { it.amount } - return separateFees.sum() + return SubstrateFee(totalFee, feeSigner.submissionOrigin(chain)) } private suspend fun constructSplitExtrinsicsForSubmission( chain: Chain, - formExtrinsic: FormMultiExtrinsicWithOrigin + origin: TransactionOrigin, + formExtrinsic: FormMultiExtrinsicWithOrigin, ): List { - val metaAccount = accountRepository.getSelectedMetaAccount() - val signer = signerProvider.signerFor(metaAccount) - val accountId = metaAccount.requireAccountIdIn(chain) + val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) + val signer = signerProvider.rootSignerFor(metaAccount) + + val requestedOrigin = metaAccount.requireAccountIdIn(chain) + val actualOrigin = signer.signerAccountId(chain) + val submissionOrigin = SubmissionOrigin(requestedOrigin = requestedOrigin, actualOrigin = actualOrigin) - val extrinsicBuilderSequence = extrinsicBuilderFactory.createMulti(chain, signer, accountId) + val extrinsicBuilderSequence = extrinsicBuilderFactory.createMulti(chain, signer, requestedOrigin) - val formExtrinsicWithOrigin: FormMultiExtrinsic = { formExtrinsic(accountId) } + val formExtrinsicWithOrigin: FormMultiExtrinsic = { formExtrinsic(submissionOrigin) } - return constructSplitExtrinsics(chain, formExtrinsicWithOrigin, extrinsicBuilderSequence) + return constructSplitExtrinsics(chain, origin, formExtrinsicWithOrigin, extrinsicBuilderSequence) } private suspend fun constructSplitExtrinsics( chain: Chain, + origin: TransactionOrigin, formExtrinsic: FormMultiExtrinsic, extrinsicBuilderSequence: Sequence, + alreadyComputedFeeSigner: FeeSigner? = null, ): List = coroutineScope { + val feeSigner = alreadyComputedFeeSigner ?: getFeeSigner(chain, origin) val runtime = chainRegistry.getRuntime(chain.id) val callBuilder = SimpleCallBuilder(runtime).apply { formExtrinsic() } - val splitCalls = extrinsicSplitter.split(callBuilder, chain) + val splitCalls = extrinsicSplitter.split(feeSigner, callBuilder, chain) val extrinsicBuilderIterator = extrinsicBuilderSequence.iterator() @@ -196,16 +200,37 @@ class RealExtrinsicService( private suspend fun buildExtrinsic( chain: Chain, - accountId: ByteArray, + metaAccount: MetaAccount, formExtrinsic: FormExtrinsicWithOrigin, - ): String { - val metaAccount = accountRepository.findMetaAccount(accountId) ?: error("No meta account found accessing ${accountId.toHexString()}") - val signer = signerProvider.signerFor(metaAccount) + ): SubmissionRaw { + val signer = signerProvider.rootSignerFor(metaAccount) + + val requestedOrigin = metaAccount.requireAccountIdIn(chain) + val actualOrigin = signer.signerAccountId(chain) + + val submissionOrigin = SubmissionOrigin(requestedOrigin = requestedOrigin, actualOrigin = actualOrigin) + + val extrinsicBuilder = extrinsicBuilderFactory.create(chain, signer, requestedOrigin) + + extrinsicBuilder.formExtrinsic(submissionOrigin) - val extrinsicBuilder = extrinsicBuilderFactory.create(chain, signer, accountId) + val extrinsic = extrinsicBuilder.build(useBatchAll = true) - extrinsicBuilder.formExtrinsic(accountId) + return SubmissionRaw(extrinsic, submissionOrigin) + } + + private suspend fun getFeeSigner(chain: Chain, origin: TransactionOrigin): FeeSigner { + val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) + return signerProvider.feeSigner(metaAccount, chain) + } + + private data class SubmissionRaw(val extrinsicRaw: String, val submissionOrigin: SubmissionOrigin) + + private suspend fun FeeSigner.submissionOrigin(chain: Chain): SubmissionOrigin { + return SubmissionOrigin(requestedFeeSignerId(chain = chain), actualFeeSignerId(chain)) + } - return extrinsicBuilder.build(useBatchAll = true) + private suspend fun zeroFee(chain: Chain, signer: FeeSigner): Fee { + return SubstrateFee(BigInteger.ZERO, signer.submissionOrigin(chain)) } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/mappers/Mappers.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/mappers/Mappers.kt index 5637e7af57..60c027fea0 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/mappers/Mappers.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/mappers/Mappers.kt @@ -7,13 +7,15 @@ import io.novafoundation.nova.core.model.Node import io.novafoundation.nova.core.model.Node.NetworkType import io.novafoundation.nova.core_db.dao.MetaAccountWithBalanceLocal import io.novafoundation.nova.core_db.model.NodeLocal -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal -import io.novafoundation.nova.core_db.model.chain.JoinedMetaAccountInfo -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.JoinedMetaAccountInfo +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ProxyAccountLocal import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountAssetBalance +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.api.AccountNameChooserMixin @@ -42,11 +44,13 @@ fun mapCryptoTypeToCryptoTypeModel( R.string.sr25519_selection_subtitle ) }" + CryptoType.ED25519 -> "${resourceManager.getString(R.string.ed25519_selection_title)} ${ resourceManager.getString( R.string.ed25519_selection_subtitle ) }" + CryptoType.ECDSA -> "${resourceManager.getString(R.string.ecdsa_selection_title)} ${ resourceManager.getString( R.string.ecdsa_selection_subtitle @@ -92,6 +96,7 @@ private fun mapMetaAccountTypeFromLocal(local: MetaAccountLocal.Type): LightMeta MetaAccountLocal.Type.PARITY_SIGNER -> LightMetaAccount.Type.PARITY_SIGNER MetaAccountLocal.Type.LEDGER -> LightMetaAccount.Type.LEDGER MetaAccountLocal.Type.POLKADOT_VAULT -> LightMetaAccount.Type.POLKADOT_VAULT + MetaAccountLocal.Type.PROXIED -> LightMetaAccount.Type.PROXIED } } @@ -114,20 +119,19 @@ fun mapMetaAccountLocalToMetaAccount( val chainAccounts = joinedMetaAccountInfo.chainAccounts.associateBy( keySelector = ChainAccountLocal::chainId, valueTransform = { - MetaAccount.ChainAccount( - metaId = joinedMetaAccountInfo.metaAccount.id, - publicKey = it.publicKey, - chainId = it.chainId, - accountId = it.accountId, - cryptoType = it.cryptoType - ) + mapChainAccountFromLocal(it) } ).filterNotNull() + val proxyAccount = joinedMetaAccountInfo.proxyAccountLocal?.let { + mapProxyAccountFromLocal(it) + } + return with(joinedMetaAccountInfo.metaAccount) { MetaAccount( id = id, chainAccounts = chainAccounts, + proxy = proxyAccount, substratePublicKey = substratePublicKey, substrateCryptoType = substrateCryptoType, substrateAccountId = substrateAccountId, @@ -135,7 +139,8 @@ fun mapMetaAccountLocalToMetaAccount( ethereumPublicKey = ethereumPublicKey, isSelected = isSelected, name = name, - type = mapMetaAccountTypeFromLocal(type) + type = mapMetaAccountTypeFromLocal(type), + status = mapMetaAccountStateFromLocal(status) ) } } @@ -153,7 +158,31 @@ fun mapMetaAccountLocalToLightMetaAccount( ethereumPublicKey = ethereumPublicKey, isSelected = isSelected, name = name, - type = mapMetaAccountTypeFromLocal(type) + type = mapMetaAccountTypeFromLocal(type), + status = mapMetaAccountStateFromLocal(status) + ) + } +} + +fun mapChainAccountFromLocal(chainAccountLocal: ChainAccountLocal): MetaAccount.ChainAccount { + return with(chainAccountLocal) { + MetaAccount.ChainAccount( + metaId = metaId, + publicKey = publicKey, + chainId = chainId, + accountId = accountId, + cryptoType = cryptoType + ) + } +} + +fun mapProxyAccountFromLocal(proxyAccountLocal: ProxyAccountLocal): ProxyAccount { + return with(proxyAccountLocal) { + ProxyAccount( + metaId = proxyMetaId, + chainId = chainId, + proxiedAccountId = proxiedAccountId, + proxyType = mapProxyTypeToString(proxyType) ) } } @@ -168,6 +197,7 @@ fun mapAddAccountPayloadToAddAccountType( AddAccountType.MetaAccount(accountNameState.value) } + is AddAccountPayload.ChainAccount -> AddAccountType.ChainAccount(payload.chainId, payload.metaId) } } @@ -176,3 +206,24 @@ fun mapOptionalNameToNameChooserState(name: String?) = when (name) { null -> AccountNameChooserMixin.State.NoInput else -> AccountNameChooserMixin.State.Input(name) } + +fun mapProxyTypeToString(proxyType: String): ProxyAccount.ProxyType { + return when (proxyType) { + "Any" -> ProxyAccount.ProxyType.Any + "NonTransfer" -> ProxyAccount.ProxyType.NonTransfer + "Governance" -> ProxyAccount.ProxyType.Governance + "Staking" -> ProxyAccount.ProxyType.Staking + "IdentityJudgement" -> ProxyAccount.ProxyType.IdentityJudgement + "CancelProxy" -> ProxyAccount.ProxyType.CancelProxy + "Auction" -> ProxyAccount.ProxyType.Auction + "NominationPools" -> ProxyAccount.ProxyType.NominationPools + else -> ProxyAccount.ProxyType.Other(proxyType) + } +} + +private fun mapMetaAccountStateFromLocal(local: MetaAccountLocal.Status): LightMetaAccount.Status { + return when (local) { + MetaAccountLocal.Status.ACTIVE -> LightMetaAccount.Status.ACTIVE + MetaAccountLocal.Status.DEACTIVATED -> LightMetaAccount.Status.DEACTIVATED + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealMetaAccountsUpdatesRegistry.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealMetaAccountsUpdatesRegistry.kt new file mode 100644 index 0000000000..c03405135b --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealMetaAccountsUpdatesRegistry.kt @@ -0,0 +1,63 @@ +package io.novafoundation.nova.feature_account_impl.data.proxy + +import io.novafoundation.nova.common.data.storage.Preferences +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class RealMetaAccountsUpdatesRegistry( + private val preferences: Preferences +) : MetaAccountsUpdatesRegistry { + + private val KEY = "meta_accounts_changes" + + override fun addMetaIds(ids: List) { + val metaIdsSet = getUpdates() + .toMutableSet() + metaIdsSet.addAll(ids) + val metaIdsJoinedToString = metaIdsSet.joinToString(",") + if (metaIdsJoinedToString.isNotEmpty()) { + preferences.putString(KEY, metaIdsJoinedToString) + } + } + + override fun observeUpdates(): Flow> { + return preferences.keyFlow(KEY) + .map { getUpdates() } + } + + override fun getUpdates(): Set { + val metaIds = preferences.getString(KEY) + if (metaIds.isNullOrEmpty()) return mutableSetOf() + + return metaIds.split(",") + .map { it.toLong() } + .toMutableSet() + } + + override fun remove(ids: List) { + val metaIdsSet = getUpdates() + .toMutableSet() + metaIdsSet.removeAll(ids) + val metaIdsJoinedToString = metaIdsSet.joinToString(",") + + if (metaIdsJoinedToString.isEmpty()) { + preferences.removeField(KEY) + } else { + preferences.putString(KEY, metaIdsJoinedToString) + } + } + + override fun clear() { + preferences.removeField(KEY) + } + + override fun observeUpdatesExist(): Flow { + return preferences.keyFlow(KEY) + .map { hasUpdates() } + } + + override fun hasUpdates(): Boolean { + return preferences.contains(KEY) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealProxySyncService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealProxySyncService.kt new file mode 100644 index 0000000000..9be3511bbf --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/proxy/RealProxySyncService.kt @@ -0,0 +1,178 @@ +package io.novafoundation.nova.feature_account_impl.data.proxy + +import android.util.Log +import io.novafoundation.nova.common.address.AccountIdKey +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.utils.LOG_TAG +import io.novafoundation.nova.common.utils.coroutines.RootScope +import io.novafoundation.nova.common.utils.mapToSet +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ProxyAccountLocal +import io.novafoundation.nova.feature_account_api.data.model.ProxiedWithProxy +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.repository.ProxyRepository +import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity +import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountId +import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.proxied.ProxiedAddAccountRepository +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.findChains +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class RealProxySyncService( + private val chainRegistry: ChainRegistry, + private val proxyRepository: ProxyRepository, + private val accountRepository: AccountRepository, + private val accountDao: MetaAccountDao, + private val identityProvider: IdentityProvider, + private val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry, + private val proxiedAddAccountRepository: ProxiedAddAccountRepository, + private val rootScope: RootScope, + private val shouldSyncWatchOnlyProxies: Boolean +) : ProxySyncService { + + override fun proxySyncTrigger(): Flow<*> { + return chainRegistry.currentChains.map { chains -> + chains + .filter(Chain::supportProxy) + .map(Chain::id) + }.distinctUntilChanged() + } + + override fun startSyncing() { + rootScope.launch(Dispatchers.Default) { + startSyncInternal() + } + } + + override suspend fun startSyncingSuspend() { + startSyncInternal() + } + + private suspend fun startSyncInternal() = runCatching { + val metaAccounts = getMetaAccounts() + if (metaAccounts.isEmpty()) return@runCatching + + val supportedProxyChains = getSupportedProxyChains() + + Log.d(LOG_TAG, "Starting syncing proxies in ${supportedProxyChains.size} chains") + + supportedProxyChains.forEach { chain -> + syncChainProxies(chain, metaAccounts) + } + }.onFailure { + Log.e(LOG_TAG, "Failed to sync proxy delegators", it) + } + + private suspend fun syncChainProxies(chain: Chain, metaAccounts: List) = runCatching { + Log.d(LOG_TAG, "Started syncing proxies for ${chain.name}") + + val availableAccountIds = chain.getAvailableAccountIds(metaAccounts) + + val proxiedsWithProxies = proxyRepository.getAllProxiesForMetaAccounts(chain.id, availableAccountIds) + + val oldProxies = accountDao.getProxyAccounts(chain.id) + + val notAddedProxies = filterNotAddedProxieds(proxiedsWithProxies, oldProxies) + + val identitiesByChain = notAddedProxies.loadProxiedIdentities(chain.id) + val addedProxiedsMetaIds = notAddedProxies.map { + val identity = identitiesByChain[it.proxied.accountId.intoKey()] + + proxiedAddAccountRepository.addAccount(ProxiedAddAccountRepository.Payload(it, identity)) + } + + val deactivatedMetaAccountIds = getDeactivatedMetaIds(proxiedsWithProxies, oldProxies) + accountDao.changeAccountsStatus(deactivatedMetaAccountIds, MetaAccountLocal.Status.DEACTIVATED) + + val changedMetaIds = addedProxiedsMetaIds + deactivatedMetaAccountIds + metaAccountsUpdatesRegistry.addMetaIds(changedMetaIds) + }.onFailure { + Log.e(LOG_TAG, "Failed to sync proxy delegators in chain ${chain.name}", it) + }.onSuccess { + Log.d(LOG_TAG, "Finished syncing proxies for ${chain.name}") + } + + private fun filterNotAddedProxieds( + proxiedsWithProxies: List, + oldProxies: List + ): List { + val oldIdentifiers = oldProxies.mapToSet { it.identifier } + return proxiedsWithProxies.filter { it.toLocalIdentifier() !in oldIdentifiers } + } + + private suspend fun getDeactivatedMetaIds( + onChainProxies: List, + oldProxies: List + ): List { + val newIdentifiers = onChainProxies.mapToSet { it.toLocalIdentifier() } + val accountsToDeactivate = oldProxies.filter { it.identifier !in newIdentifiers } + .map { it.proxiedMetaId } + + return accountsToDeactivate.takeNotYetDeactivatedMetaAccounts() + } + + private suspend fun getMetaAccounts(): List { + return accountRepository.getActiveMetaAccounts() + .filter { it.isAllowedToSyncProxy() } + } + + private fun MetaAccount.isAllowedToSyncProxy(): Boolean { + return when (type) { + LightMetaAccount.Type.SECRETS, + LightMetaAccount.Type.PARITY_SIGNER, + LightMetaAccount.Type.LEDGER, + LightMetaAccount.Type.POLKADOT_VAULT -> true + + LightMetaAccount.Type.WATCH_ONLY -> shouldSyncWatchOnlyProxies + + LightMetaAccount.Type.PROXIED -> false + } + } + + private suspend fun getSupportedProxyChains(): List { + return chainRegistry.findChains { it.supportProxy } + } + + private fun Chain.getAvailableAccountIds(metaAccounts: List): List { + return metaAccounts.mapNotNull { metaAccount -> + val accountId = metaAccount.accountIdIn(chain = this) + accountId?.let { + MetaAccountId(accountId, metaAccount.id) + } + } + } + + private suspend fun List.loadProxiedIdentities(chainId: ChainId): Map { + val proxiedAccountIds = map { it.proxied.accountId } + + return identityProvider.identitiesFor(proxiedAccountIds, chainId) + } + + private suspend fun List.takeNotYetDeactivatedMetaAccounts(): List { + val alreadyDeactivatedMetaAccountIds = accountDao.getMetaAccountIdsByStatus(MetaAccountLocal.Status.DEACTIVATED) + + return this - alreadyDeactivatedMetaAccountIds.toSet() + } + + private fun ProxiedWithProxy.toLocalIdentifier(): String { + return ProxyAccountLocal.makeIdentifier( + proxyMetaId = proxy.metaId, + chainId = proxied.chainId, + proxiedAccountId = proxied.accountId, + proxyType = proxy.proxyType + ) + } +} 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 aadf20f212..cef2adf9ca 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 @@ -18,25 +18,22 @@ import io.novafoundation.nova.feature_account_api.data.secrets.keypair import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.Account import io.novafoundation.nova.feature_account_api.domain.model.AuthType -import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountAssetBalance import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountOrdering import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_account_api.domain.model.multiChainEncryptionIn -import io.novafoundation.nova.feature_account_api.domain.model.publicKeyIn +import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.feature_account_impl.data.mappers.mapNodeLocalToNode import io.novafoundation.nova.feature_account_impl.data.network.blockchain.AccountSubstrateSource import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource import io.novafoundation.nova.runtime.ext.genesisHash -import io.novafoundation.nova.runtime.ext.isValidAddress import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory import jp.co.soramitsu.fearless_utils.encrypt.json.JsonSeedEncoder import jp.co.soramitsu.fearless_utils.encrypt.mnemonic.Mnemonic import jp.co.soramitsu.fearless_utils.encrypt.mnemonic.MnemonicCreator -import jp.co.soramitsu.fearless_utils.encrypt.qr.QrFormat import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -116,30 +113,26 @@ class AccountRepositoryImpl( return accountDataSource.selectedMetaAccountFlow() } - override suspend fun findMetaAccount(accountId: ByteArray): MetaAccount? { - return accountDataSource.findMetaAccount(accountId) + override suspend fun findMetaAccount(accountId: ByteArray, chainId: String): MetaAccount? { + return accountDataSource.findMetaAccount(accountId, chainId) } - override suspend fun accountNameFor(accountId: AccountId): String? { - return accountDataSource.accountNameFor(accountId) + override suspend fun accountNameFor(accountId: AccountId, chainId: String): String? { + return accountDataSource.accountNameFor(accountId, chainId) } - override suspend fun allMetaAccounts(): List { - return accountDataSource.allMetaAccounts() - } - - override suspend fun hasMetaAccounts(): Boolean { - return accountDataSource.hasMetaAccounts() - } - - override suspend fun allLightMetaAccounts(): List { - return accountDataSource.allLightMetaAccounts() + override suspend fun hasActiveMetaAccounts(): Boolean { + return accountDataSource.hasActiveMetaAccounts() } override fun allMetaAccountsFlow(): Flow> { return accountDataSource.allMetaAccountsFlow() } + override fun activeMetaAccountsFlow(): Flow> { + return accountDataSource.activeMetaAccountsFlow() + } + override fun metaAccountBalancesFlow(): Flow> { return accountDataSource.metaAccountsWithBalancesFlow() } @@ -247,8 +240,16 @@ class AccountRepositoryImpl( } } - override suspend fun isAccountExists(accountId: AccountId): Boolean { - return accountDataSource.accountExists(accountId) + override suspend fun isAccountExists(accountId: AccountId, chainId: String): Boolean { + return accountDataSource.accountExists(accountId, chainId) + } + + override suspend fun removeDeactivatedMetaAccounts() { + accountDataSource.removeDeactivatedMetaAccounts() + } + + override suspend fun getActiveMetaAccounts(): List { + return accountDataSource.getActiveMetaAccounts() } override fun nodesFlow(): Flow> { @@ -300,18 +301,10 @@ class AccountRepositoryImpl( } override suspend fun createQrAccountContent(chain: Chain, account: MetaAccount): String { - val payload = QrFormat.Payload( - address = account.addressIn(chain)!!, - publicKey = account.publicKeyIn(chain)!!, - name = account.name - ) - - val qrSharing = multiChainQrSharingFactory.create(addressValidator = chain::isValidAddress) - - return qrSharing.encode(payload) + return account.requireAddressIn(chain) } - private suspend fun mapAccountLocalToAccount(accountLocal: AccountLocal): Account { + private fun mapAccountLocalToAccount(accountLocal: AccountLocal): Account { val network = getNetworkForType(accountLocal.networkType) return with(accountLocal) { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AddAccountRepository.kt deleted file mode 100644 index 0e6cae3775..0000000000 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/AddAccountRepository.kt +++ /dev/null @@ -1,146 +0,0 @@ -package io.novafoundation.nova.feature_account_impl.data.repository - -import android.database.sqlite.SQLiteConstraintException -import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType -import io.novafoundation.nova.common.utils.removeHexPrefix -import io.novafoundation.nova.core.model.CryptoType -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountAlreadyExistsException -import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType -import io.novafoundation.nova.feature_account_api.domain.model.ImportJsonMetaData -import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource -import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory -import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.fearless_utils.encrypt.json.JsonSeedDecoder -import jp.co.soramitsu.fearless_utils.encrypt.model.NetworkTypeIdentifier -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class AddAccountRepository( - private val accountDataSource: AccountDataSource, - private val accountSecretsFactory: AccountSecretsFactory, - private val jsonSeedDecoder: JsonSeedDecoder, - private val chainRegistry: ChainRegistry, -) { - - suspend fun addFromMnemonic( - mnemonic: String, - advancedEncryption: AdvancedEncryption, - addAccountType: AddAccountType - ): Long = withContext(Dispatchers.Default) { - addAccount( - derivationPaths = advancedEncryption.derivationPaths, - addAccountType = addAccountType, - accountSource = AccountSecretsFactory.AccountSource.Mnemonic( - cryptoType = pickCryptoType(addAccountType, advancedEncryption), - mnemonic = mnemonic - ) - ) - } - - suspend fun addFromSeed( - seed: String, - advancedEncryption: AdvancedEncryption, - addAccountType: AddAccountType - ): Long = withContext(Dispatchers.Default) { - addAccount( - derivationPaths = advancedEncryption.derivationPaths, - addAccountType = addAccountType, - accountSource = AccountSecretsFactory.AccountSource.Seed( - cryptoType = pickCryptoType(addAccountType, advancedEncryption), - seed = seed - ) - ) - } - - suspend fun addFromJson( - json: String, - password: String, - addAccountType: AddAccountType - ): Long = withContext(Dispatchers.Default) { - addAccount( - derivationPaths = AdvancedEncryption.DerivationPaths.empty(), - addAccountType = addAccountType, - accountSource = AccountSecretsFactory.AccountSource.Json(json, password) - ) - } - - private suspend fun pickCryptoType(addAccountType: AddAccountType, advancedEncryption: AdvancedEncryption): CryptoType { - val cryptoType = if (addAccountType is AddAccountType.ChainAccount && chainRegistry.getChain(addAccountType.chainId).isEthereumBased) { - advancedEncryption.ethereumCryptoType - } else { - advancedEncryption.substrateCryptoType - } - - requireNotNull(cryptoType) { "Expected crypto type was null" } - - return cryptoType - } - - /** - * @return id of inserted/modified metaAccount - */ - private suspend fun addAccount( - derivationPaths: AdvancedEncryption.DerivationPaths, - addAccountType: AddAccountType, - accountSource: AccountSecretsFactory.AccountSource - ): Long { - return when (addAccountType) { - is AddAccountType.MetaAccount -> { - val (secrets, substrateCryptoType) = accountSecretsFactory.metaAccountSecrets( - substrateDerivationPath = derivationPaths.substrate, - ethereumDerivationPath = derivationPaths.ethereum, - accountSource = accountSource - ) - - transformingInsertionErrors { - accountDataSource.insertMetaAccountFromSecrets( - name = addAccountType.name, - substrateCryptoType = substrateCryptoType, - secrets = secrets - ) - } - } - - is AddAccountType.ChainAccount -> { - val chain = chainRegistry.getChain(addAccountType.chainId) - - val derivationPath = if (chain.isEthereumBased) derivationPaths.ethereum else derivationPaths.substrate - - val (secrets, cryptoType) = accountSecretsFactory.chainAccountSecrets( - derivationPath = derivationPath, - accountSource = accountSource, - isEthereum = chain.isEthereumBased - ) - - transformingInsertionErrors { - accountDataSource.insertChainAccount( - metaId = addAccountType.metaId, - chain = chain, - cryptoType = cryptoType, - secrets = secrets - ) - } - - addAccountType.metaId - } - } - } - - suspend fun extractJsonMetadata(importJson: String): ImportJsonMetaData = withContext(Dispatchers.Default) { - val importAccountMeta = jsonSeedDecoder.extractImportMetaData(importJson) - - with(importAccountMeta) { - val chainId = (networkTypeIdentifier as? NetworkTypeIdentifier.Genesis)?.genesis?.removeHexPrefix() - val cryptoType = mapEncryptionToCryptoType(encryption.encryptionType) - - ImportJsonMetaData(name, chainId, cryptoType) - } - } - - private inline fun transformingInsertionErrors(action: () -> R) = try { - action() - } catch (_: SQLiteConstraintException) { - throw AccountAlreadyExistsException() - } -} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/ParitySignerRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/ParitySignerRepository.kt deleted file mode 100644 index 2af2d2bf52..0000000000 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/ParitySignerRepository.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.novafoundation.nova.feature_account_impl.data.repository - -import io.novafoundation.nova.core.model.CryptoType -import io.novafoundation.nova.core_db.dao.MetaAccountDao -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal -import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant -import jp.co.soramitsu.fearless_utils.runtime.AccountId - -interface ParitySignerRepository { - - suspend fun addParitySignerWallet( - name: String, - substrateAccountId: AccountId, - variant: PolkadotVaultVariant, - ): Long -} - -class RealParitySignerRepository( - private val accountDao: MetaAccountDao -) : ParitySignerRepository { - - override suspend fun addParitySignerWallet( - name: String, - substrateAccountId: AccountId, - variant: PolkadotVaultVariant - ): Long { - val metaAccount = MetaAccountLocal( - // it is safe to assume that accountId is equal to public key since Parity Signer only uses SR25519 - substratePublicKey = substrateAccountId, - substrateAccountId = substrateAccountId, - substrateCryptoType = CryptoType.SR25519, - ethereumPublicKey = null, - ethereumAddress = null, - name = name, - isSelected = false, - position = accountDao.nextAccountPosition(), - type = variant.asMetaAccountTypeLocal() - ) - - return accountDao.insertMetaAccount(metaAccount) - } - - private fun PolkadotVaultVariant.asMetaAccountTypeLocal(): MetaAccountLocal.Type { - return when (this) { - PolkadotVaultVariant.POLKADOT_VAULT -> MetaAccountLocal.Type.POLKADOT_VAULT - PolkadotVaultVariant.PARITY_SIGNER -> MetaAccountLocal.Type.PARITY_SIGNER - } - } -} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/RealProxyRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/RealProxyRepository.kt new file mode 100644 index 0000000000..9259474815 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/RealProxyRepository.kt @@ -0,0 +1,133 @@ +package io.novafoundation.nova.feature_account_impl.data.repository + +import io.novafoundation.nova.common.address.AccountIdKey +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum +import io.novafoundation.nova.common.data.network.runtime.binding.castToList +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.common.data.network.runtime.binding.getTyped +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.feature_account_api.data.model.ProxiedWithProxy +import io.novafoundation.nova.feature_account_api.data.repository.ProxyRepository +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountId +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.feature_account_impl.data.mappers.mapProxyTypeToString +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.metadata.module +import jp.co.soramitsu.fearless_utils.runtime.metadata.storage +import java.math.BigInteger + +private class OnChainProxyModel( + val accountId: AccountIdKey, + val proxyType: String, + val delay: BigInteger +) + +class RealProxyRepository( + private val remoteSource: StorageDataSource, + private val chainRegistry: ChainRegistry +) : ProxyRepository { + + override suspend fun getAllProxiesForMetaAccounts(chainId: ChainId, metaAccountIds: List): List { + val delegatorToProxies = receiveAllProxies(chainId) + + val accountIdToMetaAccounts = metaAccountIds.groupBy { it.accountId.intoKey() } + + return delegatorToProxies + .mapNotNull { (delegator, proxies) -> + val notDelayedProxies = proxies.filter { it.delay == BigInteger.ZERO } + val matchedProxies = matchProxiesToAccountsAndMap(notDelayedProxies, accountIdToMetaAccounts) + + if (matchedProxies.isEmpty()) return@mapNotNull null + + delegator to matchedProxies + }.flatMap { (delegator, proxies) -> + proxies.map { proxy -> mapToProxiedWithProxies(chainId, delegator, proxy) } + } + } + + override suspend fun getDelegatedProxyTypes(chainId: ChainId, proxiedAccountId: AccountId, proxyAccountId: AccountId): List { + val proxies = remoteSource.query(chainId) { + runtime.metadata.module(Modules.PROXY) + .storage("Proxies") + .query( + keyArguments = arrayOf(proxiedAccountId), + binding = { result -> + bindProxyAccounts(result) + } + ) + } + + return proxies.filter { it.accountId == proxyAccountId.intoKey() } + .map { mapProxyTypeToString(it.proxyType) } + } + + private suspend fun receiveAllProxies(chainId: ChainId): Map> { + return remoteSource.query(chainId) { + runtime.metadata.module(Modules.PROXY) + .storage("Proxies") + .entries( + keyExtractor = { (accountId: AccountId) -> AccountIdKey(accountId) }, + binding = { result, _ -> + bindProxyAccounts(result) + }, + recover = { _, _ -> + // Do nothing if entry binding throws an exception + } + ) + } + } + + private fun bindProxyAccounts(dynamicInstance: Any?): List { + if (dynamicInstance == null) return emptyList() + + val root = dynamicInstance.castToList() + val proxies = root[0].castToList() + + return proxies.map { + val proxy = it.castToStruct() + val proxyAccountId: ByteArray = proxy.getTyped("delegate") + val proxyType = proxy.get("proxyType").castToDictEnum() + val delay = proxy.getTyped("delay") + OnChainProxyModel( + proxyAccountId.intoKey(), + proxyType.name, + delay + ) + } + } + + private fun mapToProxiedWithProxies( + chainId: ChainId, + delegator: AccountIdKey, + proxy: ProxiedWithProxy.Proxy + ): ProxiedWithProxy { + return ProxiedWithProxy( + proxied = ProxiedWithProxy.Proxied( + accountId = delegator.value, + chainId = chainId + ), + proxy = proxy + ) + } + + private fun matchProxiesToAccountsAndMap( + proxies: List, + accountIdToMetaAccounts: Map> + ): List { + return proxies.flatMap { onChainProxy -> + val matchedAccounts = accountIdToMetaAccounts[onChainProxy.accountId] ?: return@flatMap emptyList() + + matchedAccounts.map { + ProxiedWithProxy.Proxy( + accountId = onChainProxy.accountId.value, + metaId = it.metaId, + proxyType = onChainProxy.proxyType + ) + } + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/WatchOnlyRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/WatchOnlyRepository.kt index ab4d1b5dde..b6de09b5ef 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/WatchOnlyRepository.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/WatchOnlyRepository.kt @@ -1,10 +1,6 @@ package io.novafoundation.nova.feature_account_impl.data.repository import io.novafoundation.nova.core_db.dao.MetaAccountDao -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal -import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.fearless_utils.runtime.AccountId class WatchWalletSuggestion( val name: String, @@ -14,18 +10,6 @@ class WatchWalletSuggestion( interface WatchOnlyRepository { - suspend fun changeWatchChainAccount( - metaId: Long, - chainId: ChainId, - accountId: AccountId - ) - - suspend fun addWatchWallet( - name: String, - substrateAccountId: AccountId, - ethereumAccountId: AccountId? - ): Long - suspend fun watchWalletSuggestions(): List } @@ -33,42 +17,6 @@ class RealWatchOnlyRepository( private val accountDao: MetaAccountDao ) : WatchOnlyRepository { - override suspend fun changeWatchChainAccount( - metaId: Long, - chainId: ChainId, - accountId: AccountId - ) { - val chainAccount = ChainAccountLocal( - metaId = metaId, - chainId = chainId, - accountId = accountId, - cryptoType = null, - publicKey = null - ) - - accountDao.insertChainAccount(chainAccount) - } - - override suspend fun addWatchWallet( - name: String, - substrateAccountId: AccountId, - ethereumAccountId: AccountId? - ): Long { - val metaAccount = MetaAccountLocal( - substratePublicKey = null, - substrateCryptoType = null, - substrateAccountId = substrateAccountId, - ethereumPublicKey = null, - ethereumAddress = ethereumAccountId, - name = name, - isSelected = false, - position = accountDao.nextAccountPosition(), - type = MetaAccountLocal.Type.WATCH_ONLY - ) - - return accountDao.insertMetaAccount(metaAccount) - } - override suspend fun watchWalletSuggestions(): List { return listOf( WatchWalletSuggestion( diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/ledger/RealLedgerAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/ledger/RealLedgerAddAccountRepository.kt new file mode 100644 index 0000000000..949ea9a4d7 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/ledger/RealLedgerAddAccountRepository.kt @@ -0,0 +1,83 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.ledger + +import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType +import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository +import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerDerivationPath +import io.novafoundation.nova.runtime.ext.accountIdOf +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +class RealLedgerAddAccountRepository( + private val accountDao: MetaAccountDao, + private val chainRegistry: ChainRegistry, + private val secretStoreV2: SecretStoreV2, + private val proxySyncService: ProxySyncService, +) : LedgerAddAccountRepository(proxySyncService) { + + override suspend fun addAccountInternal(payload: Payload): Long { + return when (payload) { + is Payload.MetaAccount -> addMetaAccount(payload) + is Payload.ChainAccount -> addChainAccount(payload) + } + } + + private suspend fun addMetaAccount(payload: Payload.MetaAccount): Long { + val metaAccount = MetaAccountLocal( + substratePublicKey = null, + substrateCryptoType = null, + substrateAccountId = null, + ethereumPublicKey = null, + ethereumAddress = null, + name = payload.name, + parentMetaId = null, + isSelected = false, + position = accountDao.nextAccountPosition(), + type = MetaAccountLocal.Type.LEDGER, + status = MetaAccountLocal.Status.ACTIVE + ) + + val metaId = accountDao.insertMetaAndChainAccounts(metaAccount) { metaId -> + payload.ledgerChainAccounts.map { (chainId, account) -> + val chain = chainRegistry.getChain(chainId) + + ChainAccountLocal( + metaId = metaId, + chainId = chainId, + publicKey = account.publicKey, + accountId = chain.accountIdOf(account.publicKey), + cryptoType = mapEncryptionToCryptoType(account.encryptionType) + ) + } + } + + payload.ledgerChainAccounts.onEach { (chainId, ledgerAccount) -> + val derivationPathKey = LedgerDerivationPath.derivationPathSecretKey(chainId) + secretStoreV2.putAdditionalMetaAccountSecret(metaId, derivationPathKey, ledgerAccount.derivationPath) + } + + return metaId + } + + private suspend fun addChainAccount(payload: Payload.ChainAccount): Long { + val chain = chainRegistry.getChain(payload.chainId) + + val chainAccount = ChainAccountLocal( + metaId = payload.metaId, + chainId = payload.chainId, + publicKey = payload.ledgerChainAccount.publicKey, + accountId = chain.accountIdOf(payload.ledgerChainAccount.publicKey), + cryptoType = mapEncryptionToCryptoType(payload.ledgerChainAccount.encryptionType) + ) + + accountDao.insertChainAccount(chainAccount) + + val derivationPathKey = LedgerDerivationPath.derivationPathSecretKey(payload.chainId) + secretStoreV2.putAdditionalMetaAccountSecret(payload.metaId, derivationPathKey, payload.ledgerChainAccount.derivationPath) + + return payload.metaId + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/paritySigner/ParitySignerAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/paritySigner/ParitySignerAddAccountRepository.kt new file mode 100644 index 0000000000..77f6a49249 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/paritySigner/ParitySignerAddAccountRepository.kt @@ -0,0 +1,49 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.paritySigner + +import io.novafoundation.nova.core.model.CryptoType +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.BaseAddAccountRepository +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class ParitySignerAddAccountRepository( + private val accountDao: MetaAccountDao, + private val chainRegistry: ChainRegistry, + private val proxySyncService: ProxySyncService, +) : BaseAddAccountRepository(proxySyncService) { + + class Payload( + val name: String, + val substrateAccountId: AccountId, + val variant: PolkadotVaultVariant + ) + + override suspend fun addAccountInternal(payload: Payload): Long { + val metaAccount = MetaAccountLocal( + // it is safe to assume that accountId is equal to public key since Parity Signer only uses SR25519 + substratePublicKey = payload.substrateAccountId, + substrateAccountId = payload.substrateAccountId, + substrateCryptoType = CryptoType.SR25519, + ethereumPublicKey = null, + ethereumAddress = null, + name = payload.name, + parentMetaId = null, + isSelected = false, + position = accountDao.nextAccountPosition(), + type = payload.variant.asMetaAccountTypeLocal(), + status = MetaAccountLocal.Status.ACTIVE + ) + + return accountDao.insertMetaAccount(metaAccount) + } + + private fun PolkadotVaultVariant.asMetaAccountTypeLocal(): MetaAccountLocal.Type { + return when (this) { + PolkadotVaultVariant.POLKADOT_VAULT -> MetaAccountLocal.Type.POLKADOT_VAULT + PolkadotVaultVariant.PARITY_SIGNER -> MetaAccountLocal.Type.PARITY_SIGNER + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/proxied/ProxiedAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/proxied/ProxiedAddAccountRepository.kt new file mode 100644 index 0000000000..29365870bf --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/proxied/ProxiedAddAccountRepository.kt @@ -0,0 +1,87 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.proxied + +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.ProxyAccountLocal +import io.novafoundation.nova.feature_account_api.data.model.ProxiedWithProxy +import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.AddAccountRepository +import io.novafoundation.nova.runtime.ext.addressOf +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +/** + * It's important to extends ProxiedAddAccountRepository from AddAccountRepository instead of BaseAddAccountRepository + * since we don't need to sync proxy accounts for this case + */ +class ProxiedAddAccountRepository( + private val accountDao: MetaAccountDao, + private val chainRegistry: ChainRegistry +) : AddAccountRepository { + + class Payload( + val proxiedWithProxy: ProxiedWithProxy, + val identity: Identity? + ) + + override suspend fun addAccount(payload: Payload): Long { + val position = accountDao.nextAccountPosition() + + return accountDao.insertProxiedMetaAccount( + metaAccount = createMetaAccount(payload, position), + chainAccount = { createChainAccount(it, payload) }, + proxyAccount = { createProxyAccount(it, payload) } + ) + } + + private suspend fun createMetaAccount( + payload: Payload, + position: Int + ): MetaAccountLocal { + val proxied = payload.proxiedWithProxy.proxied + val proxy = payload.proxiedWithProxy.proxy + val chain = chainRegistry.getChain(proxied.chainId) + + return MetaAccountLocal( + substratePublicKey = null, + substrateCryptoType = null, + substrateAccountId = null, + ethereumPublicKey = null, + ethereumAddress = null, + name = payload.identity?.name ?: chain.addressOf(proxied.accountId), + parentMetaId = proxy.metaId, + isSelected = false, + position = position, + type = MetaAccountLocal.Type.PROXIED, + status = MetaAccountLocal.Status.ACTIVE + ) + } + + private fun createChainAccount(proxiedMetaId: Long, payload: Payload): ChainAccountLocal { + val proxied = payload.proxiedWithProxy.proxied + + return ChainAccountLocal( + metaId = proxiedMetaId, + chainId = proxied.chainId, + publicKey = null, + accountId = proxied.accountId, + cryptoType = null + ) + } + + private fun createProxyAccount( + proxiedMetaId: Long, + payload: Payload + ): ProxyAccountLocal { + val proxied = payload.proxiedWithProxy.proxied + val proxy = payload.proxiedWithProxy.proxy + + return ProxyAccountLocal( + proxiedMetaId = proxiedMetaId, + proxyMetaId = proxy.metaId, + chainId = proxied.chainId, + proxiedAccountId = proxied.accountId, + proxyType = proxy.proxyType + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/JsonAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/JsonAddAccountRepository.kt new file mode 100644 index 0000000000..1513108ce3 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/JsonAddAccountRepository.kt @@ -0,0 +1,54 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets + +import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType +import io.novafoundation.nova.common.utils.removeHexPrefix +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption +import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType +import io.novafoundation.nova.feature_account_api.domain.model.ImportJsonMetaData +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import jp.co.soramitsu.fearless_utils.encrypt.json.JsonSeedDecoder +import jp.co.soramitsu.fearless_utils.encrypt.model.NetworkTypeIdentifier +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class JsonAddAccountRepository( + private val accountDataSource: AccountDataSource, + private val accountSecretsFactory: AccountSecretsFactory, + private val jsonSeedDecoder: JsonSeedDecoder, + private val chainRegistry: ChainRegistry, + private val proxySyncService: ProxySyncService, +) : SecretsAddAccountRepository( + accountDataSource, + accountSecretsFactory, + chainRegistry, + proxySyncService +) { + + class Payload( + val json: String, + val password: String, + val addAccountType: AddAccountType + ) + + override suspend fun addAccountInternal(payload: Payload): Long { + return addSecretsAccount( + derivationPaths = AdvancedEncryption.DerivationPaths.empty(), + addAccountType = payload.addAccountType, + accountSource = AccountSecretsFactory.AccountSource.Json(payload.json, payload.password) + ) + } + + suspend fun extractJsonMetadata(importJson: String): ImportJsonMetaData = withContext(Dispatchers.Default) { + val importAccountMeta = jsonSeedDecoder.extractImportMetaData(importJson) + + with(importAccountMeta) { + val chainId = (networkTypeIdentifier as? NetworkTypeIdentifier.Genesis)?.genesis?.removeHexPrefix() + val cryptoType = mapEncryptionToCryptoType(encryption.encryptionType) + + ImportJsonMetaData(name, chainId, cryptoType) + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/MnemonicAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/MnemonicAddAccountRepository.kt new file mode 100644 index 0000000000..56738c682c --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/MnemonicAddAccountRepository.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets + +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption +import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +class MnemonicAddAccountRepository( + private val accountDataSource: AccountDataSource, + private val accountSecretsFactory: AccountSecretsFactory, + private val chainRegistry: ChainRegistry, + private val proxySyncService: ProxySyncService, +) : SecretsAddAccountRepository( + accountDataSource, + accountSecretsFactory, + chainRegistry, + proxySyncService +) { + + class Payload( + val mnemonic: String, + val advancedEncryption: AdvancedEncryption, + val addAccountType: AddAccountType + ) + + override suspend fun addAccountInternal(payload: Payload): Long { + return addSecretsAccount( + derivationPaths = payload.advancedEncryption.derivationPaths, + addAccountType = payload.addAccountType, + accountSource = AccountSecretsFactory.AccountSource.Mnemonic( + cryptoType = pickCryptoType(payload.addAccountType, payload.advancedEncryption), + mnemonic = payload.mnemonic + ) + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SecretsAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SecretsAddAccountRepository.kt new file mode 100644 index 0000000000..b7ff9af903 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SecretsAddAccountRepository.kt @@ -0,0 +1,111 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets + +import android.database.sqlite.SQLiteConstraintException +import io.novafoundation.nova.core.model.CryptoType +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.BaseAddAccountRepository +import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountAlreadyExistsException +import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +abstract class SecretsAddAccountRepository( + private val accountDataSource: AccountDataSource, + private val accountSecretsFactory: AccountSecretsFactory, + private val chainRegistry: ChainRegistry, + private val proxySyncService: ProxySyncService +) : BaseAddAccountRepository(proxySyncService) { + + class Payload( + val derivationPaths: AdvancedEncryption.DerivationPaths, + val addAccountType: AddAccountType, + val accountSource: AccountSecretsFactory.AccountSource + ) + + protected final suspend fun pickCryptoType(addAccountType: AddAccountType, advancedEncryption: AdvancedEncryption): CryptoType { + val cryptoType = if (addAccountType is AddAccountType.ChainAccount && chainRegistry.getChain(addAccountType.chainId).isEthereumBased) { + advancedEncryption.ethereumCryptoType + } else { + advancedEncryption.substrateCryptoType + } + + requireNotNull(cryptoType) { "Expected crypto type was null" } + + return cryptoType + } + + protected final suspend fun addSecretsAccount( + derivationPaths: AdvancedEncryption.DerivationPaths, + addAccountType: AddAccountType, + accountSource: AccountSecretsFactory.AccountSource + ): Long = withContext(Dispatchers.Default) { + when (addAccountType) { + is AddAccountType.MetaAccount -> { + addMetaAccount(derivationPaths, accountSource, addAccountType) + } + + is AddAccountType.ChainAccount -> { + addChainAccount(addAccountType, derivationPaths, accountSource) + } + } + } + + private suspend fun addMetaAccount( + derivationPaths: AdvancedEncryption.DerivationPaths, + accountSource: AccountSecretsFactory.AccountSource, + addAccountType: AddAccountType.MetaAccount + ): Long { + val (secrets, substrateCryptoType) = accountSecretsFactory.metaAccountSecrets( + substrateDerivationPath = derivationPaths.substrate, + ethereumDerivationPath = derivationPaths.ethereum, + accountSource = accountSource + ) + + return transformingInsertionErrors { + @Suppress("DEPRECATION") + accountDataSource.insertMetaAccountFromSecrets( + name = addAccountType.name, + substrateCryptoType = substrateCryptoType, + secrets = secrets + ) + } + } + + private suspend fun addChainAccount( + addAccountType: AddAccountType.ChainAccount, + derivationPaths: AdvancedEncryption.DerivationPaths, + accountSource: AccountSecretsFactory.AccountSource + ): Long { + val chain = chainRegistry.getChain(addAccountType.chainId) + + val derivationPath = if (chain.isEthereumBased) derivationPaths.ethereum else derivationPaths.substrate + + val (secrets, cryptoType) = accountSecretsFactory.chainAccountSecrets( + derivationPath = derivationPath, + accountSource = accountSource, + isEthereum = chain.isEthereumBased + ) + + transformingInsertionErrors { + @Suppress("DEPRECATION") + accountDataSource.insertChainAccount( + metaId = addAccountType.metaId, + chain = chain, + cryptoType = cryptoType, + secrets = secrets + ) + } + + return addAccountType.metaId + } + + private inline fun transformingInsertionErrors(action: () -> R) = try { + action() + } catch (_: SQLiteConstraintException) { + throw AccountAlreadyExistsException() + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SeedAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SeedAddAccountRepository.kt new file mode 100644 index 0000000000..fdafbcdc67 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/secrets/SeedAddAccountRepository.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets + +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption +import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +class SeedAddAccountRepository( + private val accountDataSource: AccountDataSource, + private val accountSecretsFactory: AccountSecretsFactory, + private val chainRegistry: ChainRegistry, + private val proxySyncService: ProxySyncService, +) : SecretsAddAccountRepository( + accountDataSource, + accountSecretsFactory, + chainRegistry, + proxySyncService +) { + + class Payload( + val seed: String, + val advancedEncryption: AdvancedEncryption, + val addAccountType: AddAccountType + ) + + override suspend fun addAccountInternal(payload: Payload): Long { + return addSecretsAccount( + derivationPaths = payload.advancedEncryption.derivationPaths, + addAccountType = payload.addAccountType, + accountSource = AccountSecretsFactory.AccountSource.Seed( + cryptoType = pickCryptoType(payload.addAccountType, payload.advancedEncryption), + seed = payload.seed + ) + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/watchOnly/WatchOnlyAddAccountRepository.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/watchOnly/WatchOnlyAddAccountRepository.kt new file mode 100644 index 0000000000..ac772a3dbb --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/repository/addAccount/watchOnly/WatchOnlyAddAccountRepository.kt @@ -0,0 +1,68 @@ +package io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly + +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.BaseAddAccountRepository +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class WatchOnlyAddAccountRepository( + private val accountDao: MetaAccountDao, + private val proxySyncService: ProxySyncService, +) : BaseAddAccountRepository(proxySyncService) { + + sealed interface Payload { + class MetaAccount( + val name: String, + val substrateAccountId: AccountId, + val ethereumAccountId: AccountId? + ) : Payload + + class ChainAccount( + val metaId: Long, + val chainId: ChainId, + val accountId: AccountId + ) : Payload + } + + override suspend fun addAccountInternal(payload: Payload): Long { + return when (payload) { + is Payload.MetaAccount -> addWatchOnlyWallet(payload) + is Payload.ChainAccount -> changeWatchOnlyChainAccount(payload) + } + } + + private suspend fun addWatchOnlyWallet(payload: Payload.MetaAccount): Long { + val metaAccount = MetaAccountLocal( + substratePublicKey = null, + substrateCryptoType = null, + substrateAccountId = payload.substrateAccountId, + ethereumPublicKey = null, + ethereumAddress = payload.ethereumAccountId, + name = payload.name, + parentMetaId = null, + isSelected = false, + position = accountDao.nextAccountPosition(), + type = MetaAccountLocal.Type.WATCH_ONLY, + status = MetaAccountLocal.Status.ACTIVE + ) + + return accountDao.insertMetaAccount(metaAccount) + } + + private suspend fun changeWatchOnlyChainAccount(payload: Payload.ChainAccount): Long { + val chainAccount = ChainAccountLocal( + metaId = payload.metaId, + chainId = payload.chainId, + accountId = payload.accountId, + cryptoType = null, + publicKey = null + ) + + accountDao.insertChainAccount(chainAccount) + + return payload.metaId + } +} 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 1b8d68824a..2cd361e60d 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 @@ -13,6 +13,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountAssetBalance import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountOrdering import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.scale.EncodableStruct import kotlinx.coroutines.flow.Flow @@ -39,16 +40,17 @@ interface AccountDataSource : SecretStoreV1 { suspend fun getSelectedMetaAccount(): MetaAccount fun selectedMetaAccountFlow(): Flow - suspend fun findMetaAccount(accountId: ByteArray): MetaAccount? - suspend fun accountNameFor(accountId: AccountId): String? + suspend fun findMetaAccount(accountId: ByteArray, chainId: ChainId): MetaAccount? - suspend fun allMetaAccounts(): List + suspend fun accountNameFor(accountId: AccountId, chainId: ChainId): String? suspend fun allLightMetaAccounts(): List fun allMetaAccountsFlow(): Flow> + fun activeMetaAccountsFlow(): Flow> + fun metaAccountsWithBalancesFlow(): Flow> fun metaAccountBalancesFlow(metaId: Long): Flow> @@ -59,7 +61,7 @@ interface AccountDataSource : SecretStoreV1 { suspend fun getSelectedLanguage(): Language suspend fun changeSelectedLanguage(language: Language) - suspend fun accountExists(accountId: AccountId): Boolean + suspend fun accountExists(accountId: AccountId, chainId: ChainId): Boolean suspend fun getMetaAccount(metaId: Long): MetaAccount fun metaAccountFlow(metaId: Long): Flow @@ -70,6 +72,8 @@ interface AccountDataSource : SecretStoreV1 { /** * @return id of inserted meta account */ + // TODO move it to SecretsAddAccountRepository + @Deprecated("Use SecretsAddAccountRepository instead") suspend fun insertMetaAccountFromSecrets( name: String, substrateCryptoType: CryptoType, @@ -79,6 +83,8 @@ interface AccountDataSource : SecretStoreV1 { /** * @return id of inserted meta account */ + // TODO move it to SecretsAddAccountRepository + @Deprecated("Use SecretsAddAccountRepository instead") suspend fun insertChainAccount( metaId: Long, chain: Chain, @@ -86,5 +92,9 @@ interface AccountDataSource : SecretStoreV1 { secrets: EncodableStruct ) - suspend fun hasMetaAccounts(): Boolean + suspend fun hasActiveMetaAccounts(): Boolean + + fun removeDeactivatedMetaAccounts() + + suspend fun getActiveMetaAccounts(): List } 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 88b92b95c4..36030a9cd4 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 @@ -15,9 +15,9 @@ import io.novafoundation.nova.core.model.Language import io.novafoundation.nova.core.model.Node import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NodeDao -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountPositionUpdate +import io.novafoundation.nova.core_db.model.chain.account.ChainAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountPositionUpdate import io.novafoundation.nova.feature_account_api.domain.model.Account import io.novafoundation.nova.feature_account_api.domain.model.AuthType import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount @@ -31,6 +31,7 @@ import io.novafoundation.nova.feature_account_impl.data.repository.datasource.mi import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.fearless_utils.extensions.asEthereumPublicKey import jp.co.soramitsu.fearless_utils.extensions.toAccountId import jp.co.soramitsu.fearless_utils.runtime.AccountId @@ -132,17 +133,18 @@ class AccountDataSourceImpl( override fun selectedMetaAccountFlow(): Flow = selectedMetaAccountFlow - override suspend fun findMetaAccount(accountId: ByteArray): MetaAccount? { - return metaAccountDao.getMetaAccountInfo(accountId) + override suspend fun findMetaAccount(accountId: ByteArray, chainId: ChainId): MetaAccount? { + return metaAccountDao.getMetaAccountInfo(accountId, chainId) ?.let(::mapMetaAccountLocalToMetaAccount) } - override suspend fun accountNameFor(accountId: AccountId): String? { - return metaAccountDao.metaAccountNameFor(accountId) + override suspend fun accountNameFor(accountId: AccountId, chainId: ChainId): String? { + return metaAccountDao.metaAccountNameFor(accountId, chainId) } - override suspend fun allMetaAccounts(): List { - return metaAccountDao.getJoinedMetaAccountsInfo().map(::mapMetaAccountLocalToMetaAccount) + override suspend fun getActiveMetaAccounts(): List { + return metaAccountDao.getMetaAccountsInfoByStatus(MetaAccountLocal.Status.ACTIVE) + .map(::mapMetaAccountLocalToMetaAccount) } override suspend fun allLightMetaAccounts(): List { @@ -155,6 +157,13 @@ class AccountDataSourceImpl( } } + override fun activeMetaAccountsFlow(): Flow> { + return metaAccountDao.getJoinedMetaAccountsInfoByStatusFlow(MetaAccountLocal.Status.ACTIVE) + .map { accountsLocal -> + accountsLocal.map(::mapMetaAccountLocalToMetaAccount) + } + } + override fun metaAccountsWithBalancesFlow(): Flow> { return metaAccountDao.metaAccountsWithBalanceFlow().mapList(::mapMetaAccountWithBalanceFromLocal) } @@ -183,8 +192,8 @@ class AccountDataSourceImpl( preferences.saveCurrentLanguage(language.iso) } - override suspend fun accountExists(accountId: AccountId): Boolean { - return metaAccountDao.isMetaAccountExists(accountId) + override suspend fun accountExists(accountId: AccountId, chainId: ChainId): Boolean { + return metaAccountDao.isMetaAccountExists(accountId, chainId) } override suspend fun getMetaAccount(metaId: Long): MetaAccount { @@ -226,9 +235,11 @@ class AccountDataSourceImpl( ethereumPublicKey = ethereumPublicKey, ethereumAddress = ethereumPublicKey?.asEthereumPublicKey()?.toAccountId()?.value, name = name, + parentMetaId = null, isSelected = false, position = metaAccountDao.nextAccountPosition(), - type = MetaAccountLocal.Type.SECRETS + type = MetaAccountLocal.Type.SECRETS, + status = MetaAccountLocal.Status.ACTIVE ) val metaId = metaAccountDao.insertMetaAccount(metaAccountLocal) @@ -258,8 +269,12 @@ class AccountDataSourceImpl( secretStoreV2.putChainAccountSecrets(metaId, accountId, secrets) } - override suspend fun hasMetaAccounts(): Boolean { - return metaAccountDao.hasMetaAccounts() + override suspend fun hasActiveMetaAccounts(): Boolean { + return metaAccountDao.hasMetaAccountsByStatus(MetaAccountLocal.Status.ACTIVE) + } + + override fun removeDeactivatedMetaAccounts() { + metaAccountDao.removeMetaAccountsByStatus(MetaAccountLocal.Status.DEACTIVATED) } private inline fun async(crossinline action: suspend () -> Unit) { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/FeeSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/DefaultFeeSigner.kt similarity index 55% rename from runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/FeeSigner.kt rename to feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/DefaultFeeSigner.kt index bbda80aad7..0b08f12712 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/FeeSigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/DefaultFeeSigner.kt @@ -1,37 +1,58 @@ -package io.novafoundation.nova.runtime.extrinsic +package io.novafoundation.nova.feature_account_impl.data.signer +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.runtime.ext.accountIdOf +import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.encrypt.EncryptionType import jp.co.soramitsu.fearless_utils.encrypt.MultiChainEncryption -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper import jp.co.soramitsu.fearless_utils.encrypt.keypair.Keypair import jp.co.soramitsu.fearless_utils.encrypt.keypair.ethereum.EthereumKeypairFactory import jp.co.soramitsu.fearless_utils.encrypt.keypair.substrate.SubstrateKeypairFactory +import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.KeyPairSigner -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw private val FAKE_CRYPTO_TYPE = EncryptionType.ECDSA -class FeeSigner(private val chain: Chain) : Signer { +class DefaultFeeSigner( + private val realMetaAccount: MetaAccount, + private val chain: Chain +) : FeeSigner { - private val keypair = generateFakeKeyPair() + private val fakeKeyPair = generateFakeKeyPair() - override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignatureWrapper { - val signer = KeyPairSigner(keypair, multiChainEncryption()) + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { + val signer = KeyPairSigner(fakeKeyPair, multiChainEncryption()) return signer.signExtrinsic(payloadExtrinsic) } - override suspend fun signRaw(payload: SignerPayloadRaw): SignatureWrapper { - val signer = KeyPairSigner(keypair, multiChainEncryption()) + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { + val signer = KeyPairSigner(fakeKeyPair, multiChainEncryption()) return signer.signRaw(payload) } - fun accountId() = chain.accountIdOf(keypair.publicKey) + override suspend fun actualFeeSignerId(chain: Chain): AccountId { + return requestedFeeSignerId(chain) + } + + override suspend fun requestedFeeSignerId(chain: Chain): AccountId { + return realMetaAccount.requireAccountIdIn(chain) + } + + override suspend fun signerAccountId(chain: Chain): ByteArray { + require(chain.id == this.chain.id) { + "Signer was created for the different chain, expected ${this.chain.name}, got ${chain.name}" + } + + return chain.accountIdOf(fakeKeyPair.publicKey) + } private fun multiChainEncryption() = if (chain.isEthereumBased) { MultiChainEncryption.Ethereum diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/LeafSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/LeafSigner.kt new file mode 100644 index 0000000000..0cc896cc2c --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/LeafSigner.kt @@ -0,0 +1,16 @@ +package io.novafoundation.nova.feature_account_impl.data.signer + +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +abstract class LeafSigner( + private val metaAccount: MetaAccount, +) : NovaSigner { + + override suspend fun signerAccountId(chain: Chain): AccountId { + return metaAccount.requireAccountIdIn(chain) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/RealSignerProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/RealSignerProvider.kt index f2a990b7cc..b59234f62f 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/RealSignerProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/RealSignerProvider.kt @@ -3,34 +3,53 @@ package io.novafoundation.nova.feature_account_impl.data.signer import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount -import io.novafoundation.nova.feature_account_impl.data.signer.ledger.LedgerSigner -import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.ParitySignerSigner -import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultSigner +import io.novafoundation.nova.feature_account_impl.data.signer.ledger.LedgerSignerFactory +import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignerFactory +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxiedFeeSignerFactory +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxiedSignerFactory import io.novafoundation.nova.feature_account_impl.data.signer.secrets.SecretsSignerFactory -import io.novafoundation.nova.feature_account_impl.data.signer.watchOnly.WatchOnlySigner -import io.novafoundation.nova.runtime.extrinsic.FeeSigner +import io.novafoundation.nova.feature_account_impl.data.signer.watchOnly.WatchOnlySignerFactory +import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer internal class RealSignerProvider( private val secretsSignerFactory: SecretsSignerFactory, - private val watchOnlySigner: WatchOnlySigner, - private val paritySignerSigner: ParitySignerSigner, - private val polkadotVaultSigner: PolkadotVaultSigner, - private val ledgerSigner: LedgerSigner, + private val proxiedSignerFactory: ProxiedSignerFactory, + private val watchOnlySigner: WatchOnlySignerFactory, + private val polkadotVaultSignerFactory: PolkadotVaultVariantSignerFactory, + private val proxiedFeeSignerFactory: ProxiedFeeSignerFactory, + private val ledgerSignerFactory: LedgerSignerFactory, ) : SignerProvider { - override fun signerFor(metaAccount: MetaAccount): Signer { + override fun rootSignerFor(metaAccount: MetaAccount): NovaSigner { + return signerFor(metaAccount, isRoot = true) + } + + override fun nestedSignerFor(metaAccount: MetaAccount): NovaSigner { + return signerFor(metaAccount, isRoot = false) + } + + override fun feeSigner(metaAccount: MetaAccount, chain: Chain): FeeSigner { return when (metaAccount.type) { - LightMetaAccount.Type.SECRETS -> secretsSignerFactory.create(metaAccount) - LightMetaAccount.Type.WATCH_ONLY -> watchOnlySigner - LightMetaAccount.Type.PARITY_SIGNER -> paritySignerSigner - LightMetaAccount.Type.POLKADOT_VAULT -> polkadotVaultSigner - LightMetaAccount.Type.LEDGER -> ledgerSigner + LightMetaAccount.Type.SECRETS, + LightMetaAccount.Type.WATCH_ONLY, + LightMetaAccount.Type.PARITY_SIGNER, + LightMetaAccount.Type.POLKADOT_VAULT, + LightMetaAccount.Type.LEDGER -> DefaultFeeSigner(metaAccount, chain) + + LightMetaAccount.Type.PROXIED -> proxiedFeeSignerFactory.create(metaAccount, chain, this) } } - override fun feeSigner(chain: Chain): Signer { - return FeeSigner(chain) + private fun signerFor(metaAccount: MetaAccount, isRoot: Boolean): NovaSigner { + return when (metaAccount.type) { + LightMetaAccount.Type.SECRETS -> secretsSignerFactory.create(metaAccount) + LightMetaAccount.Type.WATCH_ONLY -> watchOnlySigner.create(metaAccount) + LightMetaAccount.Type.PARITY_SIGNER -> polkadotVaultSignerFactory.createParitySigner(metaAccount) + LightMetaAccount.Type.POLKADOT_VAULT -> polkadotVaultSignerFactory.createPolkadotVault(metaAccount) + LightMetaAccount.Type.LEDGER -> ledgerSignerFactory.create(metaAccount) + LightMetaAccount.Type.PROXIED -> proxiedSignerFactory.create(metaAccount, this, isRoot) + } } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/SeparateFlowSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/SeparateFlowSigner.kt index a93e1b29b0..e8d3a5eee0 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/SeparateFlowSigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/SeparateFlowSigner.kt @@ -1,24 +1,28 @@ package io.novafoundation.nova.feature_account_impl.data.signer import io.novafoundation.nova.common.base.errors.SigningCancelledException -import io.novafoundation.nova.common.utils.MutableSharedState +import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenCommunicator import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenRequester import io.novafoundation.nova.feature_account_api.presenatation.sign.SignatureWrapper import io.novafoundation.nova.feature_account_api.presenatation.sign.awaitConfirmation -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext abstract class SeparateFlowSigner( - private val signingSharedState: MutableSharedState, + private val signingSharedState: SigningSharedState, private val signFlowRequester: SignInterScreenRequester, -) : Signer { + private val metaAccount: MetaAccount +) : LeafSigner(metaAccount) { - override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignatureWrapper { - signingSharedState.set(payloadExtrinsic) + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { + val payload = SeparateFlowSignerState(payloadExtrinsic, metaAccount) + + signingSharedState.set(payload) val result = withContext(Dispatchers.Main) { try { @@ -29,7 +33,10 @@ abstract class SeparateFlowSigner( } if (result is SignInterScreenCommunicator.Response.Signed) { - return SignatureWrapper(result.signature) + return SignedExtrinsic( + payloadExtrinsic, + SignatureWrapper(result.signature) + ) } else { throw SigningCancelledException() } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/ledger/LedgerSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/ledger/LedgerSigner.kt index 48e3b60ce1..e85beeb8c3 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/ledger/LedgerSigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/ledger/LedgerSigner.kt @@ -2,23 +2,42 @@ package io.novafoundation.nova.feature_account_impl.data.signer.ledger import io.novafoundation.nova.common.base.errors.SigningCancelledException import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.MutableSharedState +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenRequester import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.data.signer.SeparateFlowSigner import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw +class LedgerSignerFactory( + private val signingSharedState: SigningSharedState, + private val signFlowRequester: SignInterScreenRequester, + private val resourceManager: ResourceManager, + private val messageSigningNotSupported: SigningNotSupportedPresentable +) { + + fun create(metaAccount: MetaAccount): LedgerSigner { + return LedgerSigner( + metaAccount = metaAccount, + signingSharedState = signingSharedState, + signFlowRequester = signFlowRequester, + resourceManager = resourceManager, + messageSigningNotSupported = messageSigningNotSupported + ) + } +} + class LedgerSigner( - signingSharedState: MutableSharedState, + metaAccount: MetaAccount, + signingSharedState: SigningSharedState, signFlowRequester: SignInterScreenRequester, private val resourceManager: ResourceManager, private val messageSigningNotSupported: SigningNotSupportedPresentable -) : SeparateFlowSigner(signingSharedState, signFlowRequester) { +) : SeparateFlowSigner(signingSharedState, signFlowRequester, metaAccount) { - override suspend fun signRaw(payload: SignerPayloadRaw): SignatureWrapper { + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { messageSigningNotSupported.presentSigningNotSupported( SigningNotSupportedPresentable.Payload( iconRes = R.drawable.ic_ledger, diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/paritySigner/PolkadotVaultSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/paritySigner/PolkadotVaultSigner.kt index e44f2af127..08cae450ac 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/paritySigner/PolkadotVaultSigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/paritySigner/PolkadotVaultSigner.kt @@ -2,33 +2,67 @@ package io.novafoundation.nova.feature_account_impl.data.signer.paritySigner import io.novafoundation.nova.common.base.errors.SigningCancelledException import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.MutableSharedState +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.formatWithPolkadotVaultLabel import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.data.signer.SeparateFlowSigner import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw +class PolkadotVaultVariantSignerFactory( + private val signingSharedState: SigningSharedState, + private val signFlowRequester: PolkadotVaultVariantSignCommunicator, + private val resourceManager: ResourceManager, + private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + private val messageSigningNotSupported: SigningNotSupportedPresentable +) { + + fun createPolkadotVault(metaAccount: MetaAccount): PolkadotVaultSigner { + return PolkadotVaultSigner( + signingSharedState = signingSharedState, + metaAccount = metaAccount, + signFlowRequester = signFlowRequester, + resourceManager = resourceManager, + polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider, + messageSigningNotSupported = messageSigningNotSupported + ) + } + + fun createParitySigner(metaAccount: MetaAccount): ParitySignerSigner { + return ParitySignerSigner( + signingSharedState = signingSharedState, + metaAccount = metaAccount, + signFlowRequester = signFlowRequester, + resourceManager = resourceManager, + polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider, + messageSigningNotSupported = messageSigningNotSupported + ) + } +} + abstract class PolkadotVaultVariantSigner( - signingSharedState: MutableSharedState, + signingSharedState: SigningSharedState, + metaAccount: MetaAccount, private val signFlowRequester: PolkadotVaultVariantSignCommunicator, private val resourceManager: ResourceManager, private val variant: PolkadotVaultVariant, private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, private val messageSigningNotSupported: SigningNotSupportedPresentable -) : SeparateFlowSigner(signingSharedState, signFlowRequester) { +) : SeparateFlowSigner(signingSharedState, signFlowRequester, metaAccount) { - override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignatureWrapper { + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { signFlowRequester.setUsedVariant(variant) return super.signExtrinsic(payloadExtrinsic) } - override suspend fun signRaw(payload: SignerPayloadRaw): SignatureWrapper { + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { val config = polkadotVaultVariantConfigProvider.variantConfigFor(variant) messageSigningNotSupported.presentSigningNotSupported( @@ -43,13 +77,15 @@ abstract class PolkadotVaultVariantSigner( } class ParitySignerSigner( - signingSharedState: MutableSharedState, + signingSharedState: SigningSharedState, + metaAccount: MetaAccount, signFlowRequester: PolkadotVaultVariantSignCommunicator, resourceManager: ResourceManager, polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, - messageSigningNotSupported: SigningNotSupportedPresentable + messageSigningNotSupported: SigningNotSupportedPresentable, ) : PolkadotVaultVariantSigner( signingSharedState = signingSharedState, + metaAccount = metaAccount, signFlowRequester = signFlowRequester, resourceManager = resourceManager, variant = PolkadotVaultVariant.PARITY_SIGNER, @@ -58,13 +94,15 @@ class ParitySignerSigner( ) class PolkadotVaultSigner( - signingSharedState: MutableSharedState, + signingSharedState: SigningSharedState, + metaAccount: MetaAccount, signFlowRequester: PolkadotVaultVariantSignCommunicator, resourceManager: ResourceManager, polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, messageSigningNotSupported: SigningNotSupportedPresentable ) : PolkadotVaultVariantSigner( signingSharedState = signingSharedState, + metaAccount = metaAccount, signFlowRequester = signFlowRequester, resourceManager = resourceManager, variant = PolkadotVaultVariant.POLKADOT_VAULT, diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedFeeSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedFeeSigner.kt new file mode 100644 index 0000000000..80b247b602 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedFeeSigner.kt @@ -0,0 +1,99 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy + +import io.novafoundation.nova.common.utils.toCallInstance +import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw +import java.math.BigInteger + +class ProxiedFeeSignerFactory( + private val accountRepository: AccountRepository +) { + + fun create(metaAccount: MetaAccount, chain: Chain, signerProvider: SignerProvider): ProxiedFeeSigner { + return ProxiedFeeSigner( + proxiedMetaAccount = metaAccount, + chain = chain, + signerProvider = signerProvider, + accountRepository = accountRepository, + ) + } +} + +class ProxiedFeeSigner( + private val proxiedMetaAccount: MetaAccount, + private val chain: Chain, + private val signerProvider: SignerProvider, + private val accountRepository: AccountRepository, +) : FeeSigner { + + private var proxyMetaAccount: MetaAccount? = null + private var delegate: FeeSigner? = null + + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { + val delegator = getDelegator() + + val callInstance = payloadExtrinsic.call.toCallInstance() + return if (callInstance == null) { + delegator.signExtrinsic(payloadExtrinsic) + } else { + val modifiedPayloadExtrinsic = payloadExtrinsic.wrapIntoProxyPayload( + proxyAccountId = getProxyAccountId(), + currentProxyNonce = BigInteger.ZERO, + proxyType = ProxyAccount.ProxyType.Any, + callInstance = callInstance + ) + + delegator.signExtrinsic(modifiedPayloadExtrinsic) + } + } + + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { + return getDelegator().signRaw(payload) + } + + override suspend fun actualFeeSignerId(chain: Chain): AccountId { + return getDelegator().actualFeeSignerId(chain) + } + + override suspend fun requestedFeeSignerId(chain: Chain): AccountId { + return proxiedMetaAccount.requireAccountIdIn(chain) + } + + override suspend fun signerAccountId(chain: Chain): AccountId { + require(chain.id == this.chain.id) { + "Signer was created for the different chain, expected ${this.chain.name}, got ${chain.name}" + } + + return getDelegator().signerAccountId(chain) + } + + private suspend fun getProxyAccountId(): ByteArray { + return getProxyMetaAccount().requireAccountIdIn(chain) + } + + private suspend fun getDelegator(): FeeSigner { + if (delegate == null) { + delegate = signerProvider.feeSigner(getProxyMetaAccount(), chain) + } + + return delegate!! + } + + private suspend fun getProxyMetaAccount(): MetaAccount { + if (proxyMetaAccount == null) { + proxyMetaAccount = accountRepository.getMetaAccount(proxiedMetaAccount.proxy!!.metaId) + } + + return requireNotNull(proxyMetaAccount) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedSigner.kt new file mode 100644 index 0000000000..745d2ba107 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxiedSigner.kt @@ -0,0 +1,179 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy + +import io.novafoundation.nova.common.base.errors.SigningCancelledException +import io.novafoundation.nova.common.utils.chainId +import io.novafoundation.nova.common.utils.toCallInstance +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxiedExtrinsicValidationFailure.ProxyNotEnoughFee +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxiedExtrinsicValidationPayload +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus +import io.novafoundation.nova.feature_account_api.data.repository.ProxyRepository +import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount.ProxyType +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn +import io.novafoundation.nova.feature_account_api.presenatation.account.proxy.ProxySigningPresenter +import io.novafoundation.nova.runtime.ext.commissionAsset +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.network.rpc.RpcCalls +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw + +class ProxiedSignerFactory( + private val chainRegistry: ChainRegistry, + private val accountRepository: AccountRepository, + private val proxySigningPresenter: ProxySigningPresenter, + private val proxyRepository: ProxyRepository, + private val rpcCalls: RpcCalls, + private val proxyExtrinsicValidationEventBus: ProxyExtrinsicValidationRequestBus, + private val proxyCallFilterFactory: ProxyCallFilterFactory +) { + + fun create(metaAccount: MetaAccount, signerProvider: SignerProvider, isRoot: Boolean): ProxiedSigner { + return ProxiedSigner( + proxiedMetaAccount = metaAccount, + chainRegistry = chainRegistry, + accountRepository = accountRepository, + signerProvider = signerProvider, + proxySigningPresenter = proxySigningPresenter, + proxyRepository = proxyRepository, + rpcCalls = rpcCalls, + proxyExtrinsicValidationEventBus = proxyExtrinsicValidationEventBus, + isRootProxied = isRoot, + proxyCallFilterFactory = proxyCallFilterFactory + ) + } +} + +class ProxiedSigner( + private val proxiedMetaAccount: MetaAccount, + private val chainRegistry: ChainRegistry, + private val accountRepository: AccountRepository, + private val signerProvider: SignerProvider, + private val proxySigningPresenter: ProxySigningPresenter, + private val proxyRepository: ProxyRepository, + private val rpcCalls: RpcCalls, + private val proxyExtrinsicValidationEventBus: ProxyExtrinsicValidationRequestBus, + private val isRootProxied: Boolean, + private val proxyCallFilterFactory: ProxyCallFilterFactory +) : NovaSigner { + + override suspend fun signerAccountId(chain: Chain): AccountId { + val proxyMetaAccount = getProxyMetaAccount() + val delegate = createDelegate(proxyMetaAccount) + + return delegate.signerAccountId(chain) + } + + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { + val chain = chainRegistry.getChain(payloadExtrinsic.chainId) + val proxyMetaAccount = getProxyMetaAccount() + + acknowledgeProxyOperation(proxyMetaAccount) + + // TODO this wont use the actual payload for fee validation when multiple nested proxies are used + // We need to design a universal solution + // We actually can use `signedExtrinsic.payload` to access actual payload but in this case validation will happen only after signing + // which will have bad UX with Vault and Ledger. + // As an option we could separate signing and wrapping step specifically for such nested signers and use only the wrapping step before fee validation + val modifiedPayload = modifyPayload(proxyMetaAccount, payloadExtrinsic, chain) + + if (isRootProxied) { + validateExtrinsic(modifiedPayload, chain) + } + + val delegate = createDelegate(proxyMetaAccount) + + val signedExtrinsic = delegate.signExtrinsic(modifiedPayload) + return signedExtrinsic + } + + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { + signingNotSupported() + } + + private fun createDelegate(proxyMetaAccount: MetaAccount): NovaSigner { + return signerProvider.nestedSignerFor(proxyMetaAccount) + } + + private suspend fun validateExtrinsic(extrinsicPayload: SignerPayloadExtrinsic, chain: Chain) { + val proxyAccountId = signerAccountId(chain) + val proxyAccount = accountRepository.findMetaAccount(proxyAccountId, chain.id) ?: throw IllegalStateException("Proxy account is not found") + + val callInstance = extrinsicPayload.call.toCallInstance() ?: signingNotSupported() + + val validationPayload = ProxiedExtrinsicValidationPayload( + proxyAccount, + proxyAccountId, + ChainWithAsset(chain, chain.commissionAsset), + callInstance.call + ) + + val requestBusPayload = ProxyExtrinsicValidationRequestBus.Request(validationPayload) + val validationResponse = proxyExtrinsicValidationEventBus.handle(requestBusPayload) + + val validationStatus = validationResponse.validationResult.getOrNull() + if (validationStatus is ValidationStatus.NotValid && validationStatus.reason is ProxyNotEnoughFee) { + val reason = validationStatus.reason as ProxyNotEnoughFee + proxySigningPresenter.notEnoughFee(reason.metaAccount, reason.asset, reason.availableBalance, reason.fee) + + throw SigningCancelledException() + } + } + + private suspend fun modifyPayload(proxyMetaAccount: MetaAccount, payload: SignerPayloadExtrinsic, chain: Chain): SignerPayloadExtrinsic { + val proxyAccountId = proxyMetaAccount.requireAccountIdIn(chain) + val proxiedAccountId = proxiedMetaAccount.requireAccountIdIn(chain) + + val availableProxyTypes = proxyRepository.getDelegatedProxyTypes( + chainId = payload.chainId, + proxiedAccountId = proxiedAccountId, + proxyAccountId = proxyAccountId + ) + + val callInstance = payload.call.toCallInstance() ?: signingNotSupported() + + val proxyType = proxyCallFilterFactory.getFirstMatchedTypeOrNull(callInstance.call, availableProxyTypes) + ?: notEnoughPermission(proxyMetaAccount, availableProxyTypes) + + val proxyAddress = proxyMetaAccount.requireAddressIn(chain) + val nonce = rpcCalls.getNonce(payload.chainId, proxyAddress) + + return payload.wrapIntoProxyPayload( + proxyAccountId = proxyAccountId, + proxyType = proxyType, + callInstance = callInstance, + currentProxyNonce = nonce + ) + } + + private suspend fun acknowledgeProxyOperation(proxyMetaAccount: MetaAccount) { + val resume = proxySigningPresenter.acknowledgeProxyOperation(proxiedMetaAccount, proxyMetaAccount) + if (!resume) { + throw SigningCancelledException() + } + } + + private suspend fun getProxyMetaAccount(): MetaAccount { + val proxyAccount = proxiedMetaAccount.proxy ?: throw IllegalStateException("Proxy account is not found") + return accountRepository.getMetaAccount(proxyAccount.metaId) + } + + private suspend fun notEnoughPermission(proxyMetaAccount: MetaAccount, availableProxyTypes: List): Nothing { + proxySigningPresenter.notEnoughPermission(proxiedMetaAccount, proxyMetaAccount, availableProxyTypes) + throw SigningCancelledException() + } + + private suspend fun signingNotSupported(): Nothing { + proxySigningPresenter.signingIsNotSupported() + throw SigningCancelledException() + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxyCallFilterFactory.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxyCallFilterFactory.kt new file mode 100644 index 0000000000..df6404d7cb --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/ProxyCallFilterFactory.kt @@ -0,0 +1,95 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy + +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.CallFilter +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.AnyOfCallFilter +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.EverythingFilter +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter.WhiteListFilter +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +class ProxyCallFilterFactory { + + fun getCallFilterFor(proxyType: ProxyAccount.ProxyType): CallFilter { + return when (proxyType) { + ProxyAccount.ProxyType.Any, + is ProxyAccount.ProxyType.Other -> EverythingFilter() + + ProxyAccount.ProxyType.NonTransfer -> AnyOfCallFilter( + WhiteListFilter(Modules.SYSTEM), + WhiteListFilter(Modules.SCHEDULER), + WhiteListFilter(Modules.BABE), + WhiteListFilter(Modules.TIMESTAMP), + WhiteListFilter(Modules.INDICES, listOf("claim", "free", "freeze")), + WhiteListFilter(Modules.STAKING), + WhiteListFilter(Modules.SESSION), + WhiteListFilter(Modules.GRANDPA), + WhiteListFilter(Modules.IM_ONLINE), + WhiteListFilter(Modules.TREASURY), + WhiteListFilter(Modules.BOUNTIES), + WhiteListFilter(Modules.CHILD_BOUNTIES), + WhiteListFilter(Modules.CONVICTION_VOTING), + WhiteListFilter(Modules.REFERENDA), + WhiteListFilter(Modules.WHITELIST), + WhiteListFilter(Modules.CLAIMS), + WhiteListFilter(Modules.VESTING, listOf("vest", "vest_other")), + WhiteListFilter(Modules.UTILITY), + WhiteListFilter(Modules.IDENTITY), + WhiteListFilter(Modules.PROXY), + WhiteListFilter(Modules.MULTISIG), + WhiteListFilter(Modules.REGISTRAR, listOf("register", "deregister", "reserve")), + WhiteListFilter(Modules.CROWDLOAN), + WhiteListFilter(Modules.SLOTS), + WhiteListFilter(Modules.AUCTIONS), + WhiteListFilter(Modules.VOTER_LIST), + WhiteListFilter(Modules.NOMINATION_POOLS), + WhiteListFilter(Modules.FAST_UNSTAKE) + ) + + ProxyAccount.ProxyType.Governance -> AnyOfCallFilter( + WhiteListFilter(Modules.TREASURY), + WhiteListFilter(Modules.BOUNTIES), + WhiteListFilter(Modules.UTILITY), + WhiteListFilter(Modules.CHILD_BOUNTIES), + WhiteListFilter(Modules.CONVICTION_VOTING), + WhiteListFilter(Modules.REFERENDA), + WhiteListFilter(Modules.WHITELIST) + ) + + ProxyAccount.ProxyType.Staking -> AnyOfCallFilter( + WhiteListFilter(Modules.STAKING), + WhiteListFilter(Modules.SESSION), + WhiteListFilter(Modules.UTILITY), + WhiteListFilter(Modules.FAST_UNSTAKE), + WhiteListFilter(Modules.VOTER_LIST), + WhiteListFilter(Modules.NOMINATION_POOLS) + ) + + ProxyAccount.ProxyType.NominationPools -> AnyOfCallFilter( + WhiteListFilter(Modules.NOMINATION_POOLS), + WhiteListFilter(Modules.UTILITY) + ) + + ProxyAccount.ProxyType.IdentityJudgement -> AnyOfCallFilter( + WhiteListFilter(Modules.IDENTITY, listOf("provide_judgement")), + WhiteListFilter(Modules.UTILITY) + ) + + ProxyAccount.ProxyType.CancelProxy -> WhiteListFilter(Modules.PROXY, listOf("reject_announcement")) + + ProxyAccount.ProxyType.Auction -> AnyOfCallFilter( + WhiteListFilter(Modules.AUCTIONS), + WhiteListFilter(Modules.CROWDLOAN), + WhiteListFilter(Modules.REGISTRAR), + WhiteListFilter(Modules.SLOTS) + ) + } + } +} + +fun ProxyCallFilterFactory.getFirstMatchedTypeOrNull(call: GenericCall.Instance, proxyTypes: List): ProxyAccount.ProxyType? { + return proxyTypes.firstOrNull { + val callFilter = this.getCallFilterFor(it) + callFilter.canExecute(call) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/SignerPayloadModifierExt.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/SignerPayloadModifierExt.kt new file mode 100644 index 0000000000..12ede274f2 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/SignerPayloadModifierExt.kt @@ -0,0 +1,51 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy + +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.runtime.extrinsic.multi.SimpleCallBuilder +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.DefaultSignedExtensions +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.instances.AddressInstanceConstructor +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.encodeNonce +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.replaceBaseNone +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic +import java.math.BigInteger + +fun SignerPayloadExtrinsic.wrapIntoProxyPayload( + proxyAccountId: AccountId, + currentProxyNonce: BigInteger, + proxyType: ProxyAccount.ProxyType, + callInstance: CallRepresentation.Instance +): SignerPayloadExtrinsic { + val proxiedAccountId = accountId + val callBuilder = SimpleCallBuilder(runtime) + callBuilder.addCall( + moduleName = Modules.PROXY, + callName = "proxy", + arguments = mapOf( + "real" to AddressInstanceConstructor.constructInstance(runtime.typeRegistry, proxiedAccountId), + "force_proxy_type" to DictEnum.Entry(proxyType.name, null), + "call" to callInstance.call + ) + ) + + val newNonce = nonce.replaceBaseNone(currentProxyNonce) + + return copy( + accountId = proxyAccountId, + call = CallRepresentation.Instance(callBuilder.calls.first()), + signedExtras = signedExtras.modifyNonce(runtime, newNonce.nonce), + nonce = newNonce + ) +} + +fun Map.modifyNonce(runtimeSnapshot: RuntimeSnapshot, newNonce: BigInteger): Map { + return buildMap { + putAll(this@modifyNonce) + + put(DefaultSignedExtensions.CHECK_NONCE, runtimeSnapshot.encodeNonce(newNonce)) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/AnyOfCallFilter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/AnyOfCallFilter.kt new file mode 100644 index 0000000000..637ad90e6e --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/AnyOfCallFilter.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter + +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +class AnyOfCallFilter( + private val filters: List +) : CallFilter { + + constructor(vararg filters: CallFilter) : this(filters.toList()) + + override fun canExecute(call: GenericCall.Instance): Boolean { + return filters.any { it.canExecute(call) } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/CallFilter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/CallFilter.kt new file mode 100644 index 0000000000..24c5ec2188 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/CallFilter.kt @@ -0,0 +1,7 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter + +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +interface CallFilter { + fun canExecute(call: GenericCall.Instance): Boolean +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/EverythingFilter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/EverythingFilter.kt new file mode 100644 index 0000000000..326e6b2b05 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/EverythingFilter.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter + +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +class EverythingFilter : CallFilter { + + override fun canExecute(call: GenericCall.Instance): Boolean { + return true + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/WhiteListFilter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/WhiteListFilter.kt new file mode 100644 index 0000000000..e5029ac459 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/proxy/callFilter/WhiteListFilter.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.feature_account_impl.data.signer.proxy.callFilter + +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +class WhiteListFilter(private val matchingModule: String, private val matchingCalls: List?) : CallFilter { + + constructor(matchingModule: String) : this(matchingModule, null) + + override fun canExecute(call: GenericCall.Instance): Boolean { + val callModule = call.module.name + val callName = call.function.name + + if (matchingModule == callModule) { + if (matchingCalls == null) return true + if (matchingCalls.contains(callName)) return true + } + + return false + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt index ec29940ac6..e1b35e6d84 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt @@ -8,13 +8,14 @@ import io.novafoundation.nova.common.sequrity.TwoFactorVerificationResult import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.multiChainEncryptionFor +import io.novafoundation.nova.feature_account_impl.data.signer.LeafSigner import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chainsById import jp.co.soramitsu.fearless_utils.encrypt.MultiChainEncryption -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.KeyPairSigner -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw @@ -25,7 +26,12 @@ class SecretsSignerFactory( ) { fun create(metaAccount: MetaAccount): SecretsSigner { - return SecretsSigner(metaAccount, secretStoreV2, chainRegistry, twoFactorVerificationService) + return SecretsSigner( + metaAccount = metaAccount, + secretStoreV2 = secretStoreV2, + chainRegistry = chainRegistry, + twoFactorVerificationService = twoFactorVerificationService + ) } } @@ -33,17 +39,17 @@ class SecretsSigner( private val metaAccount: MetaAccount, private val secretStoreV2: SecretStoreV2, private val chainRegistry: ChainRegistry, - private val twoFactorVerificationService: TwoFactorVerificationService -) : Signer { + private val twoFactorVerificationService: TwoFactorVerificationService, +) : LeafSigner(metaAccount) { - override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignatureWrapper { + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { runTwoFactorVerificationIfEnabled() val delegate = createDelegate(payloadExtrinsic.accountId) return delegate.signExtrinsic(payloadExtrinsic) } - override suspend fun signRaw(payload: SignerPayloadRaw): SignatureWrapper { + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { runTwoFactorVerificationIfEnabled() val delegate = createDelegate(payload.accountId) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/watchOnly/WatchOnlySigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/watchOnly/WatchOnlySigner.kt index 512b7b4669..2bfac7055d 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/watchOnly/WatchOnlySigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/watchOnly/WatchOnlySigner.kt @@ -1,21 +1,33 @@ package io.novafoundation.nova.feature_account_impl.data.signer.watchOnly import io.novafoundation.nova.common.base.errors.SigningCancelledException +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer +import io.novafoundation.nova.feature_account_impl.data.signer.LeafSigner +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedExtrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignedRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw +class WatchOnlySignerFactory( + private val watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter, +) { + + fun create(metaAccount: MetaAccount): WatchOnlySigner { + return WatchOnlySigner(watchOnlySigningPresenter, metaAccount) + } +} + class WatchOnlySigner( - private val watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter -) : Signer { + private val watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter, + metaAccount: MetaAccount +) : LeafSigner(metaAccount) { - override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignatureWrapper { + override suspend fun signExtrinsic(payloadExtrinsic: SignerPayloadExtrinsic): SignedExtrinsic { cannotSign() } - override suspend fun signRaw(payload: SignerPayloadRaw): SignatureWrapper { + override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw { cannotSign() } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt index d71623ecc0..624415e608 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.account.advancedEncryption.di.AdvancedEncryptionComponent import io.novafoundation.nova.feature_account_impl.presentation.account.create.di.CreateAccountComponent import io.novafoundation.nova.feature_account_impl.presentation.account.details.di.AccountDetailsComponent +import io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates.di.DelegatedAccountUpdatesComponent import io.novafoundation.nova.feature_account_impl.presentation.account.list.selectAddress.di.SelectAddressComponent import io.novafoundation.nova.feature_account_impl.presentation.account.list.switching.di.SwitchWalletComponent import io.novafoundation.nova.feature_account_impl.presentation.account.management.di.WalletManagmentComponent @@ -77,6 +78,8 @@ interface AccountFeatureComponent : AccountFeatureApi { fun selectAddressComponentFactory(): SelectAddressComponent.Factory + fun delegatedAccountUpdatesFactory(): DelegatedAccountUpdatesComponent.Factory + fun accountDetailsComponentFactory(): AccountDetailsComponent.Factory fun connectionsComponentFactory(): NodesComponent.Factory diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt index a9a575ff6d..8848ee5d22 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt @@ -22,6 +22,7 @@ import io.novafoundation.nova.common.sequrity.SafeModeService import io.novafoundation.nova.common.sequrity.TwoFactorVerificationExecutor import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService import io.novafoundation.nova.common.utils.QrCodeGenerator +import io.novafoundation.nova.common.utils.coroutines.RootScope import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory import io.novafoundation.nova.common.utils.sequrity.BackgroundAccessObserver import io.novafoundation.nova.common.utils.systemCall.SystemCallExecutor @@ -141,4 +142,6 @@ interface AccountFeatureDependencies { val extrinsicSplitter: ExtrinsicSplitter val gasPriceProviderFactory: GasPriceProviderFactory + + val rootScope: RootScope } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt index ea3eff115f..1741d43d12 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt @@ -13,20 +13,27 @@ import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ClipboardManager +import io.novafoundation.nova.common.resources.ContextManager import io.novafoundation.nova.common.resources.LanguagesHolder import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory import io.novafoundation.nova.common.utils.DEFAULT_DERIVATION_PATH +import io.novafoundation.nova.common.utils.coroutines.RootScope import io.novafoundation.nova.common.utils.systemCall.SystemCallExecutor import io.novafoundation.nova.core.model.CryptoType import io.novafoundation.nova.core_db.dao.AccountDao import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NodeDao -import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository +import io.novafoundation.nova.feature_account_api.data.repository.ProxyRepository import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider +import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults +import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider +import io.novafoundation.nova.feature_account_api.domain.account.identity.OnChainIdentity import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor @@ -44,14 +51,21 @@ import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserProvider import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletCommunicator import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletMixin +import io.novafoundation.nova.feature_account_impl.BuildConfig import io.novafoundation.nova.feature_account_impl.RealBiometricServiceFactory import io.novafoundation.nova.feature_account_impl.data.ethereum.transaction.RealEvmTransactionService import io.novafoundation.nova.feature_account_impl.data.extrinsic.RealExtrinsicService import io.novafoundation.nova.feature_account_impl.data.network.blockchain.AccountSubstrateSource import io.novafoundation.nova.feature_account_impl.data.network.blockchain.AccountSubstrateSourceImpl +import io.novafoundation.nova.feature_account_impl.data.proxy.RealMetaAccountsUpdatesRegistry +import io.novafoundation.nova.feature_account_impl.data.proxy.RealProxySyncService import io.novafoundation.nova.feature_account_impl.data.repository.AccountRepositoryImpl -import io.novafoundation.nova.feature_account_impl.data.repository.AddAccountRepository import io.novafoundation.nova.feature_account_impl.data.repository.RealOnChainIdentityRepository +import io.novafoundation.nova.feature_account_impl.data.repository.RealProxyRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.proxied.ProxiedAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.JsonAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.MnemonicAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.SeedAddAccountRepository import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSourceImpl import io.novafoundation.nova.feature_account_impl.data.repository.datasource.migration.AccountDataMigration @@ -59,27 +73,32 @@ import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFa import io.novafoundation.nova.feature_account_impl.di.modules.AdvancedEncryptionStoreModule import io.novafoundation.nova.feature_account_impl.di.modules.IdentityProviderModule import io.novafoundation.nova.feature_account_impl.di.modules.ParitySignerModule -import io.novafoundation.nova.feature_account_impl.di.modules.SignersModule +import io.novafoundation.nova.feature_account_impl.di.modules.ProxySigningModule import io.novafoundation.nova.feature_account_impl.di.modules.WatchOnlyModule +import io.novafoundation.nova.feature_account_impl.di.modules.signers.SignersModule import io.novafoundation.nova.feature_account_impl.domain.AccountInteractorImpl import io.novafoundation.nova.feature_account_impl.domain.MetaAccountGroupingInteractorImpl import io.novafoundation.nova.feature_account_impl.domain.NodeHostValidator import io.novafoundation.nova.feature_account_impl.domain.account.add.AddAccountInteractor import io.novafoundation.nova.feature_account_impl.domain.account.advancedEncryption.AdvancedEncryptionInteractor -import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults -import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountDetailsInteractor +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.DelegatedMetaAccountUpdatesListingMixinFactory import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountTypePresentationMapper import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountWithBalanceListingMixinFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.ProxyFormatter import io.novafoundation.nova.feature_account_impl.presentation.account.wallet.WalletUiUseCaseImpl import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.addAccountChooser.AddAccountLauncherMixin import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.addAccountChooser.AddAccountLauncherProvider +import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.RealSigningNotSupportedPresentable +import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable import io.novafoundation.nova.feature_account_impl.presentation.language.RealLanguageUseCase import io.novafoundation.nova.feature_account_impl.presentation.mixin.identity.RealIdentityMixinFactory import io.novafoundation.nova.feature_account_impl.presentation.mixin.selectWallet.RealRealSelectWalletMixinFactory import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.config.RealPolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -87,17 +106,61 @@ import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory import io.novafoundation.nova.runtime.network.rpc.RpcCalls import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor +import jp.co.soramitsu.fearless_utils.encrypt.MultiChainEncryption import jp.co.soramitsu.fearless_utils.encrypt.json.JsonSeedDecoder import jp.co.soramitsu.fearless_utils.encrypt.json.JsonSeedEncoder -import javax.inject.Named -import jp.co.soramitsu.fearless_utils.encrypt.MultiChainEncryption import jp.co.soramitsu.fearless_utils.encrypt.junction.BIP32JunctionDecoder +import javax.inject.Named @Module( - includes = [SignersModule::class, WatchOnlyModule::class, ParitySignerModule::class, IdentityProviderModule::class, AdvancedEncryptionStoreModule::class] + includes = [ + SignersModule::class, + WatchOnlyModule::class, + ProxySigningModule::class, + ParitySignerModule::class, + IdentityProviderModule::class, + AdvancedEncryptionStoreModule::class, + AddAccountsModule::class + ] ) class AccountFeatureModule { + @Provides + @FeatureScope + fun provideMetaAccountsUpdatesRegistry( + preferences: Preferences + ): MetaAccountsUpdatesRegistry = RealMetaAccountsUpdatesRegistry(preferences) + + @Provides + @FeatureScope + fun provideProxyRepository( + @Named(REMOTE_STORAGE_SOURCE) storageDataSource: StorageDataSource, + chainRegistry: ChainRegistry + ): ProxyRepository = RealProxyRepository(storageDataSource, chainRegistry) + + @Provides + @FeatureScope + fun provideProxySyncService( + chainRegistry: ChainRegistry, + proxyRepository: ProxyRepository, + accounRepository: AccountRepository, + metaAccountDao: MetaAccountDao, + @OnChainIdentity identityProvider: IdentityProvider, + metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry, + proxiedAddAccountRepository: ProxiedAddAccountRepository, + rootScope: RootScope + ): ProxySyncService = RealProxySyncService( + chainRegistry = chainRegistry, + proxyRepository = proxyRepository, + accountRepository = accounRepository, + accountDao = metaAccountDao, + identityProvider = identityProvider, + metaAccountsUpdatesRegistry = metaAccountsUpdatesRegistry, + proxiedAddAccountRepository = proxiedAddAccountRepository, + rootScope = rootScope, + shouldSyncWatchOnlyProxies = BuildConfig.DEBUG + ) + @Provides @FeatureScope fun provideEncryptionDefaults(): EncryptionDefaults = EncryptionDefaults( @@ -239,12 +302,14 @@ class AccountFeatureModule { accountRepository: AccountRepository, addressIconGenerator: AddressIconGenerator, walletUiUseCase: WalletUiUseCase, - polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider + polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry ) = SelectedAccountUseCase( accountRepository = accountRepository, walletUiUseCase = walletUiUseCase, addressIconGenerator = addressIconGenerator, - polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider + polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider, + metaAccountsUpdatesRegistry = metaAccountsUpdatesRegistry ) @Provides @@ -260,7 +325,7 @@ class AccountFeatureModule { accountRepository: AccountRepository, secretStoreV2: SecretStoreV2, chainRegistry: ChainRegistry, - ) = AccountDetailsInteractor( + ) = WalletDetailsInteractor( accountRepository, secretStoreV2, chainRegistry @@ -272,26 +337,19 @@ class AccountFeatureModule { jsonSeedDecoder: JsonSeedDecoder ) = AccountSecretsFactory(jsonSeedDecoder) - @Provides - @FeatureScope - fun provideAddAccountRepository( - accountDataSource: AccountDataSource, - accountSecretsFactory: AccountSecretsFactory, - jsonSeedDecoder: JsonSeedDecoder, - chainRegistry: ChainRegistry, - ) = AddAccountRepository( - accountDataSource, - accountSecretsFactory, - jsonSeedDecoder, - chainRegistry - ) - @Provides @FeatureScope fun provideAddAccountInteractor( - addAccountRepository: AddAccountRepository, + mnemonicAddAccountRepository: MnemonicAddAccountRepository, + jsonAddAccountRepository: JsonAddAccountRepository, + seedAddAccountRepository: SeedAddAccountRepository, accountRepository: AccountRepository, - ) = AddAccountInteractor(addAccountRepository, accountRepository) + ) = AddAccountInteractor( + mnemonicAddAccountRepository, + jsonAddAccountRepository, + seedAddAccountRepository, + accountRepository + ) @Provides @FeatureScope @@ -351,17 +409,36 @@ class AccountFeatureModule { chainRegistry: ChainRegistry, accountRepository: AccountRepository, currencyRepository: CurrencyRepository, + metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry ): MetaAccountGroupingInteractor { - return MetaAccountGroupingInteractorImpl(chainRegistry, accountRepository, currencyRepository) + return MetaAccountGroupingInteractorImpl(chainRegistry, accountRepository, currencyRepository, metaAccountsUpdatesRegistry) } + @Provides + @FeatureScope + fun provideProxyFormatter( + walletUseCase: WalletUiUseCase, + resourceManager: ResourceManager + ) = ProxyFormatter(walletUseCase, resourceManager) + + @Provides + @FeatureScope + fun provideDelegatedMetaAccountUpdatesListingMixinFactory( + walletUseCase: WalletUiUseCase, + metaAccountGroupingInteractor: MetaAccountGroupingInteractor, + proxyFormatter: ProxyFormatter, + resourceManager: ResourceManager + ) = DelegatedMetaAccountUpdatesListingMixinFactory(walletUseCase, metaAccountGroupingInteractor, proxyFormatter, resourceManager) + @Provides @FeatureScope fun provideAccountListingMixinFactory( walletUseCase: WalletUiUseCase, metaAccountGroupingInteractor: MetaAccountGroupingInteractor, + proxyFormatter: ProxyFormatter, accountTypePresentationMapper: MetaAccountTypePresentationMapper, - ) = MetaAccountWithBalanceListingMixinFactory(walletUseCase, metaAccountGroupingInteractor, accountTypePresentationMapper) + resourceManager: ResourceManager + ) = MetaAccountWithBalanceListingMixinFactory(walletUseCase, metaAccountGroupingInteractor, accountTypePresentationMapper, proxyFormatter, resourceManager) @Provides @FeatureScope @@ -425,4 +502,10 @@ class AccountFeatureModule { fun provideBiometricServiceFactory(accountRepository: AccountRepository): BiometricServiceFactory { return RealBiometricServiceFactory(accountRepository) } + + @Provides + @FeatureScope + fun provideSigningNotSupportedPresentable( + contextManager: ContextManager + ): SigningNotSupportedPresentable = RealSigningNotSupportedPresentable(contextManager) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AddAccountsModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AddAccountsModule.kt new file mode 100644 index 0000000000..ac8cc8c5f8 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AddAccountsModule.kt @@ -0,0 +1,114 @@ +package io.novafoundation.nova.feature_account_impl.di + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.datasource.AccountDataSource +import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.ledger.RealLedgerAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.paritySigner.ParitySignerAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.proxied.ProxiedAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.JsonAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.MnemonicAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.SeedAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly.WatchOnlyAddAccountRepository +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import jp.co.soramitsu.fearless_utils.encrypt.json.JsonSeedDecoder + +@Module() +class AddAccountsModule { + + @Provides + @FeatureScope + fun provideMnemonicAddAccountRepository( + accountDataSource: AccountDataSource, + accountSecretsFactory: AccountSecretsFactory, + chainRegistry: ChainRegistry, + proxySyncService: ProxySyncService + ) = MnemonicAddAccountRepository( + accountDataSource, + accountSecretsFactory, + chainRegistry, + proxySyncService + ) + + @Provides + @FeatureScope + fun provideJsonAddAccountRepository( + accountDataSource: AccountDataSource, + accountSecretsFactory: AccountSecretsFactory, + jsonSeedDecoder: JsonSeedDecoder, + chainRegistry: ChainRegistry, + proxySyncService: ProxySyncService, + ) = JsonAddAccountRepository( + accountDataSource, + accountSecretsFactory, + jsonSeedDecoder, + chainRegistry, + proxySyncService + ) + + @Provides + @FeatureScope + fun provideSeedAddAccountRepository( + accountDataSource: AccountDataSource, + accountSecretsFactory: AccountSecretsFactory, + chainRegistry: ChainRegistry, + proxySyncService: ProxySyncService, + ) = SeedAddAccountRepository( + accountDataSource, + accountSecretsFactory, + chainRegistry, + proxySyncService + ) + + @Provides + @FeatureScope + fun provideWatchOnlyAddAccountRepository( + accountDao: MetaAccountDao, + proxySyncService: ProxySyncService, + ) = WatchOnlyAddAccountRepository( + accountDao, + proxySyncService + ) + + @Provides + @FeatureScope + fun provideParitySignerAddAccountRepository( + accountDao: MetaAccountDao, + chainRegistry: ChainRegistry, + proxySyncService: ProxySyncService + ) = ParitySignerAddAccountRepository( + accountDao, + chainRegistry, + proxySyncService + ) + + @Provides + @FeatureScope + fun provideProxiedAddAccountRepository( + accountDao: MetaAccountDao, + chainRegistry: ChainRegistry + ) = ProxiedAddAccountRepository( + accountDao, + chainRegistry + ) + + @Provides + @FeatureScope + fun provideLedgerAddAccountRepository( + accountDao: MetaAccountDao, + chainRegistry: ChainRegistry, + secretStoreV2: SecretStoreV2, + proxySyncService: ProxySyncService, + ): LedgerAddAccountRepository = RealLedgerAddAccountRepository( + accountDao, + chainRegistry, + secretStoreV2, + proxySyncService + ) +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ParitySignerModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ParitySignerModule.kt index 7ed4764507..6b65e50d16 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ParitySignerModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ParitySignerModule.kt @@ -4,19 +4,13 @@ import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin -import io.novafoundation.nova.common.resources.ContextManager import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.MutableSharedState import io.novafoundation.nova.common.utils.SharedState -import io.novafoundation.nova.core_db.dao.MetaAccountDao -import io.novafoundation.nova.feature_account_impl.data.repository.ParitySignerRepository -import io.novafoundation.nova.feature_account_impl.data.repository.RealParitySignerRepository +import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignCommunicator import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter -import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.RealSigningNotSupportedPresentable -import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.common.QrCodeExpiredPresentableFactory -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic @Module class ParitySignerModule { @@ -24,14 +18,8 @@ class ParitySignerModule { @Provides @FeatureScope fun provideReadOnlySharedState( - mutableSharedState: MutableSharedState - ): SharedState = mutableSharedState - - @Provides - @FeatureScope - fun provideRepository( - accountDao: MetaAccountDao - ): ParitySignerRepository = RealParitySignerRepository(accountDao) + mutableSharedState: SigningSharedState + ): SharedState = mutableSharedState @Provides @FeatureScope @@ -41,10 +29,4 @@ class ParitySignerModule { router: AccountRouter, communicator: PolkadotVaultVariantSignCommunicator ) = QrCodeExpiredPresentableFactory(resourceManager, actionAwaitableMixinFactory, router, communicator) - - @Provides - @FeatureScope - fun provideSigningNotSupportedPresentable( - contextManager: ContextManager - ): SigningNotSupportedPresentable = RealSigningNotSupportedPresentable(contextManager) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ProxySigningModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ProxySigningModule.kt new file mode 100644 index 0000000000..81805ed872 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/ProxySigningModule.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_account_impl.di.modules + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.data.storage.Preferences +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.resources.ContextManager +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_impl.presentation.proxy.sign.RealProxySigningPresenter +import io.novafoundation.nova.feature_account_api.presenatation.account.proxy.ProxySigningPresenter +import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable + +@Module +class ProxySigningModule { + + @Provides + @FeatureScope + fun provideProxySigningPresenter( + contextManager: ContextManager, + resourceManager: ResourceManager, + signingNotSupportedPresentable: SigningNotSupportedPresentable, + preferences: Preferences + ): ProxySigningPresenter = RealProxySigningPresenter(contextManager, resourceManager, signingNotSupportedPresentable, preferences) +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/ProxiedSignerModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/ProxiedSignerModule.kt new file mode 100644 index 0000000000..a2e0c136c4 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/ProxiedSignerModule.kt @@ -0,0 +1,45 @@ +package io.novafoundation.nova.feature_account_impl.di.modules.signers + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_account_api.data.repository.ProxyRepository +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.presenatation.account.proxy.ProxySigningPresenter +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxiedSignerFactory +import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxyCallFilterFactory +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.network.rpc.RpcCalls + +@Module +class ProxiedSignerModule { + + @Provides + @FeatureScope + fun provideProxyExtrinsicValidationRequestBus() = ProxyExtrinsicValidationRequestBus() + + @Provides + @FeatureScope + fun provideProxyCallFilterFactory() = ProxyCallFilterFactory() + + @Provides + @FeatureScope + fun provideProxiedSignerFactory( + chainRegistry: ChainRegistry, + accountRepository: AccountRepository, + proxySigningPresenter: ProxySigningPresenter, + proxyRepository: ProxyRepository, + rpcCalls: RpcCalls, + proxyExtrinsicValidationRequestBus: ProxyExtrinsicValidationRequestBus, + proxyCallFilterFactory: ProxyCallFilterFactory + ) = ProxiedSignerFactory( + chainRegistry, + accountRepository, + proxySigningPresenter, + proxyRepository, + rpcCalls, + proxyExtrinsicValidationRequestBus, + proxyCallFilterFactory + ) +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/SignersModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/SignersModule.kt similarity index 61% rename from feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/SignersModule.kt rename to feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/SignersModule.kt index a84de240a0..a08d1d33eb 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/SignersModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/signers/SignersModule.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_account_impl.di.modules +package io.novafoundation.nova.feature_account_impl.di.modules.signers import dagger.Module import dagger.Provides @@ -7,28 +7,29 @@ import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService import io.novafoundation.nova.common.utils.DefaultMutableSharedState -import io.novafoundation.nova.common.utils.MutableSharedState import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter import io.novafoundation.nova.feature_account_api.presenatation.sign.LedgerSignCommunicator import io.novafoundation.nova.feature_account_impl.data.signer.RealSignerProvider -import io.novafoundation.nova.feature_account_impl.data.signer.ledger.LedgerSigner -import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.ParitySignerSigner -import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultSigner +import io.novafoundation.nova.feature_account_impl.data.signer.ledger.LedgerSignerFactory import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignCommunicator +import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignerFactory +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxiedFeeSignerFactory +import io.novafoundation.nova.feature_account_impl.data.signer.proxy.ProxiedSignerFactory import io.novafoundation.nova.feature_account_impl.data.signer.secrets.SecretsSignerFactory -import io.novafoundation.nova.feature_account_impl.data.signer.watchOnly.WatchOnlySigner +import io.novafoundation.nova.feature_account_impl.data.signer.watchOnly.WatchOnlySignerFactory import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic -@Module +@Module(includes = [ProxiedSignerModule::class]) class SignersModule { @Provides @FeatureScope - fun provideSignSharedState(): MutableSharedState = DefaultMutableSharedState() + fun provideSignSharedState(): SigningSharedState = DefaultMutableSharedState() @Provides @FeatureScope @@ -40,35 +41,25 @@ class SignersModule { @Provides @FeatureScope - fun provideWatchOnlySigner( - watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter - ) = WatchOnlySigner(watchOnlySigningPresenter) + fun provideProxiedFeeSignerFactory( + accountRepository: AccountRepository + ) = ProxiedFeeSignerFactory(accountRepository) @Provides @FeatureScope - fun provideParitySignerSigner( - signingSharedState: MutableSharedState, - communicator: PolkadotVaultVariantSignCommunicator, - resourceManager: ResourceManager, - polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, - signingNotSupportedPresentable: SigningNotSupportedPresentable - ) = ParitySignerSigner( - signingSharedState = signingSharedState, - signFlowRequester = communicator, - resourceManager = resourceManager, - polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider, - messageSigningNotSupported = signingNotSupportedPresentable - ) + fun provideWatchOnlySignerFactory( + watchOnlySigningPresenter: WatchOnlyMissingKeysPresenter + ) = WatchOnlySignerFactory(watchOnlySigningPresenter) @Provides @FeatureScope - fun providePolkadotVaultSigner( - signingSharedState: MutableSharedState, + fun providePolkadotVaultVariantSignerFactory( + signingSharedState: SigningSharedState, communicator: PolkadotVaultVariantSignCommunicator, resourceManager: ResourceManager, polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, signingNotSupportedPresentable: SigningNotSupportedPresentable - ) = PolkadotVaultSigner( + ) = PolkadotVaultVariantSignerFactory( signingSharedState = signingSharedState, signFlowRequester = communicator, resourceManager = resourceManager, @@ -78,26 +69,28 @@ class SignersModule { @Provides @FeatureScope - fun provideLedgerSigner( - signingSharedState: MutableSharedState, + fun provideLedgerSignerFactory( + signingSharedState: SigningSharedState, communicator: LedgerSignCommunicator, resourceManager: ResourceManager, signingNotSupportedPresentable: SigningNotSupportedPresentable - ) = LedgerSigner(signingSharedState, communicator, resourceManager, signingNotSupportedPresentable) + ) = LedgerSignerFactory(signingSharedState, communicator, resourceManager, signingNotSupportedPresentable) @Provides @FeatureScope fun provideSignerProvider( secretsSignerFactory: SecretsSignerFactory, - watchOnlySigner: WatchOnlySigner, - paritySignerSigner: ParitySignerSigner, - polkadotVaultSigner: PolkadotVaultSigner, - ledgerSigner: LedgerSigner + proxiedSignerFactory: ProxiedSignerFactory, + watchOnlySignerFactory: WatchOnlySignerFactory, + polkadotVaultSignerFactory: PolkadotVaultVariantSignerFactory, + proxiedFeeSignerFactory: ProxiedFeeSignerFactory, + ledgerSignerFactory: LedgerSignerFactory ): SignerProvider = RealSignerProvider( secretsSignerFactory = secretsSignerFactory, - watchOnlySigner = watchOnlySigner, - paritySignerSigner = paritySignerSigner, - polkadotVaultSigner = polkadotVaultSigner, - ledgerSigner = ledgerSigner + watchOnlySigner = watchOnlySignerFactory, + polkadotVaultSignerFactory = polkadotVaultSignerFactory, + proxiedSignerFactory = proxiedSignerFactory, + proxiedFeeSignerFactory = proxiedFeeSignerFactory, + ledgerSignerFactory = ledgerSignerFactory ) } 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 b71f273fe5..b719e586a1 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 @@ -6,6 +6,7 @@ import io.novafoundation.nova.core.model.Node import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.Account +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountOrdering import io.novafoundation.nova.feature_account_api.domain.model.PreferredCryptoType @@ -24,8 +25,8 @@ class AccountInteractorImpl( private val accountRepository: AccountRepository, ) : AccountInteractor { - override suspend fun getMetaAccounts(): List { - return accountRepository.allMetaAccounts() + override suspend fun getActiveMetaAccounts(): List { + return accountRepository.getActiveMetaAccounts() } override suspend fun generateMnemonic(): Mnemonic { @@ -72,7 +73,7 @@ class AccountInteractorImpl( override suspend fun deleteAccount(metaId: Long) = withContext(Dispatchers.Default) { accountRepository.deleteAccount(metaId) if (!accountRepository.isAccountSelected()) { - val metaAccounts = getMetaAccounts() + val metaAccounts = getActiveMetaAccounts() if (metaAccounts.isNotEmpty()) { accountRepository.selectMetaAccount(metaAccounts.first().id) } @@ -179,4 +180,25 @@ class AccountInteractorImpl( val chain = chainRegistry.getChain(chainId) return metaAccount.addressIn(chain) } + + override suspend fun removeDeactivatedMetaAccounts() { + accountRepository.removeDeactivatedMetaAccounts() + + if (!accountRepository.isAccountSelected()) { + val metaAccounts = getActiveMetaAccounts() + if (metaAccounts.isNotEmpty()) { + accountRepository.selectMetaAccount(metaAccounts.first().id) + } + } + } + + override suspend fun switchToNotDeactivatedAccountIfNeeded() { + val metaAccount = accountRepository.getSelectedMetaAccount() + if (metaAccount.status != LightMetaAccount.Status.DEACTIVATED) return + + val metaAccounts = accountRepository.getActiveMetaAccounts() + if (metaAccounts.isNotEmpty()) { + accountRepository.selectMetaAccount(metaAccounts.first().id) + } + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/MetaAccountGroupingInteractorImpl.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/MetaAccountGroupingInteractorImpl.kt index 869365bfcd..e9ed2268c5 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/MetaAccountGroupingInteractorImpl.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/MetaAccountGroupingInteractorImpl.kt @@ -6,12 +6,14 @@ import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.removed import io.novafoundation.nova.common.utils.sumByBigDecimal +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountAssetBalance import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountWithTotalBalance +import io.novafoundation.nova.feature_account_api.domain.model.ProxiedAndProxyMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_account_api.domain.model.hasAccountIn import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository @@ -26,20 +28,23 @@ class MetaAccountGroupingInteractorImpl( private val chainRegistry: ChainRegistry, private val accountRepository: AccountRepository, private val currencyRepository: CurrencyRepository, + private val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry, ) : MetaAccountGroupingInteractor { override fun metaAccountsWithTotalBalanceFlow(): Flow> { return combine( currencyRepository.observeSelectCurrency(), - accountRepository.allMetaAccountsFlow(), - accountRepository.metaAccountBalancesFlow() - ) { selectedCurrency, accounts, allBalances -> + accountRepository.activeMetaAccountsFlow(), + accountRepository.metaAccountBalancesFlow(), + metaAccountsUpdatesRegistry.observeUpdates(), + chainRegistry.chainsById + ) { selectedCurrency, accounts, allBalances, updatedMetaAccounts, chains -> val groupedBalances = allBalances.groupBy(MetaAccountAssetBalance::metaId) accounts.map { metaAccount -> val accountBalances = groupedBalances[metaAccount.id] ?: emptyList() - - metaAccountWithTotalBalance(accountBalances, metaAccount, selectedCurrency) + val hasUpdates = updatedMetaAccounts.contains(metaAccount.id) + metaAccountWithTotalBalance(accountBalances, metaAccount, accounts, selectedCurrency, chains, hasUpdates) } .groupBy { it.metaAccount.type } .toSortedMap(metaAccountTypeComparator()) @@ -49,10 +54,12 @@ class MetaAccountGroupingInteractorImpl( override fun metaAccountWithTotalBalanceFlow(metaId: Long): Flow { return combine( currencyRepository.observeSelectCurrency(), + accountRepository.activeMetaAccountsFlow(), accountRepository.metaAccountFlow(metaId), - accountRepository.metaAccountBalancesFlow(metaId) - ) { selectedCurrency, metaAccount, metaAccountBalances -> - metaAccountWithTotalBalance(metaAccountBalances, metaAccount, selectedCurrency) + accountRepository.metaAccountBalancesFlow(metaId), + chainRegistry.chainsById + ) { selectedCurrency, allMetaAccounts, metaAccount, metaAccountBalances, chains -> + metaAccountWithTotalBalance(metaAccountBalances, metaAccount, allMetaAccounts, selectedCurrency, chains, false) } } @@ -64,6 +71,28 @@ class MetaAccountGroupingInteractorImpl( .toSortedMap(metaAccountTypeComparator()) } + override fun updatedProxieds(): Flow> { + return combine( + metaAccountsUpdatesRegistry.observeUpdates(), + accountRepository.allMetaAccountsFlow(), + chainRegistry.chainsById + ) { updatedMetaIds, metaAccounts, chainsById -> + val metaById = metaAccounts.associateBy(MetaAccount::id) + + metaAccounts + .filter { it.type == LightMetaAccount.Type.PROXIED && updatedMetaIds.contains(it.id) } + .mapNotNull { + ProxiedAndProxyMetaAccount( + it, + metaById[it.proxy?.metaId] ?: return@mapNotNull null, + chainsById[it.proxy?.chainId] ?: return@mapNotNull null + ) + } + .groupBy { it.proxied.status } + .toSortedMap(metaAccountStateComparator()) + } + } + override suspend fun hasAvailableMetaAccountsForDestination(fromId: ChainId, destinationId: ChainId): Boolean { val fromChain = chainRegistry.getChain(fromId) val destinationChain = chainRegistry.getChain(destinationId) @@ -71,10 +100,13 @@ class MetaAccountGroupingInteractorImpl( .any { it.hasAccountIn(destinationChain) } } - private fun metaAccountWithTotalBalance( + private suspend fun metaAccountWithTotalBalance( metaAccountBalances: List, metaAccount: MetaAccount, - selectedCurrency: Currency + allMetaAccounts: List, + selectedCurrency: Currency, + chains: Map, + hasUpdates: Boolean ): MetaAccountWithTotalBalance { val totalBalance = metaAccountBalances.sumByBigDecimal { val totalInPlanks = it.freeInPlanks + it.reservedInPlanks + it.offChainBalance.orZero() @@ -82,19 +114,34 @@ class MetaAccountGroupingInteractorImpl( totalInPlanks.amountFromPlanks(it.precision) * it.rate.orZero() } + val proxyMetaAccount = metaAccount.proxy?.let { proxy -> allMetaAccounts.firstOrNull { it.id == proxy.metaId } } + return MetaAccountWithTotalBalance( metaAccount = metaAccount, + proxyMetaAccount = proxyMetaAccount, + proxyChain = metaAccount.proxy?.chainId?.let(chains::getValue), totalBalance = totalBalance, - currency = selectedCurrency + currency = selectedCurrency, + hasUpdates = hasUpdates ) } private suspend fun getValidMetaAccountsForTransaction(from: Chain, destination: Chain): List { val selectedMetaAccount = accountRepository.getSelectedMetaAccount() val fromChainAddress = selectedMetaAccount.addressIn(from) - return accountRepository.allMetaAccounts() + return accountRepository.getActiveMetaAccounts() .removed { fromChainAddress == it.addressIn(destination) } - .filter { it.type != LightMetaAccount.Type.WATCH_ONLY } + .filter { + when (it.type) { + LightMetaAccount.Type.SECRETS, + LightMetaAccount.Type.POLKADOT_VAULT, + LightMetaAccount.Type.PARITY_SIGNER, + LightMetaAccount.Type.PROXIED, + LightMetaAccount.Type.LEDGER -> true + + LightMetaAccount.Type.WATCH_ONLY -> false + } + } } private fun metaAccountTypeComparator() = compareBy { @@ -103,7 +150,15 @@ class MetaAccountGroupingInteractorImpl( LightMetaAccount.Type.POLKADOT_VAULT -> 1 LightMetaAccount.Type.PARITY_SIGNER -> 2 LightMetaAccount.Type.LEDGER -> 3 - LightMetaAccount.Type.WATCH_ONLY -> 4 + LightMetaAccount.Type.PROXIED -> 4 + LightMetaAccount.Type.WATCH_ONLY -> 5 + } + } + + private fun metaAccountStateComparator() = compareBy { + when (it) { + LightMetaAccount.Status.ACTIVE -> 0 + LightMetaAccount.Status.DEACTIVATED -> 1 } } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/add/AddAccountInteractor.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/add/AddAccountInteractor.kt index 71a6a59dcd..5d6739c844 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/add/AddAccountInteractor.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/add/AddAccountInteractor.kt @@ -3,11 +3,15 @@ package io.novafoundation.nova.feature_account_impl.domain.account.add import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.AddAccountType import io.novafoundation.nova.feature_account_api.domain.model.ImportJsonMetaData -import io.novafoundation.nova.feature_account_impl.data.repository.AddAccountRepository import io.novafoundation.nova.feature_account_api.domain.account.advancedEncryption.AdvancedEncryption +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.JsonAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.MnemonicAddAccountRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.secrets.SeedAddAccountRepository class AddAccountInteractor( - private val addAccountRepository: AddAccountRepository, + private val mnemonicAddAccountRepository: MnemonicAddAccountRepository, + private val jsonAddAccountRepository: JsonAddAccountRepository, + private val seedAddAccountRepository: SeedAddAccountRepository, private val accountRepository: AccountRepository, ) { @@ -17,10 +21,12 @@ class AddAccountInteractor( addAccountType: AddAccountType ): Result { return addAccount(addAccountType) { - addAccountRepository.addFromMnemonic( - mnemonic, - advancedEncryption, - addAccountType + mnemonicAddAccountRepository.addAccount( + MnemonicAddAccountRepository.Payload( + mnemonic, + advancedEncryption, + addAccountType + ) ) } } @@ -31,10 +37,12 @@ class AddAccountInteractor( addAccountType: AddAccountType ): Result { return addAccount(addAccountType) { - addAccountRepository.addFromMnemonic( - mnemonic, - advancedEncryption, - addAccountType + mnemonicAddAccountRepository.addAccount( + MnemonicAddAccountRepository.Payload( + mnemonic, + advancedEncryption, + addAccountType + ) ) } } @@ -45,10 +53,12 @@ class AddAccountInteractor( addAccountType: AddAccountType ): Result { return addAccount(addAccountType) { - addAccountRepository.addFromSeed( - seed, - advancedEncryption, - addAccountType + seedAddAccountRepository.addAccount( + SeedAddAccountRepository.Payload( + seed, + advancedEncryption, + addAccountType + ) ) } } @@ -59,10 +69,12 @@ class AddAccountInteractor( addAccountType: AddAccountType ): Result { return addAccount(addAccountType) { - addAccountRepository.addFromJson( - json = json, - password = password, - addAccountType = addAccountType + jsonAddAccountRepository.addAccount( + JsonAddAccountRepository.Payload( + json = json, + password = password, + addAccountType = addAccountType + ) ) } } @@ -77,7 +89,7 @@ class AddAccountInteractor( suspend fun extractJsonMetadata(json: String): Result { return runCatching { - addAccountRepository.extractJsonMetadata(json) + jsonAddAccountRepository.extractJsonMetadata(json) } } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/AccountDetailsInteractor.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/WalletDetailsInteractor.kt similarity index 51% rename from feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/AccountDetailsInteractor.kt rename to feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/WalletDetailsInteractor.kt index 58d58d9809..192a63958a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/AccountDetailsInteractor.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/details/WalletDetailsInteractor.kt @@ -5,24 +5,20 @@ import io.novafoundation.nova.common.data.secrets.v2.entropy import io.novafoundation.nova.common.data.secrets.v2.getAccountSecrets import io.novafoundation.nova.common.data.secrets.v2.seed import io.novafoundation.nova.common.list.GroupedList -import io.novafoundation.nova.common.utils.mapToSet import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_account_api.domain.model.hasChainAccountIn import io.novafoundation.nova.feature_account_api.presenatation.account.add.SecretType import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain.From -import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateApplicationConfig -import io.novafoundation.nova.runtime.ext.defaultComparatorFrom import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -class AccountDetailsInteractor( +class WalletDetailsInteractor( private val accountRepository: AccountRepository, private val secretStoreV2: SecretStoreV2, private val chainRegistry: ChainRegistry, @@ -36,28 +32,32 @@ class AccountDetailsInteractor( accountRepository.updateMetaAccountName(metaId, newName) } - suspend fun getChainProjections(metaAccount: MetaAccount): GroupedList = withContext(Dispatchers.Default) { - val chains = shownChainsFor(metaAccount.type) - - chains.map { chain -> - val address = metaAccount.addressIn(chain) - val accountId = metaAccount.accountIdIn(chain) - - val projection = if (address != null && accountId != null) { - AccountInChain.Projection(address, accountId) - } else { - null + suspend fun getChainProjections( + metaAccount: MetaAccount, + chains: List, + sorting: Comparator + ): GroupedList { + return withContext(Dispatchers.Default) { + chains.map { chain -> + val address = metaAccount.addressIn(chain) + val accountId = metaAccount.accountIdIn(chain) + + val projection = if (address != null && accountId != null) { + AccountInChain.Projection(address, accountId) + } else { + null + } + + AccountInChain( + chain = chain, + projection = projection, + from = if (metaAccount.hasChainAccountIn(chain.id)) From.CHAIN_ACCOUNT else From.META_ACCOUNT + ) } - - AccountInChain( - chain = chain, - projection = projection, - from = if (metaAccount.hasChainAccountIn(chain.id)) From.CHAIN_ACCOUNT else From.META_ACCOUNT - ) + .sortedWith(sorting) + .groupBy(AccountInChain::from) + .toSortedMap(compareBy(From::ordering)) } - .sortedWith(accountInChainComparator(metaAccount.type)) - .groupBy(AccountInChain::from) - .toSortedMap(compareBy(From::ordering)) } suspend fun availableExportTypes( @@ -75,29 +75,8 @@ class AccountDetailsInteractor( ) } - private val AccountInChain.hasChainAccount - get() = projection != null - - private fun accountInChainComparator(metaAccountType: LightMetaAccount.Type): Comparator { - val hasAccountOrdering: Comparator = when (metaAccountType) { - LightMetaAccount.Type.LEDGER -> compareBy { !it.hasChainAccount } - else -> compareBy { it.hasChainAccount } - } - - return hasAccountOrdering.then(Chain.defaultComparatorFrom(AccountInChain::chain)) - } - - private suspend fun shownChainsFor(metaAccountType: LightMetaAccount.Type): List { - val allChains = chainRegistry.currentChains.first() - - return when (metaAccountType) { - LightMetaAccount.Type.LEDGER -> { - val ledgerSupportedChainIds = SubstrateApplicationConfig.all().mapToSet { it.chainId } - - allChains.filter { it.id in ledgerSupportedChainIds } - } - else -> allChains - } + suspend fun getAllChains(): List { + return chainRegistry.currentChains.first() } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/identity/LocalIdentityProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/identity/LocalIdentityProvider.kt index 5f7f06b72e..df37cdfd74 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/identity/LocalIdentityProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/account/identity/LocalIdentityProvider.kt @@ -13,7 +13,7 @@ class LocalIdentityProvider( ) : IdentityProvider { override suspend fun identityFor(accountId: AccountId, chainId: ChainId): Identity? = withContext(Dispatchers.IO) { - val name = accountRepository.accountNameFor(accountId) + val name = accountRepository.accountNameFor(accountId, chainId) name?.let(::Identity) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/paritySigner/connect/finish/FinishImportParitySignerInteractor.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/paritySigner/connect/finish/FinishImportParitySignerInteractor.kt index 27c8d7c55a..d2e1171c6d 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/paritySigner/connect/finish/FinishImportParitySignerInteractor.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/paritySigner/connect/finish/FinishImportParitySignerInteractor.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect. import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant -import io.novafoundation.nova.feature_account_impl.data.repository.ParitySignerRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.paritySigner.ParitySignerAddAccountRepository import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -17,7 +17,7 @@ interface FinishImportParitySignerInteractor { } class RealFinishImportParitySignerInteractor( - private val repository: ParitySignerRepository, + private val paritySignerAddAccountRepository: ParitySignerAddAccountRepository, private val accountRepository: AccountRepository, ) : FinishImportParitySignerInteractor { @@ -27,7 +27,13 @@ class RealFinishImportParitySignerInteractor( variant: PolkadotVaultVariant ): Result = withContext(Dispatchers.Default) { runCatching { - val metaId = repository.addParitySignerWallet(name, substrateAccountId, variant) + val metaId = paritySignerAddAccountRepository.addAccount( + ParitySignerAddAccountRepository.Payload( + name, + substrateAccountId, + variant + ) + ) accountRepository.selectMetaAccount(metaId) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/change/ChangeWatchAccountInteractor.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/change/ChangeWatchAccountInteractor.kt index 3979ae03a8..007f750f52 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/change/ChangeWatchAccountInteractor.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/change/ChangeWatchAccountInteractor.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_account_impl.domain.watchOnly.change -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_impl.data.repository.WatchOnlyRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly.WatchOnlyAddAccountRepository import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -15,8 +14,7 @@ interface ChangeWatchAccountInteractor { } class RealChangeWatchAccountInteractor( - private val accountRepository: AccountRepository, - private val watchOnlyRepository: WatchOnlyRepository, + private val watchOnlyAddAccountRepository: WatchOnlyAddAccountRepository ) : ChangeWatchAccountInteractor { override suspend fun changeChainAccount( @@ -26,10 +24,12 @@ class RealChangeWatchAccountInteractor( ): Result<*> = runCatching { val accountId = chain.accountIdOf(address) - watchOnlyRepository.changeWatchChainAccount( - metaId = metaId, - chainId = chain.id, - accountId = accountId + watchOnlyAddAccountRepository.addAccount( + WatchOnlyAddAccountRepository.Payload.ChainAccount( + metaId = metaId, + chainId = chain.id, + accountId = accountId + ) ) } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/create/CreateWatchWalletInteractor.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/create/CreateWatchWalletInteractor.kt index 0d6257dfba..22d4b6a9c9 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/create/CreateWatchWalletInteractor.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/domain/watchOnly/create/CreateWatchWalletInteractor.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.utils.ethereumAddressToAccountId import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_impl.data.repository.WatchOnlyRepository import io.novafoundation.nova.feature_account_impl.data.repository.WatchWalletSuggestion +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly.WatchOnlyAddAccountRepository import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId interface CreateWatchWalletInteractor { @@ -19,6 +20,7 @@ interface CreateWatchWalletInteractor { class RealCreateWatchWalletInteractor( private val repository: WatchOnlyRepository, + private val watchOnlyAddAccountRepository: WatchOnlyAddAccountRepository, private val accountRepository: AccountRepository, ) : CreateWatchWalletInteractor { @@ -26,7 +28,13 @@ class RealCreateWatchWalletInteractor( val substrateAccountId = substrateAddress.toAccountId() val evmAccountId = evmAddress.takeIf { it.isNotEmpty() }?.ethereumAddressToAccountId() - val metaId = repository.addWatchWallet(name, substrateAccountId, evmAccountId) + val metaId = watchOnlyAddAccountRepository.addAccount( + WatchOnlyAddAccountRepository.Payload.MetaAccount( + name, + substrateAccountId, + evmAccountId + ) + ) accountRepository.selectMetaAccount(metaId) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/AccountRouter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/AccountRouter.kt index 0fec5e83c0..dd8bddf19d 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/AccountRouter.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/AccountRouter.kt @@ -32,11 +32,13 @@ interface AccountRouter : SecureRouter, ReturnableRouter { fun openSwitchWallet() + fun openDelegatedAccountsUpdates() + fun openNodes() fun openAddAccount(payload: AddAccountPayload) - fun openAccountDetails(metaId: Long) + fun openWalletDetails(metaId: Long) fun openNodeDetails(nodeId: Int) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/DelegatedMetaAccountUpdatesListingMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/DelegatedMetaAccountUpdatesListingMixin.kt new file mode 100644 index 0000000000..0fa5cfd3c1 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/DelegatedMetaAccountUpdatesListingMixin.kt @@ -0,0 +1,104 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.common.listing + +import io.novafoundation.nova.common.list.toListWithHeaders +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions +import io.novafoundation.nova.common.utils.colorSpan +import io.novafoundation.nova.common.utils.toSpannable +import io.novafoundation.nova.common.utils.withAlphaDrawable +import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.ProxiedAndProxyMetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountTitleGroupRvItem +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase +import io.novafoundation.nova.feature_account_impl.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.map + +class DelegatedMetaAccountUpdatesListingMixinFactory( + private val walletUiUseCase: WalletUiUseCase, + private val metaAccountGroupingInteractor: MetaAccountGroupingInteractor, + private val proxyFormatter: ProxyFormatter, + private val resourceManager: ResourceManager +) { + + fun create(coroutineScope: CoroutineScope): MetaAccountListingMixin { + return DelegatedMetaAccountUpdatesListingMixin( + walletUiUseCase = walletUiUseCase, + metaAccountGroupingInteractor = metaAccountGroupingInteractor, + proxyFormatter = proxyFormatter, + resourceManager = resourceManager, + coroutineScope = coroutineScope + ) + } +} + +private const val DISABLED_ICON_ALPHA = 0.56f +private const val ENABLED_ICON_ALPHA = 1.0f + +private class DelegatedMetaAccountUpdatesListingMixin( + private val metaAccountGroupingInteractor: MetaAccountGroupingInteractor, + private val walletUiUseCase: WalletUiUseCase, + private val proxyFormatter: ProxyFormatter, + private val resourceManager: ResourceManager, + coroutineScope: CoroutineScope, +) : MetaAccountListingMixin, WithCoroutineScopeExtensions by WithCoroutineScopeExtensions(coroutineScope) { + + override val metaAccountsFlow = metaAccountGroupingInteractor.updatedProxieds() + .map { list -> + list.toListWithHeaders( + keyMapper = { type, _ -> mapHeader(type) }, + valueMapper = { mapProxiedToUi(it) } + ) + } + .shareInBackground() + + private fun mapHeader(status: LightMetaAccount.Status): AccountTitleGroupRvItem { + val text = when (status) { + LightMetaAccount.Status.ACTIVE -> resourceManager.getString(R.string.account_proxieds) + LightMetaAccount.Status.DEACTIVATED -> resourceManager.getString(R.string.proxieds_updates_deactivated_title) + } + + return AccountTitleGroupRvItem(text) + } + + private suspend fun mapProxiedToUi(proxiedWithProxy: ProxiedAndProxyMetaAccount) = with(proxiedWithProxy) { + val isEnabled = proxiedWithProxy.proxied.status == LightMetaAccount.Status.ACTIVE + val secondaryColor = resourceManager.getColor(R.color.text_secondary) + val title = proxied.name + val subtitle = mapSubtitle(this, isEnabled) + val walletIcon = walletUiUseCase.walletIcon(proxied) + AccountUi( + id = proxied.id, + title = if (isEnabled) title else title.toSpannable(colorSpan(secondaryColor)), + subtitle = if (isEnabled) subtitle else subtitle.toSpannable(colorSpan(secondaryColor)), + isSelected = false, + isClickable = true, + picture = if (isEnabled) walletIcon else walletIcon.withAlphaDrawable(DISABLED_ICON_ALPHA), + chainIconUrl = proxiedWithProxy.chain.icon, + updateIndicator = false, + subtitleIconRes = null, + chainIconOpacity = if (isEnabled) ENABLED_ICON_ALPHA else DISABLED_ICON_ALPHA, + isEditable = false + ) + } + + private suspend fun mapSubtitle( + proxiedWithProxy: ProxiedAndProxyMetaAccount, + isEnabled: Boolean + ): CharSequence { + val proxy = proxiedWithProxy.proxied.proxy ?: return proxiedWithProxy.proxiedAddress() // fallback + val proxyIcon = proxyFormatter.makeAccountDrawable(proxiedWithProxy.proxy) + return proxyFormatter.mapProxyMetaAccountSubtitle( + proxiedWithProxy.proxy.name, + if (isEnabled) proxyIcon else proxyIcon.withAlphaDrawable(DISABLED_ICON_ALPHA), + proxy + ) + } + + private fun ProxiedAndProxyMetaAccount.proxiedAddress(): String { + return proxied.requireAddressIn(chain) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountTypePresentationMapper.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountTypePresentationMapper.kt index 4e1b4436f5..6427f9b56b 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountTypePresentationMapper.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountTypePresentationMapper.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.view.ChipLabelModel import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.asPolkadotVaultVariantOrThrow +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountChipGroupRvItem import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_account_impl.R @@ -12,13 +13,14 @@ class MetaAccountTypePresentationMapper( private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, ) { - fun mapMetaAccountTypeToUi(type: LightMetaAccount.Type): ChipLabelModel? { - return when (type) { + fun mapMetaAccountTypeToUi(type: LightMetaAccount.Type): AccountChipGroupRvItem? { + val chipLabelModel = when (type) { LightMetaAccount.Type.SECRETS -> null LightMetaAccount.Type.WATCH_ONLY -> ChipLabelModel( iconRes = R.drawable.ic_watch_only_filled, title = resourceManager.getString(R.string.account_watch_only) ) + LightMetaAccount.Type.PARITY_SIGNER, LightMetaAccount.Type.POLKADOT_VAULT -> { val config = polkadotVaultVariantConfigProvider.variantConfigFor(type.asPolkadotVaultVariantOrThrow()) @@ -27,10 +29,18 @@ class MetaAccountTypePresentationMapper( title = resourceManager.getString(config.common.nameRes) ) } + LightMetaAccount.Type.LEDGER -> ChipLabelModel( iconRes = R.drawable.ic_ledger, title = resourceManager.getString(R.string.common_ledger) ) + + LightMetaAccount.Type.PROXIED -> ChipLabelModel( + iconRes = R.drawable.ic_proxy, + title = resourceManager.getString(R.string.account_proxieds) + ) } + + return chipLabelModel?.let { AccountChipGroupRvItem(it) } } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountValidForTransactionListingMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountValidForTransactionListingMixin.kt index cacafe9e1c..9c6645ba91 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountValidForTransactionListingMixin.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountValidForTransactionListingMixin.kt @@ -7,7 +7,7 @@ import io.novafoundation.nova.common.utils.lazyAsync import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.addressIn -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -79,7 +79,10 @@ private class MetaAccountValidForTransactionListingMixin( isSelected = isSelected, isClickable = chainAddress != null, picture = icon, - subtitleIconRes = if (chainAddress == null) R.drawable.ic_warning_filled else null + chainIconUrl = null, + updateIndicator = false, + subtitleIconRes = if (chainAddress == null) R.drawable.ic_warning_filled else null, + isEditable = false ) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountWithBalanceListingMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountWithBalanceListingMixin.kt index ec3d2776b6..f0da9558ac 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountWithBalanceListingMixin.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/MetaAccountWithBalanceListingMixin.kt @@ -1,11 +1,13 @@ package io.novafoundation.nova.feature_account_impl.presentation.account.common.listing import io.novafoundation.nova.common.list.toListWithHeaders +import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccountWithTotalBalance -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency import kotlinx.coroutines.CoroutineScope @@ -15,6 +17,8 @@ class MetaAccountWithBalanceListingMixinFactory( private val walletUiUseCase: WalletUiUseCase, private val metaAccountGroupingInteractor: MetaAccountGroupingInteractor, private val accountTypePresentationMapper: MetaAccountTypePresentationMapper, + private val proxyFormatter: ProxyFormatter, + private val resourceManager: ResourceManager ) { fun create( @@ -26,7 +30,9 @@ class MetaAccountWithBalanceListingMixinFactory( metaAccountGroupingInteractor = metaAccountGroupingInteractor, coroutineScope = coroutineScope, isMetaAccountSelected = isMetaAccountSelected, - accountTypePresentationMapper = accountTypePresentationMapper + accountTypePresentationMapper = accountTypePresentationMapper, + proxyFormatter = proxyFormatter, + resourceManager = resourceManager ) } } @@ -36,6 +42,8 @@ private class MetaAccountWithBalanceListingMixin( private val walletUiUseCase: WalletUiUseCase, private val isMetaAccountSelected: suspend (MetaAccount) -> Boolean, private val accountTypePresentationMapper: MetaAccountTypePresentationMapper, + private val proxyFormatter: ProxyFormatter, + private val resourceManager: ResourceManager, coroutineScope: CoroutineScope, ) : MetaAccountListingMixin, WithCoroutineScopeExtensions by WithCoroutineScopeExtensions(coroutineScope) { @@ -49,13 +57,59 @@ private class MetaAccountWithBalanceListingMixin( private suspend fun mapMetaAccountToUi(metaAccountWithBalance: MetaAccountWithTotalBalance) = with(metaAccountWithBalance) { AccountUi( - id = metaAccountWithBalance.metaAccount.id, - title = metaAccountWithBalance.metaAccount.name, - subtitle = totalBalance.formatAsCurrency(metaAccountWithBalance.currency), - isSelected = isMetaAccountSelected(metaAccountWithBalance.metaAccount), + id = metaAccount.id, + title = metaAccount.name, + subtitle = mapSubtitle(this), + isSelected = isMetaAccountSelected(metaAccount), + isEditable = metaAccount.isEditable(), isClickable = true, - picture = walletUiUseCase.walletIcon(metaAccountWithBalance.metaAccount), - subtitleIconRes = null, + picture = walletUiUseCase.walletIcon(metaAccount), + chainIconUrl = proxyChain?.icon, + updateIndicator = hasUpdates, + subtitleIconRes = null ) } + + private suspend fun mapSubtitle( + metaAccountWithBalance: MetaAccountWithTotalBalance + ): CharSequence = with(metaAccountWithBalance) { + when (metaAccount.type) { + LightMetaAccount.Type.SECRETS, + LightMetaAccount.Type.WATCH_ONLY, + LightMetaAccount.Type.PARITY_SIGNER, + LightMetaAccount.Type.LEDGER, + LightMetaAccount.Type.POLKADOT_VAULT -> formattedTotalBalance() + + LightMetaAccount.Type.PROXIED -> mapProxyTypeToSubtitle(metaAccountWithBalance) + } + } + + private suspend fun mapProxyTypeToSubtitle( + metaAccountWithBalance: MetaAccountWithTotalBalance + ): CharSequence = with(metaAccountWithBalance) { + val proxy = metaAccount.proxy ?: return formattedTotalBalance() + val proxyMetaAccount = proxyMetaAccount ?: return formattedTotalBalance() + + return proxyFormatter.mapProxyMetaAccountSubtitle( + proxyMetaAccount.name, + proxyFormatter.makeAccountDrawable(proxyMetaAccount), + proxy + ) + } + + private fun MetaAccount.isEditable(): Boolean { + return when (type) { + LightMetaAccount.Type.SECRETS, + LightMetaAccount.Type.WATCH_ONLY, + LightMetaAccount.Type.PARITY_SIGNER, + LightMetaAccount.Type.LEDGER, + LightMetaAccount.Type.POLKADOT_VAULT -> true + + LightMetaAccount.Type.PROXIED -> false + } + } + + private fun MetaAccountWithTotalBalance.formattedTotalBalance(): String { + return totalBalance.formatAsCurrency(currency) + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/ProxyFormatter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/ProxyFormatter.kt new file mode 100644 index 0000000000..d4cc391f81 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/common/listing/ProxyFormatter.kt @@ -0,0 +1,63 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.common.listing + +import android.graphics.drawable.Drawable +import android.text.SpannableStringBuilder +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.append +import io.novafoundation.nova.common.utils.appendEnd +import io.novafoundation.nova.common.utils.appendSpace +import io.novafoundation.nova.common.utils.capitalize +import io.novafoundation.nova.common.utils.colorSpan +import io.novafoundation.nova.common.utils.drawableSpan +import io.novafoundation.nova.common.utils.splitCamelCase +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase +import io.novafoundation.nova.feature_account_impl.R + +class ProxyFormatter( + private val walletUiUseCase: WalletUiUseCase, + private val resourceManager: ResourceManager, +) { + + suspend fun mapProxyMetaAccountSubtitle( + proxyAccountName: String, + proxyAccountIcon: Drawable, + proxyAccount: ProxyAccount + ): CharSequence { + val proxyType = mapProxyTypeToString(proxyAccount.proxyType) + val formattedProxyMetaAccount = mapProxyMetaAccount(proxyAccountName, proxyAccountIcon) + + return SpannableStringBuilder(proxyType) + .append(":") + .appendSpace() + .append(formattedProxyMetaAccount) + } + + suspend fun mapProxyMetaAccount(proxyAccountName: String, proxyAccountIcon: Drawable): CharSequence { + return SpannableStringBuilder() + .appendEnd(drawableSpan(proxyAccountIcon)) + .appendSpace() + .append(proxyAccountName, colorSpan(resourceManager.getColor(R.color.text_primary))) + } + + fun mapProxyTypeToString(type: ProxyAccount.ProxyType): String { + val proxyType = when (type) { + ProxyAccount.ProxyType.Any -> resourceManager.getString(R.string.account_proxy_type_any) + ProxyAccount.ProxyType.NonTransfer -> resourceManager.getString(R.string.account_proxy_type_non_transfer) + ProxyAccount.ProxyType.Governance -> resourceManager.getString(R.string.account_proxy_type_governance) + ProxyAccount.ProxyType.Staking -> resourceManager.getString(R.string.account_proxy_type_staking) + ProxyAccount.ProxyType.IdentityJudgement -> resourceManager.getString(R.string.account_proxy_type_identity_judgement) + ProxyAccount.ProxyType.CancelProxy -> resourceManager.getString(R.string.account_proxy_type_cancel_proxy) + ProxyAccount.ProxyType.Auction -> resourceManager.getString(R.string.account_proxy_type_auction) + ProxyAccount.ProxyType.NominationPools -> resourceManager.getString(R.string.account_proxy_type_nomination_pools) + is ProxyAccount.ProxyType.Other -> type.name.splitCamelCase().joinToString(separator = " ") { it.capitalize() } + } + + return resourceManager.getString(R.string.proxy_wallet_type, proxyType) + } + + suspend fun makeAccountDrawable(metaAccount: MetaAccount): Drawable { + return walletUiUseCase.walletIcon(metaAccount, 16) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsViewModel.kt deleted file mode 100644 index 51a098557c..0000000000 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsViewModel.kt +++ /dev/null @@ -1,237 +0,0 @@ -package io.novafoundation.nova.feature_account_impl.presentation.account.details - -import androidx.lifecycle.viewModelScope -import io.novafoundation.nova.common.address.AddressIconGenerator -import io.novafoundation.nova.common.base.BaseViewModel -import io.novafoundation.nova.common.list.headers.TextHeader -import io.novafoundation.nova.common.list.toListWithHeaders -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.filterToSet -import io.novafoundation.nova.common.utils.flowOf -import io.novafoundation.nova.common.utils.inBackground -import io.novafoundation.nova.common.utils.invoke -import io.novafoundation.nova.common.view.AlertView -import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi -import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount -import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount.Type -import io.novafoundation.nova.feature_account_api.domain.model.asPolkadotVaultVariantOrThrow -import io.novafoundation.nova.feature_account_api.domain.model.isPolkadotVaultLike -import io.novafoundation.nova.feature_account_api.presenatation.account.add.SecretType -import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction -import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider -import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.formatWithPolkadotVaultLabel -import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions -import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserMixin -import io.novafoundation.nova.feature_account_impl.R -import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountDetailsInteractor -import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain -import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter -import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert -import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi -import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.addAccountChooser.AddAccountLauncherMixin -import io.novafoundation.nova.feature_account_impl.presentation.exporting.ExportPayload -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.seconds - -private const val UPDATE_NAME_INTERVAL_SECONDS = 1L - -class AccountDetailsViewModel( - private val interactor: AccountDetailsInteractor, - private val accountRouter: AccountRouter, - private val iconGenerator: AddressIconGenerator, - private val resourceManager: ResourceManager, - private val metaId: Long, - private val externalActions: ExternalActions.Presentation, - private val chainRegistry: ChainRegistry, - private val importTypeChooserMixin: ImportTypeChooserMixin.Presentation, - private val addAccountLauncherMixin: AddAccountLauncherMixin.Presentation, - private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, -) : BaseViewModel(), - ExternalActions by externalActions, - ImportTypeChooserMixin by importTypeChooserMixin, - AddAccountLauncherMixin by addAccountLauncherMixin { - - val accountNameFlow: MutableStateFlow = MutableStateFlow("") - - private val metaAccount = async(Dispatchers.Default) { interactor.getMetaAccount(metaId) } - - val availableAccountActions = flowOf { - availableAccountActions(metaAccount().type) - }.shareInBackground() - - val typeAlert = flowOf { - accountTypeAlertFor(metaAccount().type) - }.shareInBackground() - - val chainAccountProjections = flowOf { interactor.getChainProjections(metaAccount()) } - .map { groupedList -> - groupedList.toListWithHeaders( - keyMapper = { type, _ -> mapFromToTextHeader(type) }, - valueMapper = { mapChainAccountProjectionToUi(metaAccount(), it) } - ) - } - .inBackground() - .share() - - init { - launch { - accountNameFlow.emit(metaAccount().name) - } - - syncNameChangesWithDb() - } - - fun backClicked() { - accountRouter.back() - } - - private fun syncNameChangesWithDb() { - accountNameFlow - .filter { it.isNotEmpty() } - .debounce(UPDATE_NAME_INTERVAL_SECONDS.seconds) - .onEach { interactor.updateName(metaId, it) } - .launchIn(viewModelScope) - } - - private suspend fun mapFromToTextHeader(from: AccountInChain.From): TextHeader? { - return when (metaAccount().type) { - Type.LEDGER, Type.PARITY_SIGNER, Type.POLKADOT_VAULT -> null - Type.SECRETS, Type.WATCH_ONLY -> { - val resId = when (from) { - AccountInChain.From.META_ACCOUNT -> R.string.account_shared_secret - AccountInChain.From.CHAIN_ACCOUNT -> R.string.account_custom_secret - } - - return TextHeader(resourceManager.getString(resId)) - } - } - } - - private suspend fun mapChainAccountProjectionToUi(metaAccount: LightMetaAccount, accountInChain: AccountInChain) = with(accountInChain) { - val addressOrHint = when { - projection != null -> projection.address - metaAccount.type.isPolkadotVaultLike() -> { - val polkadotVaultVariant = metaAccount.type.asPolkadotVaultVariantOrThrow() - resourceManager.formatWithPolkadotVaultLabel(R.string.account_details_parity_signer_not_supported, polkadotVaultVariant) - } - else -> resourceManager.getString(R.string.account_no_chain_projection) - } - - val accountIcon = projection?.let { - iconGenerator.createAddressIcon(it.accountId, AddressIconGenerator.SIZE_SMALL, backgroundColorRes = AddressIconGenerator.BACKGROUND_TRANSPARENT) - } ?: resourceManager.getDrawable(R.drawable.ic_warning_filled) - - val availableActionsForChain = availableActionsFor(accountInChain) - val canViewAddresses = accountInChain.projection != null - val canDoAnyActions = availableActionsForChain.isNotEmpty() || canViewAddresses - - AccountInChainUi( - chainUi = mapChainToUi(chain), - addressOrHint = addressOrHint, - address = projection?.address, - accountIcon = accountIcon, - actionsAvailable = canDoAnyActions - ) - } - - fun chainAccountClicked(item: AccountInChainUi) = launch { - if (!item.actionsAvailable) return@launch - - val chain = chainRegistry.getChain(item.chainUi.id) - - val type = ExternalActions.Type.Address(item.address) - - externalActions.showExternalActions(type, chain) - } - - fun exportClicked(inChain: Chain) = launch { - viewModelScope.launch { - val sources = interactor.availableExportTypes(metaAccount(), inChain) - - val payload = ImportTypeChooserMixin.Payload( - allowedTypes = sources, - onChosen = { exportTypeChosen(it, inChain) } - ) - importTypeChooserMixin.showChooser(payload) - } - } - - fun changeChainAccountClicked(inChain: Chain) { - launch { - addAccountLauncherMixin.initiateLaunch(inChain, metaAccount()) - } - } - - private fun availableAccountActions(accountType: Type): Set { - return when (accountType) { - Type.SECRETS -> setOf(AccountAction.EXPORT, AccountAction.CHANGE) - Type.WATCH_ONLY -> setOf(AccountAction.CHANGE) - Type.PARITY_SIGNER, Type.POLKADOT_VAULT -> emptySet() - Type.LEDGER -> setOf(AccountAction.CHANGE) - } - } - - private fun accountTypeAlertFor(accountType: Type): AccountTypeAlert? { - return when (accountType) { - Type.WATCH_ONLY -> AccountTypeAlert( - style = AlertView.Style( - backgroundColorRes = R.color.block_background, - iconRes = R.drawable.ic_watch_only_filled - ), - text = resourceManager.getString(R.string.account_details_watch_only_alert) - ) - Type.PARITY_SIGNER, Type.POLKADOT_VAULT -> { - val polkadotVaultVariant = accountType.asPolkadotVaultVariantOrThrow() - val variantConfig = polkadotVaultVariantConfigProvider.variantConfigFor(polkadotVaultVariant) - - AccountTypeAlert( - style = AlertView.Style( - backgroundColorRes = R.color.block_background, - iconRes = variantConfig.common.iconRes - ), - text = resourceManager.formatWithPolkadotVaultLabel(R.string.account_details_parity_signer_alert, polkadotVaultVariant) - ) - } - Type.SECRETS -> null - Type.LEDGER -> AccountTypeAlert( - style = AlertView.Style( - backgroundColorRes = R.color.block_background, - iconRes = R.drawable.ic_ledger - ), - text = resourceManager.getString(R.string.ledger_wallet_details_description) - ) - } - } - - private fun exportTypeChosen(type: SecretType, chain: Chain) { - val exportPayload = ExportPayload(metaId, chain.id) - - val navigationAction = when (type) { - SecretType.MNEMONIC -> accountRouter.exportMnemonicAction(exportPayload) - SecretType.SEED -> accountRouter.exportSeedAction(exportPayload) - SecretType.JSON -> accountRouter.exportJsonPasswordAction(exportPayload) - } - - accountRouter.withPinCodeCheckRequired(navigationAction) - } - - private suspend fun availableActionsFor(accountInChain: AccountInChain): Set { - return availableAccountActions.first().filterToSet { action -> - when (action) { - AccountAction.CHANGE -> true - AccountAction.EXPORT -> accountInChain.projection != null - } - } - } -} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsFragment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsFragment.kt similarity index 84% rename from feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsFragment.kt rename to feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsFragment.kt index faebd16fff..b29dd29b5e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/AccountDetailsFragment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsFragment.kt @@ -21,16 +21,16 @@ import io.novafoundation.nova.feature_account_impl.di.AccountFeatureComponent import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.ChainAccountsAdapter import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.addAccountChooser.ui.setupAddAccountLauncher -import kotlinx.android.synthetic.main.fragment_account_details.accountDetailsChainAccounts -import kotlinx.android.synthetic.main.fragment_account_details.accountDetailsNameField -import kotlinx.android.synthetic.main.fragment_account_details.accountDetailsToolbar -import kotlinx.android.synthetic.main.fragment_account_details.accountDetailsTypeAlert +import kotlinx.android.synthetic.main.fragment_wallet_details.accountDetailsChainAccounts +import kotlinx.android.synthetic.main.fragment_wallet_details.accountDetailsNameField +import kotlinx.android.synthetic.main.fragment_wallet_details.accountDetailsToolbar +import kotlinx.android.synthetic.main.fragment_wallet_details.accountDetailsTypeAlert import kotlinx.coroutines.flow.first import javax.inject.Inject private const val ACCOUNT_ID_KEY = "ACCOUNT_ADDRESS_KEY" -class AccountDetailsFragment : BaseFragment(), ChainAccountsAdapter.Handler { +class WalletDetailsFragment : BaseFragment(), ChainAccountsAdapter.Handler { @Inject lateinit var imageLoader: ImageLoader @@ -52,7 +52,7 @@ class AccountDetailsFragment : BaseFragment(), ChainAcc inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ) = layoutInflater.inflate(R.layout.fragment_account_details, container, false) + ) = layoutInflater.inflate(R.layout.fragment_wallet_details, container, false) override fun initViews() { accountDetailsToolbar.setHomeButtonListener { @@ -81,7 +81,7 @@ class AccountDetailsFragment : BaseFragment(), ChainAcc .inject(this) } - override fun subscribe(viewModel: AccountDetailsViewModel) { + override fun subscribe(viewModel: WalletDetailsViewModel) { setupExternalActions(viewModel) { context, payload -> ChainAccountActionsSheet( context, @@ -103,7 +103,8 @@ class AccountDetailsFragment : BaseFragment(), ChainAcc viewModel.typeAlert.observe { if (it != null) { accountDetailsTypeAlert.makeVisible() - accountDetailsTypeAlert.setText(it.text) + accountDetailsTypeAlert.setMessage(it.message) + accountDetailsTypeAlert.setSubMessage(it.subMessage) accountDetailsTypeAlert.setStyle(it.style) } else { accountDetailsTypeAlert.makeGone() diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsViewModel.kt new file mode 100644 index 0000000000..46ee90ba75 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/WalletDetailsViewModel.kt @@ -0,0 +1,116 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details + +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.common.utils.invoke +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount.Type +import io.novafoundation.nova.feature_account_api.presenatation.account.add.SecretType +import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions +import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserMixin +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.WalletDetailsMixinFactory +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.addAccountChooser.AddAccountLauncherMixin +import io.novafoundation.nova.feature_account_impl.presentation.exporting.ExportPayload +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +private const val UPDATE_NAME_INTERVAL_SECONDS = 1L + +class WalletDetailsViewModel( + private val interactor: WalletDetailsInteractor, + private val accountRouter: AccountRouter, + private val metaId: Long, + private val externalActions: ExternalActions.Presentation, + private val chainRegistry: ChainRegistry, + private val importTypeChooserMixin: ImportTypeChooserMixin.Presentation, + private val addAccountLauncherMixin: AddAccountLauncherMixin.Presentation, + private val walletDetailsMixinFactory: WalletDetailsMixinFactory +) : BaseViewModel(), + ExternalActions by externalActions, + ImportTypeChooserMixin by importTypeChooserMixin, + AddAccountLauncherMixin by addAccountLauncherMixin { + + val walletDetailsMixin = async { walletDetailsMixinFactory.create(metaId) } + + val accountNameFlow: MutableStateFlow = MutableStateFlow("") + + val availableAccountActions = flowOfAll { walletDetailsMixin().availableAccountActions } + .shareInBackground() + + val typeAlert = flowOfAll { walletDetailsMixin().typeAlert } + .shareInBackground() + + val chainAccountProjections = flowOfAll { walletDetailsMixin().chainAccountProjections } + .shareInBackground() + + init { + launch { + accountNameFlow.emit(walletDetailsMixin().metaAccount.name) + } + + syncNameChangesWithDb() + } + + fun backClicked() { + accountRouter.back() + } + + private fun syncNameChangesWithDb() { + accountNameFlow + .filter { it.isNotEmpty() } + .debounce(UPDATE_NAME_INTERVAL_SECONDS.seconds) + .onEach { interactor.updateName(metaId, it) } + .launchIn(viewModelScope) + } + + fun chainAccountClicked(item: AccountInChainUi) = launch { + if (!item.actionsAvailable) return@launch + + val chain = chainRegistry.getChain(item.chainUi.id) + + val type = ExternalActions.Type.Address(item.address) + + externalActions.showExternalActions(type, chain) + } + + fun exportClicked(inChain: Chain) = launch { + viewModelScope.launch { + val sources = interactor.availableExportTypes(walletDetailsMixin().metaAccount, inChain) + + val payload = ImportTypeChooserMixin.Payload( + allowedTypes = sources, + onChosen = { exportTypeChosen(it, inChain) } + ) + importTypeChooserMixin.showChooser(payload) + } + } + + fun changeChainAccountClicked(inChain: Chain) { + launch { + addAccountLauncherMixin.initiateLaunch(inChain, walletDetailsMixin().metaAccount) + } + } + + private fun exportTypeChosen(type: SecretType, chain: Chain) { + val exportPayload = ExportPayload(metaId, chain.id) + + val navigationAction = when (type) { + SecretType.MNEMONIC -> accountRouter.exportMnemonicAction(exportPayload) + SecretType.SEED -> accountRouter.exportSeedAction(exportPayload) + SecretType.JSON -> accountRouter.exportJsonPasswordAction(exportPayload) + } + + accountRouter.withPinCodeCheckRequired(navigationAction) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsComponent.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsComponent.kt index 9d3b7b144d..00c1287c0b 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsComponent.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsComponent.kt @@ -4,7 +4,7 @@ import androidx.fragment.app.Fragment import dagger.BindsInstance import dagger.Subcomponent import io.novafoundation.nova.common.di.scope.ScreenScope -import io.novafoundation.nova.feature_account_impl.presentation.account.details.AccountDetailsFragment +import io.novafoundation.nova.feature_account_impl.presentation.account.details.WalletDetailsFragment @Subcomponent( modules = [ @@ -23,5 +23,5 @@ interface AccountDetailsComponent { ): AccountDetailsComponent } - fun inject(fragment: AccountDetailsFragment) + fun inject(fragment: WalletDetailsFragment) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsModule.kt index 453dea5a96..2d9585c5e7 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/di/AccountDetailsModule.kt @@ -14,41 +14,65 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserMixin -import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountDetailsInteractor +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter -import io.novafoundation.nova.feature_account_impl.presentation.account.details.AccountDetailsViewModel +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.ProxyFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.WalletDetailsViewModel +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.WalletDetailsMixinFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory import io.novafoundation.nova.feature_account_impl.presentation.common.mixin.addAccountChooser.AddAccountLauncherMixin import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [ViewModelModule::class]) class AccountDetailsModule { + @Provides + fun provideAccountFormatterFactory( + resourceManager: ResourceManager, + @Caching iconGenerator: AddressIconGenerator, + ): AccountFormatterFactory { + return AccountFormatterFactory(iconGenerator, resourceManager) + } + + @Provides + fun provideWalletDetailsMixinFactory( + polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + resourceManager: ResourceManager, + accountFormatterFactory: AccountFormatterFactory, + proxyFormatter: ProxyFormatter, + interactor: WalletDetailsInteractor + ): WalletDetailsMixinFactory { + return WalletDetailsMixinFactory( + polkadotVaultVariantConfigProvider, + resourceManager, + accountFormatterFactory, + proxyFormatter, + interactor + ) + } + @Provides @IntoMap - @ViewModelKey(AccountDetailsViewModel::class) + @ViewModelKey(WalletDetailsViewModel::class) fun provideViewModel( - interactor: AccountDetailsInteractor, + interactor: WalletDetailsInteractor, router: AccountRouter, - resourceManager: ResourceManager, - @Caching iconGenerator: AddressIconGenerator, metaId: Long, externalActions: ExternalActions.Presentation, chainRegistry: ChainRegistry, importTypeChooserMixin: ImportTypeChooserMixin.Presentation, addAccountLauncherMixin: AddAccountLauncherMixin.Presentation, - polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + walletDetailsMixinFactory: WalletDetailsMixinFactory ): ViewModel { - return AccountDetailsViewModel( + return WalletDetailsViewModel( interactor = interactor, accountRouter = router, - iconGenerator = iconGenerator, - resourceManager = resourceManager, metaId = metaId, externalActions = externalActions, chainRegistry = chainRegistry, importTypeChooserMixin = importTypeChooserMixin, addAccountLauncherMixin = addAccountLauncherMixin, - polkadotVaultVariantConfigProvider = polkadotVaultVariantConfigProvider + walletDetailsMixinFactory = walletDetailsMixinFactory ) } @@ -56,7 +80,7 @@ class AccountDetailsModule { fun provideViewModelCreator( fragment: Fragment, viewModelFactory: ViewModelProvider.Factory - ): AccountDetailsViewModel { - return ViewModelProvider(fragment, viewModelFactory).get(AccountDetailsViewModel::class.java) + ): WalletDetailsViewModel { + return ViewModelProvider(fragment, viewModelFactory).get(WalletDetailsViewModel::class.java) } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/LedgerWalletDetailsMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/LedgerWalletDetailsMixin.kt new file mode 100644 index 0000000000..fee1580e81 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/LedgerWalletDetailsMixin.kt @@ -0,0 +1,69 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.utils.mapToSet +import io.novafoundation.nova.common.view.AlertView +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.baseAccountTitleFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.notHasAccountComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.withChainComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateApplicationConfig +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +class LedgerWalletDetailsMixin( + private val resourceManager: ResourceManager, + private val accountFormatterFactory: AccountFormatterFactory, + private val interactor: WalletDetailsInteractor, + metaAccount: MetaAccount +) : WalletDetailsMixin( + interactor, + metaAccount +) { + + private val accountFormatter = accountFormatterFactory.create(baseAccountTitleFormatter(resourceManager)) + + override val availableAccountActions: Flow> = flowOf { setOf(AccountAction.CHANGE) } + + override val typeAlert: Flow = flowOf { + AccountTypeAlert( + style = AlertView.Style( + backgroundColorRes = R.color.block_background, + iconRes = R.drawable.ic_ledger + ), + message = resourceManager.getString(R.string.ledger_wallet_details_description) + ) + } + + override suspend fun getChainProjections(): GroupedList { + val ledgerSupportedChainIds = SubstrateApplicationConfig.all().mapToSet { it.chainId } + val chains = interactor.getAllChains() + .filter { it.id in ledgerSupportedChainIds } + return interactor.getChainProjections( + metaAccount, + chains, + notHasAccountComparator().withChainComparator() + ) + } + + override suspend fun mapAccountHeader(from: AccountInChain.From): TextHeader? { + return null + } + + override suspend fun mapAccount(accountInChain: AccountInChain): AccountInChainUi { + return accountFormatter.formatChainAccountProjection( + accountInChain, + availableAccountActions.first() + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/PolkadotVaultWalletDetailsMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/PolkadotVaultWalletDetailsMixin.kt new file mode 100644 index 0000000000..35aa965051 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/PolkadotVaultWalletDetailsMixin.kt @@ -0,0 +1,59 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.asPolkadotVaultVariantOrThrow +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.hasAccountComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.polkadotVaultAccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.polkadotVaultTitle +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.withChainComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +class PolkadotVaultWalletDetailsMixin( + private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + private val resourceManager: ResourceManager, + private val accountFormatterFactory: AccountFormatterFactory, + private val interactor: WalletDetailsInteractor, + metaAccount: MetaAccount +) : WalletDetailsMixin( + interactor, + metaAccount +) { + private val accountFormatter = accountFormatterFactory.create( + accountTitleFormatter = { it.polkadotVaultTitle(resourceManager, metaAccount) } + ) + + override val availableAccountActions: Flow> = flowOf { emptySet() } + + override val typeAlert: Flow = flowOf { + val vaultVariant = metaAccount.type.asPolkadotVaultVariantOrThrow() + val variantConfig = polkadotVaultVariantConfigProvider.variantConfigFor(vaultVariant) + polkadotVaultAccountTypeAlert(vaultVariant, variantConfig, resourceManager) + } + + override suspend fun getChainProjections(): GroupedList { + return interactor.getChainProjections(metaAccount, interactor.getAllChains(), hasAccountComparator().withChainComparator()) + } + + override suspend fun mapAccountHeader(from: AccountInChain.From): TextHeader? { + return null + } + + override suspend fun mapAccount(accountInChain: AccountInChain): AccountInChainUi { + return accountFormatter.formatChainAccountProjection( + accountInChain, + availableAccountActions.first() + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/ProxiedWalletDetailsMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/ProxiedWalletDetailsMixin.kt new file mode 100644 index 0000000000..35f1ac1bfd --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/ProxiedWalletDetailsMixin.kt @@ -0,0 +1,73 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import android.text.SpannableStringBuilder +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.appendSpace +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.view.AlertView +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.ProxyFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.baseAccountTitleFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.hasAccountComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.withChainComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +class ProxiedWalletDetailsMixin( + private val resourceManager: ResourceManager, + private val accountFormatterFactory: AccountFormatterFactory, + private val interactor: WalletDetailsInteractor, + private val proxyFormatter: ProxyFormatter, + metaAccount: MetaAccount +) : WalletDetailsMixin( + interactor, + metaAccount +) { + private val accountFormatter = accountFormatterFactory.create(baseAccountTitleFormatter(resourceManager)) + + override val availableAccountActions: Flow> = flowOf { emptySet() } + + override val typeAlert: Flow = flowOf { + val proxyAccount = metaAccount.proxy ?: return@flowOf null + val proxyMetaAccount = interactor.getMetaAccount(proxyAccount.metaId) + + val proxyAccountWithIcon = proxyFormatter.mapProxyMetaAccount(proxyMetaAccount.name, proxyFormatter.makeAccountDrawable(proxyMetaAccount)) + AccountTypeAlert( + style = AlertView.Style( + backgroundColorRes = R.color.block_background, + iconRes = R.drawable.ic_proxy + ), + message = resourceManager.getString(R.string.proxied_wallet_details_info_warning), + subMessage = SpannableStringBuilder(proxyAccountWithIcon) + .appendSpace() + .append(proxyFormatter.mapProxyTypeToString(proxyAccount.proxyType)) + ) + } + + override suspend fun getChainProjections(): GroupedList { + val proxiedChainIds = metaAccount.chainAccounts.keys + val chains = interactor.getAllChains() + .filter { it.id in proxiedChainIds } + return interactor.getChainProjections(metaAccount, chains, hasAccountComparator().withChainComparator()) + } + + override suspend fun mapAccountHeader(from: AccountInChain.From): TextHeader? { + return null + } + + override suspend fun mapAccount(accountInChain: AccountInChain): AccountInChainUi { + return accountFormatter.formatChainAccountProjection( + accountInChain, + availableAccountActions.first() + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/SecretsWalletDetailsMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/SecretsWalletDetailsMixin.kt new file mode 100644 index 0000000000..e57aa0582b --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/SecretsWalletDetailsMixin.kt @@ -0,0 +1,50 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.baseAccountTitleFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.hasAccountComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.mapToAccountHeader +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.withChainComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +class SecretsWalletDetailsMixin( + private val resourceManager: ResourceManager, + private val accountFormatterFactory: AccountFormatterFactory, + private val interactor: WalletDetailsInteractor, + metaAccount: MetaAccount +) : WalletDetailsMixin( + interactor, + metaAccount +) { + private val accountFormatter = accountFormatterFactory.create(baseAccountTitleFormatter(resourceManager)) + + override val availableAccountActions: Flow> = flowOf { setOf(AccountAction.EXPORT, AccountAction.CHANGE) } + + override val typeAlert: Flow = flowOf { null } + + override suspend fun getChainProjections(): GroupedList { + return interactor.getChainProjections(metaAccount, interactor.getAllChains(), hasAccountComparator().withChainComparator()) + } + + override suspend fun mapAccountHeader(from: AccountInChain.From): TextHeader? { + return from.mapToAccountHeader(resourceManager) + } + + override suspend fun mapAccount(accountInChain: AccountInChain): AccountInChainUi { + return accountFormatter.formatChainAccountProjection( + accountInChain, + availableAccountActions.first() + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixin.kt new file mode 100644 index 0000000000..c39bc2ccdb --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixin.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.list.toListWithHeaders +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +abstract class WalletDetailsMixin( + private val interactor: WalletDetailsInteractor, + val metaAccount: MetaAccount +) { + + abstract val availableAccountActions: Flow> + + abstract val typeAlert: Flow + + val chainAccountProjections: Flow> = flowOf { getChainProjections() } + .map { groupedList -> + groupedList.toListWithHeaders( + keyMapper = { type, _ -> mapAccountHeader(type) }, + valueMapper = { mapAccount(it) } + ) + } + + abstract suspend fun getChainProjections(): GroupedList + + abstract suspend fun mapAccountHeader(from: AccountInChain.From): TextHeader? + + abstract suspend fun mapAccount(accountInChain: AccountInChain): AccountInChainUi +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixinFactory.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixinFactory.kt new file mode 100644 index 0000000000..5bbaf95061 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WalletDetailsMixinFactory.kt @@ -0,0 +1,39 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount.Type +import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.ProxyFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory + +class WalletDetailsMixinFactory( + private val polkadotVaultVariantConfigProvider: PolkadotVaultVariantConfigProvider, + private val resourceManager: ResourceManager, + private val accountFormatterFactory: AccountFormatterFactory, + private val proxyFormatter: ProxyFormatter, + private val interactor: WalletDetailsInteractor +) { + + suspend fun create(metaId: Long): WalletDetailsMixin { + val metaAccount = interactor.getMetaAccount(metaId) + return when (metaAccount.type) { + Type.SECRETS -> SecretsWalletDetailsMixin(resourceManager, accountFormatterFactory, interactor, metaAccount) + + Type.WATCH_ONLY -> WatchOnlyWalletDetailsMixin(resourceManager, accountFormatterFactory, interactor, metaAccount) + + Type.LEDGER -> LedgerWalletDetailsMixin(resourceManager, accountFormatterFactory, interactor, metaAccount) + + Type.PARITY_SIGNER, + Type.POLKADOT_VAULT -> PolkadotVaultWalletDetailsMixin( + polkadotVaultVariantConfigProvider, + resourceManager, + accountFormatterFactory, + interactor, + metaAccount + ) + + Type.PROXIED -> ProxiedWalletDetailsMixin(resourceManager, accountFormatterFactory, interactor, proxyFormatter, metaAccount) + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WatchOnlyWalletDetailsMixin.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WatchOnlyWalletDetailsMixin.kt new file mode 100644 index 0000000000..bf585c93a1 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/WatchOnlyWalletDetailsMixin.kt @@ -0,0 +1,60 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin + +import io.novafoundation.nova.common.list.GroupedList +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.view.AlertView +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.AccountFormatterFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.baseAccountTitleFormatter +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.hasAccountComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.mapToAccountHeader +import io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common.withChainComparator +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +class WatchOnlyWalletDetailsMixin( + private val resourceManager: ResourceManager, + private val accountFormatterFactory: AccountFormatterFactory, + private val interactor: WalletDetailsInteractor, + metaAccount: MetaAccount +) : WalletDetailsMixin( + interactor, + metaAccount +) { + private val accountFormatter = accountFormatterFactory.create(baseAccountTitleFormatter(resourceManager)) + + override val availableAccountActions: Flow> = flowOf { setOf(AccountAction.CHANGE) } + + override val typeAlert: Flow = flowOf { + AccountTypeAlert( + style = AlertView.Style( + backgroundColorRes = R.color.block_background, + iconRes = R.drawable.ic_watch_only_filled + ), + message = resourceManager.getString(R.string.account_details_watch_only_alert) + ) + } + + override suspend fun getChainProjections(): GroupedList { + return interactor.getChainProjections(metaAccount, interactor.getAllChains(), hasAccountComparator().withChainComparator()) + } + + override suspend fun mapAccountHeader(from: AccountInChain.From): TextHeader? { + return from.mapToAccountHeader(resourceManager) + } + + override suspend fun mapAccount(accountInChain: AccountInChain): AccountInChainUi { + return accountFormatter.formatChainAccountProjection( + accountInChain, + availableAccountActions.first() + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/AccountFormatter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/AccountFormatter.kt new file mode 100644 index 0000000000..9e4b3bab78 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/AccountFormatter.kt @@ -0,0 +1,66 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common + +import io.novafoundation.nova.common.address.AddressIconGenerator +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.filterToSet +import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_account_api.presenatation.account.details.ChainAccountActionsSheet.AccountAction +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.presentation.common.chainAccounts.AccountInChainUi + +class AccountFormatterFactory( + private val iconGenerator: AddressIconGenerator, + private val resourceManager: ResourceManager +) { + + fun create(accountTitleFormatter: suspend (AccountInChain) -> String): AccountFormatter { + return AccountFormatter( + iconGenerator, + resourceManager, + accountTitleFormatter + ) + } +} + +class AccountFormatter( + private val iconGenerator: AddressIconGenerator, + private val resourceManager: ResourceManager, + private val accountTitleFormatter: suspend (AccountInChain) -> String +) { + + suspend fun formatChainAccountProjection(accountInChain: AccountInChain, availableActions: Set): AccountInChainUi { + return with(accountInChain) { + val accountIcon = projection?.let { + iconGenerator.createAddressIcon(it.accountId, AddressIconGenerator.SIZE_SMALL, backgroundColorRes = AddressIconGenerator.BACKGROUND_TRANSPARENT) + } ?: resourceManager.getDrawable(R.drawable.ic_warning_filled) + + val availableActionsForChain = availableActionsFor(accountInChain, availableActions) + val canViewAddresses = accountInChain.projection != null + val canDoAnyActions = availableActionsForChain.isNotEmpty() || canViewAddresses + + AccountInChainUi( + chainUi = mapChainToUi(chain), + addressOrHint = accountTitleFormatter(accountInChain), + address = projection?.address, + accountIcon = accountIcon, + actionsAvailable = canDoAnyActions + ) + } + } + + private suspend fun availableActionsFor(accountInChain: AccountInChain, availableActions: Set): Set { + return availableActions.filterToSet { action -> + when (action) { + AccountAction.CHANGE -> true + AccountAction.EXPORT -> accountInChain.projection != null + } + } + } +} + +fun baseAccountTitleFormatter(resourceManager: ResourceManager): (AccountInChain) -> String { + return { accountInChain -> + accountInChain.projection?.address ?: resourceManager.getString(R.string.account_no_chain_projection) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/WalletMixinCommon.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/WalletMixinCommon.kt new file mode 100644 index 0000000000..e72f332355 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/mixin/common/WalletMixinCommon.kt @@ -0,0 +1,58 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.details.mixin.common + +import io.novafoundation.nova.common.list.headers.TextHeader +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.view.AlertView +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.PolkadotVaultVariant +import io.novafoundation.nova.feature_account_api.domain.model.asPolkadotVaultVariantOrThrow +import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfig +import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.formatWithPolkadotVaultLabel +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_impl.domain.account.details.AccountInChain +import io.novafoundation.nova.feature_account_impl.presentation.account.details.model.AccountTypeAlert +import io.novafoundation.nova.runtime.ext.defaultComparatorFrom +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +fun polkadotVaultAccountTypeAlert( + pokadotVaultVariant: PolkadotVaultVariant, + variantConfig: PolkadotVaultVariantConfig, + resourceManager: ResourceManager +): AccountTypeAlert { + return AccountTypeAlert( + style = AlertView.Style( + backgroundColorRes = R.color.block_background, + iconRes = variantConfig.common.iconRes + ), + message = resourceManager.formatWithPolkadotVaultLabel(R.string.account_details_parity_signer_alert, pokadotVaultVariant) + ) +} + +fun AccountInChain.polkadotVaultTitle(resourceManager: ResourceManager, metaAccount: MetaAccount): String { + val polkadotVaultVariant = metaAccount.type.asPolkadotVaultVariantOrThrow() + return resourceManager.formatWithPolkadotVaultLabel(R.string.account_details_parity_signer_not_supported, polkadotVaultVariant) +} + +fun AccountInChain.From.mapToAccountHeader(resourceManager: ResourceManager): TextHeader { + val resId = when (this) { + AccountInChain.From.META_ACCOUNT -> R.string.account_shared_secret + AccountInChain.From.CHAIN_ACCOUNT -> R.string.account_custom_secret + } + + return TextHeader(resourceManager.getString(resId)) +} + +fun notHasAccountComparator(): Comparator { + return compareBy { !it.hasChainAccount } +} + +fun hasAccountComparator(): Comparator { + return compareBy { it.hasChainAccount } +} + +fun Comparator.withChainComparator(): Comparator { + return then(Chain.defaultComparatorFrom(AccountInChain::chain)) +} + +val AccountInChain.hasChainAccount + get() = projection != null diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/model/AccountTypeAlert.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/model/AccountTypeAlert.kt index 988879a8a1..78360ebfcb 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/model/AccountTypeAlert.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/details/model/AccountTypeAlert.kt @@ -4,5 +4,6 @@ import io.novafoundation.nova.common.view.AlertView class AccountTypeAlert( val style: AlertView.Style, - val text: String + val message: String, + val subMessage: CharSequence? = null ) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListFragment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListFragment.kt index 8ca735b546..2f47fea984 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListFragment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListFragment.kt @@ -6,20 +6,26 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import coil.ImageLoader import io.novafoundation.nova.common.base.BaseBottomSheetFragment -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder import io.novafoundation.nova.feature_account_impl.R +import javax.inject.Inject import kotlinx.android.synthetic.main.fragment_wallet_list.walletListBarAction import kotlinx.android.synthetic.main.fragment_wallet_list.walletListContent import kotlinx.android.synthetic.main.fragment_wallet_list.walletListTitle abstract class WalletListFragment : BaseBottomSheetFragment(), - AccountsAdapter.AccountItemHandler { + AccountHolder.AccountItemHandler { + + @Inject + lateinit var imageLoader: ImageLoader private val adapter by lazy(LazyThreadSafetyMode.NONE) { - AccountsAdapter(this, initialMode = viewModel.mode) + AccountsAdapter(this, imageLoader, initialMode = viewModel.mode, chainBorderColor = R.color.bottom_sheet_background) } override fun onCreateView( diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListViewModel.kt index e880d5ed47..90e46ff33d 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/WalletListViewModel.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_account_impl.presentation.account.list import io.novafoundation.nova.common.base.BaseViewModel -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter.Mode +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder.Mode +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountListingMixin abstract class WalletListViewModel : BaseViewModel() { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesBottomSheet.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesBottomSheet.kt new file mode 100644 index 0000000000..7dae319454 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesBottomSheet.kt @@ -0,0 +1,54 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import coil.ImageLoader +import io.novafoundation.nova.common.base.BaseBottomSheetFragment +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi +import io.novafoundation.nova.feature_account_impl.di.AccountFeatureComponent +import javax.inject.Inject +import kotlinx.android.synthetic.main.bottom_sheet_delegated_account_updates.delegatedAccountUpdatesDone +import kotlinx.android.synthetic.main.bottom_sheet_delegated_account_updates.delegatedAccountUpdatesLink +import kotlinx.android.synthetic.main.bottom_sheet_delegated_account_updates.delegatedAccountUpdatesList + +class DelegatedAccountUpdatesBottomSheet() : BaseBottomSheetFragment() { + + @Inject + lateinit var imageLoader: ImageLoader + + private val adapter by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { DelegatedAccountsAdapter(imageLoader) } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.bottom_sheet_delegated_account_updates, container, false) + } + + override fun initViews() { + delegatedAccountUpdatesLink.setOnClickListener { viewModel.clickAbout() } + delegatedAccountUpdatesDone.setOnClickListener { viewModel.clickDone() } + delegatedAccountUpdatesList.adapter = adapter + } + + override fun inject() { + FeatureUtils.getFeature( + requireContext(), + AccountFeatureApi::class.java + ) + .delegatedAccountUpdatesFactory() + .create(this) + .inject(this) + } + + override fun subscribe(viewModel: DelegatedAccountUpdatesViewModel) { + observeBrowserEvents(viewModel) + viewModel.accounts.observe { adapter.submitList(it) } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesViewModel.kt new file mode 100644 index 0000000000..ed6ec01a32 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountUpdatesViewModel.kt @@ -0,0 +1,40 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.data.network.AppLinksProvider +import io.novafoundation.nova.common.mixin.api.Browserable +import io.novafoundation.nova.common.utils.Event +import io.novafoundation.nova.common.utils.event +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor +import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.DelegatedMetaAccountUpdatesListingMixinFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +class DelegatedAccountUpdatesViewModel( + private val delegatedMetaAccountUpdatesListingMixinFactory: DelegatedMetaAccountUpdatesListingMixinFactory, + private val accountRouter: AccountRouter, + private val appLinksProvider: AppLinksProvider, + private val accountInteractor: AccountInteractor +) : BaseViewModel(), Browserable { + + private val listingMixin = delegatedMetaAccountUpdatesListingMixinFactory.create(viewModelScope) + + val accounts: Flow> = listingMixin.metaAccountsFlow + + override val openBrowserEvent = MutableLiveData>() + + fun clickAbout() { + openBrowserEvent.value = appLinksProvider.wikiProxy.event() + } + + fun clickDone() { + launch { + accountInteractor.switchToNotDeactivatedAccountIfNeeded() + + accountRouter.back() + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountsAdapter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountsAdapter.kt new file mode 100644 index 0000000000..e83959240d --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/DelegatedAccountsAdapter.kt @@ -0,0 +1,37 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates + +import android.view.ViewGroup +import coil.ImageLoader +import io.novafoundation.nova.common.list.GroupedListHolder +import io.novafoundation.nova.common.utils.inflateChild +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountDiffCallback +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountGroupViewHolderFactory +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.CommonAccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountTitleHolder +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountTitleGroupRvItem +import io.novafoundation.nova.feature_account_impl.R + +class DelegatedAccountsAdapter( + private val imageLoader: ImageLoader +) : CommonAccountsAdapter( + accountItemHandler = null, + imageLoader = imageLoader, + diffCallback = AccountDiffCallback(AccountTitleGroupRvItem::class.java), + groupFactory = DelegatedAccountsGroupFactory(), + groupBinder = { holder, item -> (holder as AccountTitleHolder).bind(item) }, + chainBorderColor = R.color.bottom_sheet_background, + initialMode = AccountHolder.Mode.VIEW +) + +private class DelegatedAccountsGroupFactory : AccountGroupViewHolderFactory { + + override fun create(parent: ViewGroup): GroupedListHolder { + return AccountTitleHolder( + parent.inflateChild( + R.layout.item_delegated_account_group, + false + ) + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesComponent.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesComponent.kt new file mode 100644 index 0000000000..042959dbcf --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesComponent.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates.DelegatedAccountUpdatesBottomSheet + +@Subcomponent( + modules = [ + DelegatedAccountUpdatesModule::class + ] +) +@ScreenScope +interface DelegatedAccountUpdatesComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment + ): DelegatedAccountUpdatesComponent + } + + fun inject(bottomSheet: DelegatedAccountUpdatesBottomSheet) +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesModule.kt new file mode 100644 index 0000000000..69dbb66ff7 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/delegationUpdates/di/DelegatedAccountUpdatesModule.kt @@ -0,0 +1,44 @@ +package io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.data.network.AppLinksProvider +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor +import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.DelegatedMetaAccountUpdatesListingMixinFactory +import io.novafoundation.nova.feature_account_impl.presentation.account.list.delegationUpdates.DelegatedAccountUpdatesViewModel + +@Module(includes = [ViewModelModule::class]) +class DelegatedAccountUpdatesModule { + + @Provides + @IntoMap + @ViewModelKey(DelegatedAccountUpdatesViewModel::class) + fun provideViewModel( + delegatedMetaAccountUpdatesListingMixinFactory: DelegatedMetaAccountUpdatesListingMixinFactory, + accountRouter: AccountRouter, + appLinksProvider: AppLinksProvider, + accountInteractor: AccountInteractor + ): ViewModel { + return DelegatedAccountUpdatesViewModel( + delegatedMetaAccountUpdatesListingMixinFactory, + accountRouter, + appLinksProvider, + accountInteractor + ) + } + + @Provides + fun provideViewModelCreator( + fragment: Fragment, + viewModelFactory: ViewModelProvider.Factory + ): DelegatedAccountUpdatesViewModel { + return ViewModelProvider(fragment, viewModelFactory).get(DelegatedAccountUpdatesViewModel::class.java) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/selectAddress/SelectAddressViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/selectAddress/SelectAddressViewModel.kt index f7f779b74a..8405dcfe04 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/selectAddress/SelectAddressViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/selectAddress/SelectAddressViewModel.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_account_impl.presentation.account.list.selectAddress import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectAddressForTransactionRequester import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectAddressForTransactionResponder import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter @@ -20,7 +20,7 @@ class SelectAddressViewModel( override val walletsListingMixin = accountListingMixinFactory.create(this, request.fromChainId, request.destinationChainId, request.selectedAddress) - override val mode: AccountsAdapter.Mode = AccountsAdapter.Mode.SWITCH + override val mode: AccountHolder.Mode = AccountHolder.Mode.SWITCH override fun accountClicked(accountModel: AccountUi) { launch { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletFragment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletFragment.kt index ce3b5ca44d..12e9fc7301 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletFragment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletFragment.kt @@ -24,4 +24,9 @@ class SwitchWalletFragment : WalletListFragment() { .create(this) .inject(this) } + + override fun onDestroy() { + super.onDestroy() + viewModel.onDestroy() + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletViewModel.kt index 69a2b65772..9f2ba595ee 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/SwitchWalletViewModel.kt @@ -1,22 +1,33 @@ package io.novafoundation.nova.feature_account_impl.presentation.account.list.switching +import io.novafoundation.nova.common.utils.coroutines.RootScope +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountWithBalanceListingMixinFactory import io.novafoundation.nova.feature_account_impl.presentation.account.list.WalletListViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class SwitchWalletViewModel( private val accountInteractor: AccountInteractor, private val router: AccountRouter, + private val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry, + private val rootScope: RootScope, accountListingMixinFactory: MetaAccountWithBalanceListingMixinFactory, ) : WalletListViewModel() { override val walletsListingMixin = accountListingMixinFactory.create(this) - override val mode: AccountsAdapter.Mode = AccountsAdapter.Mode.SWITCH + override val mode: AccountHolder.Mode = AccountHolder.Mode.SWITCH + + init { + if (metaAccountsUpdatesRegistry.hasUpdates()) { + router.openDelegatedAccountsUpdates() + } + } override fun accountClicked(accountModel: AccountUi) { launch { @@ -29,4 +40,11 @@ class SwitchWalletViewModel( fun settingsClicked() { router.openWallets() } + + fun onDestroy() { + rootScope.launch(Dispatchers.Default) { + metaAccountsUpdatesRegistry.clear() + accountInteractor.removeDeactivatedMetaAccounts() + } + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/di/SwitchWalletModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/di/SwitchWalletModule.kt index 250ac8d7a8..3c0a2372e0 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/di/SwitchWalletModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/list/switching/di/SwitchWalletModule.kt @@ -8,6 +8,8 @@ import dagger.Provides 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.utils.coroutines.RootScope +import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountWithBalanceListingMixinFactory @@ -23,11 +25,15 @@ class SwitchWalletModule { accountInteractor: AccountInteractor, router: AccountRouter, accountListingMixinFactory: MetaAccountWithBalanceListingMixinFactory, + metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry, + rootScope: RootScope ): ViewModel { return SwitchWalletViewModel( accountInteractor = accountInteractor, router = router, accountListingMixinFactory = accountListingMixinFactory, + metaAccountsUpdatesRegistry = metaAccountsUpdatesRegistry, + rootScope = rootScope ) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentFragment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentFragment.kt index 29ee647e89..6d5b22fc2b 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentFragment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentFragment.kt @@ -3,19 +3,25 @@ package io.novafoundation.nova.feature_account_impl.presentation.account.managem import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import coil.ImageLoader import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.view.dialog.warningDialog import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.di.AccountFeatureComponent +import javax.inject.Inject import kotlinx.android.synthetic.main.fragment_accounts.accountListToolbar import kotlinx.android.synthetic.main.fragment_accounts.accountsList import kotlinx.android.synthetic.main.fragment_accounts.addAccount -class WalletManagmentFragment : BaseFragment(), AccountsAdapter.AccountItemHandler { +class WalletManagmentFragment : BaseFragment(), AccountHolder.AccountItemHandler { + + @Inject + lateinit var imageLoader: ImageLoader private lateinit var adapter: AccountsAdapter @@ -26,7 +32,12 @@ class WalletManagmentFragment : BaseFragment(), Accoun ) = layoutInflater.inflate(R.layout.fragment_accounts, container, false) override fun initViews() { - adapter = AccountsAdapter(this, initialMode = viewModel.mode.value) + adapter = AccountsAdapter( + this, + imageLoader, + initialMode = viewModel.mode.value, + chainBorderColor = R.color.secondary_screen_background + ) accountsList.setHasFixedSize(true) accountsList.adapter = adapter diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentViewModel.kt index 5eabd353f0..c4f674abb6 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/WalletManagmentViewModel.kt @@ -7,8 +7,8 @@ import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingOrDenyingAc import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter.Mode +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder.Mode +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountWithBalanceListingMixinFactory @@ -21,15 +21,15 @@ class WalletManagmentViewModel( private val accountRouter: AccountRouter, private val resourceManager: ResourceManager, private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, - private val accountListingMixinFactory: MetaAccountWithBalanceListingMixinFactory, + private val accountListingMixinFactory: MetaAccountWithBalanceListingMixinFactory ) : BaseViewModel() { val walletsListingMixin = accountListingMixinFactory.create(this) - val mode = MutableStateFlow(Mode.VIEW) + val mode = MutableStateFlow(Mode.SELECT) val toolbarAction = mode.map { - if (it == Mode.VIEW) { + if (it == Mode.SELECT) { resourceManager.getString(R.string.common_edit) } else { resourceManager.getString(R.string.common_done) @@ -40,11 +40,11 @@ class WalletManagmentViewModel( val confirmAccountDeletion = actionAwaitableMixinFactory.confirmingOrDenyingAction() fun accountClicked(accountModel: AccountUi) { - accountRouter.openAccountDetails(accountModel.id) + accountRouter.openWalletDetails(accountModel.id) } fun editClicked() { - val newMode = if (mode.value == Mode.VIEW) Mode.EDIT else Mode.VIEW + val newMode = if (mode.value == Mode.SELECT) Mode.EDIT else Mode.SELECT mode.value = newMode } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/di/WalletManagmentModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/di/WalletManagmentModule.kt index 00ec5289fc..02f75fc60a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/di/WalletManagmentModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/management/di/WalletManagmentModule.kt @@ -28,7 +28,13 @@ class WalletManagmentModule { actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, metaAccountListingMixinFactory: MetaAccountWithBalanceListingMixinFactory ): ViewModel { - return WalletManagmentViewModel(interactor, router, resourceManager, actionAwaitableMixinFactory, metaAccountListingMixinFactory) + return WalletManagmentViewModel( + interactor, + router, + resourceManager, + actionAwaitableMixinFactory, + metaAccountListingMixinFactory + ) } @Provides diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/wallet/WalletUiUseCaseImpl.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/wallet/WalletUiUseCaseImpl.kt index 04d42462af..486f51ca20 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/wallet/WalletUiUseCaseImpl.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/account/wallet/WalletUiUseCaseImpl.kt @@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.address.AddressIconGenerator.Companion.BACKGROUND_DEFAULT import io.novafoundation.nova.common.address.AddressIconGenerator.Companion.BACKGROUND_TRANSPARENT +import io.novafoundation.nova.common.address.AddressIconGenerator.Companion.SIZE_MEDIUM import io.novafoundation.nova.common.utils.ByteArrayComparator import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -53,39 +54,40 @@ class WalletUiUseCaseImpl( return WalletModel( metaId = metaAccount.id, name = metaAccount.name, - icon = walletIcon(metaAccount) + icon = walletIcon(metaAccount, SIZE_MEDIUM) ) } override suspend fun walletIcon( metaAccount: MetaAccount, + iconSize: Int, transparentBackground: Boolean ): Drawable { val seed = metaAccount.walletIconSeed() - return generateWalletIcon(seed, transparentBackground) + return generateWalletIcon(seed, iconSize, transparentBackground) } override suspend fun walletUiFor(metaAccount: MetaAccount): WalletModel { return WalletModel( metaId = metaAccount.id, name = metaAccount.name, - icon = walletIcon(metaAccount, transparentBackground = true) + icon = walletIcon(metaAccount, SIZE_MEDIUM, transparentBackground = true) ) } private suspend fun maybeGenerateIcon(accountId: AccountId, shouldGenerate: Boolean): Drawable? { return if (shouldGenerate) { - generateWalletIcon(seed = accountId, transparentBackground = true) + generateWalletIcon(seed = accountId, iconSize = SIZE_MEDIUM, transparentBackground = true) } else { null } } - private suspend fun generateWalletIcon(seed: ByteArray, transparentBackground: Boolean): Drawable { + private suspend fun generateWalletIcon(seed: ByteArray, iconSize: Int, transparentBackground: Boolean): Drawable { return addressIconGenerator.createAddressIcon( accountId = seed, - sizeInDp = AddressIconGenerator.SIZE_MEDIUM, + sizeInDp = iconSize, backgroundColorRes = if (transparentBackground) BACKGROUND_TRANSPARENT else BACKGROUND_DEFAULT ) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/mixin/addAccountChooser/AddAccountLauncherProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/mixin/addAccountChooser/AddAccountLauncherProvider.kt index 846ea9e0e7..1fd646b8c4 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/mixin/addAccountChooser/AddAccountLauncherProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/mixin/addAccountChooser/AddAccountLauncherProvider.kt @@ -44,9 +44,9 @@ class AddAccountLauncherProvider( when (metaAccount.type) { LightMetaAccount.Type.SECRETS -> launchAddFromSecrets(chain, metaAccount) LightMetaAccount.Type.WATCH_ONLY -> launchAddWatchOnly(chain, metaAccount) - // adding chain accounts is not supported for Polkadot Vault like wallets - LightMetaAccount.Type.PARITY_SIGNER, LightMetaAccount.Type.POLKADOT_VAULT -> {} LightMetaAccount.Type.LEDGER -> launchAddLedger(chain, metaAccount) + // adding chain accounts is not supported for Polkadot Vault like wallets and for Proxied wallets + LightMetaAccount.Type.PARITY_SIGNER, LightMetaAccount.Type.POLKADOT_VAULT, LightMetaAccount.Type.PROXIED -> {} } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/sign/notSupported/AcknowledgeSigningNotSupportedBottomSheet.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/sign/notSupported/AcknowledgeSigningNotSupportedBottomSheet.kt index 84d3d39f44..bf2fabfa98 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/sign/notSupported/AcknowledgeSigningNotSupportedBottomSheet.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/common/sign/notSupported/AcknowledgeSigningNotSupportedBottomSheet.kt @@ -19,8 +19,8 @@ class AcknowledgeSigningNotSupportedBottomSheet( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - title.setText(R.string.account_parity_signer_not_supported_title) - subtitle.text = payload.message + titleView.setText(R.string.account_parity_signer_not_supported_title) + subtitleView.text = payload.message applySolidIconStyle(payload.iconRes) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/mixin/selectWallet/SelectWalletViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/mixin/selectWallet/SelectWalletViewModel.kt index 85b0f312e9..d935a4fb6c 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/mixin/selectWallet/SelectWalletViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/mixin/selectWallet/SelectWalletViewModel.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_account_impl.presentation.mixin.selectWallet import io.novafoundation.nova.common.navigation.requireLastInput -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletCommunicator import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletResponder import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter @@ -22,7 +22,7 @@ class SelectWalletViewModel( isMetaAccountSelected = { currentSelectedId == it.id } ) - override val mode: AccountsAdapter.Mode = AccountsAdapter.Mode.SWITCH + override val mode: AccountHolder.Mode = AccountHolder.Mode.SWITCH override fun accountClicked(accountModel: AccountUi) { responder.respond(SelectWalletCommunicator.Response(accountModel.id)) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/connect/finish/di/FinishImportParitySignerModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/connect/finish/di/FinishImportParitySignerModule.kt index d7020dd04a..64432f042e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/connect/finish/di/FinishImportParitySignerModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/connect/finish/di/FinishImportParitySignerModule.kt @@ -12,7 +12,7 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_impl.data.repository.ParitySignerRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.paritySigner.ParitySignerAddAccountRepository import io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.finish.FinishImportParitySignerInteractor import io.novafoundation.nova.feature_account_impl.domain.paritySigner.connect.finish.RealFinishImportParitySignerInteractor import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter @@ -25,9 +25,9 @@ class FinishImportParitySignerModule { @Provides @ScreenScope fun provideInteractor( - paritySignerRepository: ParitySignerRepository, + paritySignerAddAccountRepository: ParitySignerAddAccountRepository, accountRepository: AccountRepository - ): FinishImportParitySignerInteractor = RealFinishImportParitySignerInteractor(paritySignerRepository, accountRepository) + ): FinishImportParitySignerInteractor = RealFinishImportParitySignerInteractor(paritySignerAddAccountRepository, accountRepository) @Provides @IntoMap diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/ScanSignParitySignerViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/ScanSignParitySignerViewModel.kt index 61b9263c7e..6de400028a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/ScanSignParitySignerViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/ScanSignParitySignerViewModel.kt @@ -5,9 +5,9 @@ import io.novafoundation.nova.common.mixin.actionAwaitable.awaitAction import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction import io.novafoundation.nova.common.presentation.scan.ScanQrViewModel import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.SharedState import io.novafoundation.nova.common.utils.getOrThrow import io.novafoundation.nova.common.utils.permissions.PermissionsAsker +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.formatWithPolkadotVaultLabel import io.novafoundation.nova.feature_account_api.presenatation.sign.signed import io.novafoundation.nova.feature_account_impl.R @@ -18,7 +18,6 @@ import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sig import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.model.ScanSignParitySignerPayload import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.model.mapValidityPeriodFromParcel import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper.Sr25519 -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch @@ -26,7 +25,7 @@ class ScanSignParitySignerViewModel( private val router: AccountRouter, permissionsAsker: PermissionsAsker.Presentation, private val interactor: ScanSignParitySignerInteractor, - private val signSharedState: SharedState, + private val signSharedState: SigningSharedState, private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, private val responder: PolkadotVaultVariantSignCommunicator, private val payload: ScanSignParitySignerPayload, @@ -55,7 +54,7 @@ class ScanSignParitySignerViewModel( } override suspend fun scanned(result: String) { - interactor.encodeAndVerifySignature(signSharedState.getOrThrow(), result) + interactor.encodeAndVerifySignature(signSharedState.getOrThrow().extrinsic, result) .onSuccess(::respondResult) .onFailure { invalidQrConfirmation.awaitAction() diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/di/ScanSignParitySignerModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/di/ScanSignParitySignerModule.kt index 5a999f9e7d..addb0dcfa1 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/di/ScanSignParitySignerModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/scan/di/ScanSignParitySignerModule.kt @@ -10,9 +10,9 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.MutableSharedState import io.novafoundation.nova.common.utils.permissions.PermissionsAsker import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignCommunicator import io.novafoundation.nova.feature_account_impl.domain.paritySigner.sign.scan.RealScanSignParitySignerInteractor import io.novafoundation.nova.feature_account_impl.domain.paritySigner.sign.scan.ScanSignParitySignerInteractor @@ -20,7 +20,6 @@ import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.common.QrCodeExpiredPresentableFactory import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.ScanSignParitySignerViewModel import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.model.ScanSignParitySignerPayload -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic @Module(includes = [ViewModelModule::class]) class ScanSignParitySignerModule { @@ -42,7 +41,7 @@ class ScanSignParitySignerModule { router: AccountRouter, permissionsAsker: PermissionsAsker.Presentation, interactor: ScanSignParitySignerInteractor, - signSharedState: MutableSharedState, + signSharedState: SigningSharedState, actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, communicator: PolkadotVaultVariantSignCommunicator, payload: ScanSignParitySignerPayload, diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/ShowSignParitySignerViewModel.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/ShowSignParitySignerViewModel.kt index 5d2e6e6df7..a38f88628b 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/ShowSignParitySignerViewModel.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/ShowSignParitySignerViewModel.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.getOrThrow import io.novafoundation.nova.common.utils.mediatorLiveData import io.novafoundation.nova.common.utils.updateFrom +import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider @@ -28,7 +29,6 @@ import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sig import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.fearless_utils.extensions.toHexString -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.genesisHash import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -37,7 +37,7 @@ import kotlinx.coroutines.launch class ShowSignParitySignerViewModel( private val router: AccountRouter, private val interactor: ShowSignParitySignerInteractor, - private val signSharedState: SharedState, + private val signSharedState: SharedState, private val qrCodeGenerator: QrCodeGenerator, private val responder: PolkadotVaultVariantSignCommunicator, private val payload: ShowSignParitySignerPayload, @@ -59,7 +59,7 @@ class ShowSignParitySignerViewModel( val chain = flowOf { val signPayload = signSharedState.getOrThrow() - val chainId = signPayload.genesisHash.toHexString() + val chainId = signPayload.extrinsic.genesisHash.toHexString() chainRegistry.getChain(chainId) }.shareInBackground() @@ -67,7 +67,7 @@ class ShowSignParitySignerViewModel( val qrCodeSequence = flowOf { val signPayload = signSharedState.getOrThrow() - val frames = interactor.qrCodeContent(signPayload).frames + val frames = interactor.qrCodeContent(signPayload.extrinsic).frames frames.map { qrCodeGenerator.generateQrBitmap(it) } .cycleMultiple() @@ -76,11 +76,11 @@ class ShowSignParitySignerViewModel( val addressModel = chain.map { chain -> val signPayload = signSharedState.getOrThrow() - addressIconGenerator.createAccountAddressModel(chain, signPayload.accountId, addressDisplayUseCase) + addressIconGenerator.createAccountAddressModel(chain, signPayload.extrinsic.accountId, addressDisplayUseCase) }.shareInBackground() val validityPeriod = flowOf { - extrinsicValidityUseCase.extrinsicValidityPeriod(signSharedState.getOrThrow()) + extrinsicValidityUseCase.extrinsicValidityPeriod(signSharedState.getOrThrow().extrinsic) }.shareInBackground() val title = resourceManager.formatWithPolkadotVaultLabel(R.string.account_parity_signer_sign_title, payload.polkadotVaultVariant) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/di/ShowSignParitySignerModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/di/ShowSignParitySignerModule.kt index 7345465164..2a3de95543 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/di/ShowSignParitySignerModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/paritySigner/sign/show/di/ShowSignParitySignerModule.kt @@ -13,6 +13,7 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.QrCodeGenerator import io.novafoundation.nova.common.utils.SharedState +import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions @@ -25,7 +26,6 @@ import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sig import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.show.ShowSignParitySignerViewModel import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic @Module(includes = [ViewModelModule::class]) class ShowSignParitySignerModule { @@ -39,7 +39,7 @@ class ShowSignParitySignerModule { @ViewModelKey(ShowSignParitySignerViewModel::class) fun provideViewModel( interactor: ShowSignParitySignerInteractor, - signSharedState: SharedState, + signSharedState: SharedState, qrCodeGenerator: QrCodeGenerator, communicator: PolkadotVaultVariantSignCommunicator, payload: ShowSignParitySignerPayload, diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignNotEnoughPermissionBottomSheet.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignNotEnoughPermissionBottomSheet.kt new file mode 100644 index 0000000000..22e95999bc --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignNotEnoughPermissionBottomSheet.kt @@ -0,0 +1,23 @@ +package io.novafoundation.nova.feature_account_impl.presentation.proxy.sign + +import android.content.Context +import android.os.Bundle +import io.novafoundation.nova.common.view.bottomSheet.ActionNotAllowedBottomSheet +import io.novafoundation.nova.feature_account_impl.R + +class ProxySignNotEnoughPermissionBottomSheet( + context: Context, + private val subtitle: CharSequence, + onSuccess: () -> Unit +) : ActionNotAllowedBottomSheet(context, onSuccess) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + titleView.setText(R.string.proxy_signing_not_enough_permission_title) + subtitleView.setText(subtitle) + buttonView.setText(R.string.common_ok_back) + + applySolidIconStyle(R.drawable.ic_proxy) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignWarningBottomSheet.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignWarningBottomSheet.kt new file mode 100644 index 0000000000..7ac47a699a --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/ProxySignWarningBottomSheet.kt @@ -0,0 +1,44 @@ +package io.novafoundation.nova.feature_account_impl.presentation.proxy.sign + +import android.content.Context +import android.os.Bundle +import io.novafoundation.nova.common.utils.WithContextExtensions +import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet +import io.novafoundation.nova.feature_account_impl.R +import kotlinx.android.synthetic.main.bottom_sheet_proxy_warning.proxySigningWarningCancel +import kotlinx.android.synthetic.main.bottom_sheet_proxy_warning.proxySigningWarningContinue +import kotlinx.android.synthetic.main.bottom_sheet_proxy_warning.proxySigningWarningDontShowAgain +import kotlinx.android.synthetic.main.bottom_sheet_proxy_warning.proxySigningWarningMessage + +class ProxySignWarningBottomSheet( + context: Context, + private val subtitle: CharSequence, + private val onFinish: (Boolean) -> Unit, + private val dontShowAgain: () -> Unit +) : BaseBottomSheet(context, io.novafoundation.nova.common.R.style.BottomSheetDialog), WithContextExtensions by WithContextExtensions(context) { + + private var finishWithContinue = false + + init { + setContentView(R.layout.bottom_sheet_proxy_warning) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + proxySigningWarningMessage.setText(subtitle) + + proxySigningWarningContinue.setDismissingClickListener { + if (proxySigningWarningDontShowAgain.isChecked) { + dontShowAgain() + } + finishWithContinue = true + } + + proxySigningWarningCancel.setDismissingClickListener { + finishWithContinue = false + } + + setOnDismissListener { onFinish(finishWithContinue) } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/RealProxySigningPresenter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/RealProxySigningPresenter.kt new file mode 100644 index 0000000000..d7fbc7dcf8 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/proxy/sign/RealProxySigningPresenter.kt @@ -0,0 +1,143 @@ +package io.novafoundation.nova.feature_account_impl.presentation.proxy.sign + +import android.text.SpannableStringBuilder +import io.novafoundation.nova.common.data.storage.Preferences +import io.novafoundation.nova.common.resources.ContextManager +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.colorSpan +import io.novafoundation.nova.common.utils.formatting.spannable.SpannableFormatter +import io.novafoundation.nova.common.utils.toSpannable +import io.novafoundation.nova.common.view.dialog.dialog +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.ProxyAccount +import io.novafoundation.nova.feature_account_api.presenatation.account.proxy.ProxySigningPresenter +import io.novafoundation.nova.feature_account_impl.R +import io.novafoundation.nova.feature_account_impl.presentation.common.sign.notSupported.SigningNotSupportedPresentable +import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.math.BigInteger +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +private const val KEY_DONT_SHOW_AGAIN = "proxy_sign_warning_dont_show_again" + +class RealProxySigningPresenter( + private val contextManager: ContextManager, + private val resourceManager: ResourceManager, + private val signingNotSupportedPresentable: SigningNotSupportedPresentable, + private val preferences: Preferences +) : ProxySigningPresenter { + + override suspend fun acknowledgeProxyOperation(proxiedMetaAccount: MetaAccount, proxyMetaAccount: MetaAccount): Boolean = withContext(Dispatchers.Main) { + if (noNeedToShowWarning(proxiedMetaAccount)) { + return@withContext true + } + + val resumingAllowed = suspendCoroutine { continuation -> + ProxySignWarningBottomSheet( + context = contextManager.getActivity()!!, + subtitle = formatSubtitleForWarning(proxyMetaAccount), + onFinish = { + continuation.resume(it) + }, + dontShowAgain = { dontShowAgain(proxiedMetaAccount) } + ).show() + } + + return@withContext resumingAllowed + } + + override suspend fun notEnoughPermission( + proxiedMetaAccount: MetaAccount, + proxyMetaAccount: MetaAccount, + proxyTypes: List + ) = withContext(Dispatchers.Main) { + suspendCoroutine { continuation -> + ProxySignNotEnoughPermissionBottomSheet( + context = contextManager.getActivity()!!, + subtitle = formatNotEnoughPermissionWarning(proxiedMetaAccount, proxyMetaAccount, proxyTypes), + onSuccess = { continuation.resume(Unit) } + ).show() + } + } + + override suspend fun signingIsNotSupported() { + signingNotSupportedPresentable.presentSigningNotSupported( + SigningNotSupportedPresentable.Payload( + iconRes = R.drawable.ic_proxy, + message = resourceManager.getString(R.string.proxy_signing_is_not_supported_message) + ) + ) + } + + override suspend fun notEnoughFee( + metaAccount: MetaAccount, + chainAsset: Chain.Asset, + availableBalance: BigInteger, + fee: Fee + ) = withContext(Dispatchers.Main) { + suspendCoroutine { continuation -> + dialog(contextManager.getActivity()!!) { + setTitle(R.string.error_not_enough_to_pay_fee_title) + setMessage( + resourceManager.getString( + R.string.proxy_error_not_enough_to_pay_fee_message, + metaAccount.name, + chainAsset.amountFromPlanks(fee.amount).formatTokenAmount(chainAsset), + chainAsset.amountFromPlanks(availableBalance).formatTokenAmount(chainAsset), + ) + ) + + chainAsset.amountFromPlanks(availableBalance).formatTokenAmount(chainAsset) + + setPositiveButton(io.novafoundation.nova.common.R.string.common_close) { _, _ -> continuation.resume(Unit) } + } + } + } + + private fun noNeedToShowWarning(proxyMetaAccount: MetaAccount): Boolean { + return preferences.getBoolean(makePrefsKey(proxyMetaAccount), false) + } + + private fun dontShowAgain(proxyMetaAccount: MetaAccount) { + preferences.putBoolean(makePrefsKey(proxyMetaAccount), true) + } + + private fun makePrefsKey(proxyMetaAccount: MetaAccount): String { + return "${KEY_DONT_SHOW_AGAIN}_${proxyMetaAccount.id}" + } + + private fun formatSubtitleForWarning(proxyMetaAccount: MetaAccount): CharSequence { + val subtitle = resourceManager.getString(R.string.proxy_signing_warning_message) + val primaryColor = resourceManager.getColor(R.color.text_primary) + val proxyName = proxyMetaAccount.name.toSpannable(colorSpan(primaryColor)) + return SpannableFormatter.format(subtitle, proxyName) + } + + private fun formatNotEnoughPermissionWarning( + proxiedMetaAccount: MetaAccount, + proxyMetaAccount: MetaAccount, + proxyTypes: List + ): CharSequence { + val primaryColor = resourceManager.getColor(R.color.text_primary) + + val proxiedName = proxiedMetaAccount.name.toSpannable(colorSpan(primaryColor)) + val proxyName = proxyMetaAccount.name.toSpannable(colorSpan(primaryColor)) + + return if (proxyTypes.isNotEmpty()) { + val subtitle = resourceManager.getString(R.string.proxy_signing_not_enough_permission_message) + + val proxyTypesBuffer = SpannableStringBuilder() + val proxyTypesCharSequence = proxyTypes.joinTo(proxyTypesBuffer) { it.name.toSpannable(colorSpan(primaryColor)) } + + SpannableFormatter.format(subtitle, proxiedName, proxyName, proxyTypesCharSequence) + } else { + val subtitle = resourceManager.getString(R.string.proxy_signing_none_permissions_message) + SpannableFormatter.format(subtitle, proxiedName, proxyName) + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/change/di/ChangeWatchAccountModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/change/di/ChangeWatchAccountModule.kt index 2ce3bc3a7c..6ba93fb9f5 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/change/di/ChangeWatchAccountModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/change/di/ChangeWatchAccountModule.kt @@ -10,10 +10,9 @@ import io.novafoundation.nova.common.di.scope.ScreenScope 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.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.AddressInputMixinFactory -import io.novafoundation.nova.feature_account_impl.data.repository.WatchOnlyRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly.WatchOnlyAddAccountRepository import io.novafoundation.nova.feature_account_impl.domain.watchOnly.change.ChangeWatchAccountInteractor import io.novafoundation.nova.feature_account_impl.domain.watchOnly.change.RealChangeWatchAccountInteractor import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter @@ -26,9 +25,8 @@ class ChangeWatchAccountModule { @Provides @ScreenScope fun provideInteractor( - watchOnlyRepository: WatchOnlyRepository, - accountRepository: AccountRepository - ): ChangeWatchAccountInteractor = RealChangeWatchAccountInteractor(accountRepository, watchOnlyRepository) + watchOnlyAddAccountRepository: WatchOnlyAddAccountRepository + ): ChangeWatchAccountInteractor = RealChangeWatchAccountInteractor(watchOnlyAddAccountRepository) @Provides @IntoMap diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/create/di/CreateWatchWalletModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/create/di/CreateWatchWalletModule.kt index fc11f6545d..58aeb7b875 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/create/di/CreateWatchWalletModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/create/di/CreateWatchWalletModule.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInter import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.AddressInputMixinFactory import io.novafoundation.nova.feature_account_impl.data.repository.WatchOnlyRepository +import io.novafoundation.nova.feature_account_impl.data.repository.addAccount.watchOnly.WatchOnlyAddAccountRepository import io.novafoundation.nova.feature_account_impl.domain.watchOnly.create.CreateWatchWalletInteractor import io.novafoundation.nova.feature_account_impl.domain.watchOnly.create.RealCreateWatchWalletInteractor import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter @@ -26,8 +27,9 @@ class CreateWatchWalletModule { @ScreenScope fun provideInteractor( watchOnlyRepository: WatchOnlyRepository, + watchOnlyAddAccountRepository: WatchOnlyAddAccountRepository, accountRepository: AccountRepository - ): CreateWatchWalletInteractor = RealCreateWatchWalletInteractor(watchOnlyRepository, accountRepository) + ): CreateWatchWalletInteractor = RealCreateWatchWalletInteractor(watchOnlyRepository, watchOnlyAddAccountRepository, accountRepository) @Provides @IntoMap diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/sign/WatchOnlySignBottomSheet.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/sign/WatchOnlySignBottomSheet.kt index 350f9f654c..05f39cc80e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/sign/WatchOnlySignBottomSheet.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/watchOnly/sign/WatchOnlySignBottomSheet.kt @@ -13,8 +13,8 @@ class WatchOnlySignBottomSheet( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - title.setText(R.string.account_watch_key_missing_title) - subtitle.setText(R.string.account_watch_key_missing_description) + titleView.setText(R.string.account_watch_key_missing_title) + subtitleView.setText(R.string.account_watch_key_missing_description) applyDashedIconStyle(R.drawable.ic_key_missing) } diff --git a/feature-account-impl/src/main/res/layout/bottom_sheet_delegated_account_updates.xml b/feature-account-impl/src/main/res/layout/bottom_sheet_delegated_account_updates.xml new file mode 100644 index 0000000000..dee585a82b --- /dev/null +++ b/feature-account-impl/src/main/res/layout/bottom_sheet_delegated_account_updates.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature-account-impl/src/main/res/layout/bottom_sheet_proxy_warning.xml b/feature-account-impl/src/main/res/layout/bottom_sheet_proxy_warning.xml new file mode 100644 index 0000000000..5c757a8f1b --- /dev/null +++ b/feature-account-impl/src/main/res/layout/bottom_sheet_proxy_warning.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature-account-impl/src/main/res/layout/fragment_account_details.xml b/feature-account-impl/src/main/res/layout/fragment_wallet_details.xml similarity index 100% rename from feature-account-impl/src/main/res/layout/fragment_account_details.xml rename to feature-account-impl/src/main/res/layout/fragment_wallet_details.xml diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt index 517197a7c0..38207b755d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt @@ -1,8 +1,11 @@ package io.novafoundation.nova.feature_assets.domain.send import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.InlineFee +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer @@ -14,13 +17,12 @@ import io.novafoundation.nova.feature_wallet_api.domain.implementations.transfer import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration import io.novafoundation.nova.feature_wallet_api.domain.model.RecipientSearchResult -import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.repository.ParachainInfoRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.math.BigDecimal class SendInteractor( private val walletRepository: WalletRepository, @@ -78,25 +80,29 @@ class SendInteractor( crossChainFee.reserve.orZero() + crossChainFee.destination.orZero() } - InlineFee(feePlanks) + val submissionOriginId = transfer.sender.requireAccountIdIn(transfer.originChain) + val submissionOrigin = SubmissionOrigin.singleOrigin(submissionOriginId) // cross-chain fee is always paid by requested account id + + SubstrateFee(feePlanks, submissionOrigin) } else { null } suspend fun performTransfer( transfer: WeightedAssetTransfer, - originFee: BigDecimal, - crossChainFee: BigDecimal?, + originFee: DecimalFee, + crossChainFee: DecimalFee?, ): Result<*> = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { - val crossChainFeePlanks = transfer.originChainAsset.planksFromAmount(crossChainFee!!) + val crossChainFeePlanks = crossChainFee!!.networkFee.amountByRequestedAccount val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! crossChainTransactor.performTransfer(config, transfer, crossChainFeePlanks) } else { getAssetTransfers(transfer).performTransfer(transfer) - .onSuccess { hash -> - walletRepository.insertPendingTransfer(hash, transfer, originFee) + .onSuccess { submission -> + // Insert used fee regardless of who paid it + walletRepository.insertPendingTransfer(submission.hash, transfer, originFee.networkFeeDecimalAmount) } } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/view/LedgerNotSupportedWarningBottomSheet.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/view/LedgerNotSupportedWarningBottomSheet.kt index d6d9acfa0a..fd7cec66d0 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/view/LedgerNotSupportedWarningBottomSheet.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/receive/view/LedgerNotSupportedWarningBottomSheet.kt @@ -14,8 +14,8 @@ class LedgerNotSupportedWarningBottomSheet( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - title.setText(R.string.assets_receive_ledger_not_supported_title) - subtitle.text = message + titleView.setText(R.string.assets_receive_ledger_not_supported_title) + subtitleView.text = message applySolidIconStyle(R.drawable.ic_ledger) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt index 8f4921bfab..ac49adb1cf 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt @@ -10,7 +10,7 @@ import java.math.BigDecimal class TransferDraft( val amount: BigDecimal, val originFee: FeeParcelModel, - val crossChainFee: BigDecimal?, + val crossChainFee: FeeParcelModel?, val origin: AssetPayload, val destination: AssetPayload, val recipientAddress: String, diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt index 4c96ebc202..70e2e91241 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt @@ -59,6 +59,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal @@ -113,6 +114,7 @@ class SelectSendViewModel( } private val availableCrossChainDestinations = availableCrossChainDestinations() + .onStart { emit(emptyList()) } .shareInBackground() val isSelectAddressAvailable = combine(originChain, destinationChain) { originChain, destinationChain -> @@ -190,8 +192,8 @@ class SelectSendViewModel( assetTransfer = transfer, fee = originFee, ), - crossChainFee = crossChainFee?.networkFeeDecimalAmount, - originFee = originFee.networkFeeDecimalAmount, + crossChainFee = crossChainFee, + originFee = originFee, originCommissionAsset = commissionAssetFlow.first(), originUsedAsset = originAssetFlow.first() ) @@ -334,7 +336,7 @@ class SelectSendViewModel( chainAssetId = validPayload.transfer.destinationChainAsset.id ), recipientAddress = validPayload.transfer.recipient, - crossChainFee = validPayload.crossChainFee, + crossChainFee = validPayload.crossChainFee?.let(::mapFeeToParcel), openAssetDetailsOnCompletion = payload is SendPayload.SpecifiedOrigin ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt index 50aa5caac4..68b652068d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt @@ -37,6 +37,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset @@ -47,7 +48,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch -import java.math.BigDecimal class ConfirmSendChainsModel( val origin: ChainUi, @@ -75,6 +75,7 @@ class ConfirmSendViewModel( Validatable by validationExecutor { private val originFee = mapFeeFromParcel(transferDraft.originFee) + private val crossChainFee = transferDraft.crossChainFee?.let(::mapFeeFromParcel) private val originChain by lazyAsync { chainRegistry.getChain(transferDraft.origin.chainId) } private val originAsset by lazyAsync { chainRegistry.asset(transferDraft.origin.chainId, transferDraft.origin.chainAssetId) } @@ -185,8 +186,8 @@ class ConfirmSendViewModel( } private fun setInitialState() = launch { - originFeeMixin.setFee(originFee.fee) - crossChainFeeMixin.setFee(transferDraft.crossChainFee) + originFeeMixin.setFee(originFee.genericFee) + crossChainFeeMixin.setFee(crossChainFee?.genericFee) } private suspend fun createAddressModel( @@ -204,8 +205,8 @@ class ConfirmSendViewModel( private fun performTransfer( transfer: WeightedAssetTransfer, - originFee: BigDecimal, - crossChainFee: BigDecimal? + originFee: DecimalFee, + crossChainFee: DecimalFee? ) = launch { sendInteractor.performTransfer(transfer, originFee, crossChainFee) .onSuccess { @@ -246,10 +247,10 @@ class ConfirmSendViewModel( commissionAssetToken = commissionAssetFlow.first().token, decimalFee = originFee, ), - originFee = originFee.networkFeeDecimalAmount, + originFee = originFee, originCommissionAsset = commissionAssetFlow.first(), originUsedAsset = assetFlow.first(), - crossChainFee = transferDraft.crossChainFee + crossChainFee = crossChainFee ) } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt index f635e7b590..b34007ec5d 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt @@ -28,7 +28,7 @@ class ContributeValidationsModule { feeExtractor = { it.fee }, availableBalanceProducer = { it.asset.transferable }, extraAmountExtractor = { it.contributionAmount }, - errorProducer = { _, _ -> ContributeValidationFailure.CannotPayFees } + errorProducer = { ContributeValidationFailure.CannotPayFees } ) @Provides diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/MoonbeamTermsValidationsModule.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/MoonbeamTermsValidationsModule.kt index b81f5583d4..7a7da1663b 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/MoonbeamTermsValidationsModule.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/MoonbeamTermsValidationsModule.kt @@ -19,7 +19,7 @@ class MoonbeamTermsValidationsModule { fun provideFeesValidation(): MoonbeamTermsValidation = MoonbeamTermsFeeValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.asset.transferable }, - errorProducer = { _, _ -> MoonbeamTermsValidationFailure.CANNOT_PAY_FEES } + errorProducer = { MoonbeamTermsValidationFailure.CANNOT_PAY_FEES } ) @Provides diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt index 852594e964..3b0d232328 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt @@ -2,7 +2,9 @@ package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute import android.os.Parcelable import io.novafoundation.nova.common.data.network.runtime.binding.ParaId +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn @@ -79,14 +81,14 @@ class CrowdloanContributeInteractor( contribution: BigDecimal, bonusPayload: BonusPayload?, customizationPayload: Parcelable?, - ) = formingSubmission( + ): Fee = formingSubmission( crowdloan = crowdloan, contribution = contribution, bonusPayload = bonusPayload, customizationPayload = customizationPayload, toCalculateFee = true ) { submission, chain, _ -> - extrinsicService.estimateFee(chain, submission) + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, submission) } suspend fun contribute( @@ -99,19 +101,17 @@ class CrowdloanContributeInteractor( customContributeManager.getFactoryOrNull(it)?.submitter?.submitOffChain(customizationPayload, bonusPayload, contribution) } - val txHash = formingSubmission( + val extrinsicSubmission = formingSubmission( crowdloan = crowdloan, contribution = contribution, bonusPayload = bonusPayload, toCalculateFee = false, customizationPayload = customizationPayload ) { submission, chain, account -> - val accountId = account.accountIdIn(chain)!! - - extrinsicService.submitExtrinsicWithAnySuitableWallet(chain, accountId) { submission() } + extrinsicService.submitExtrinsic(chain, TransactionOrigin.Wallet(account)) { submission() } }.getOrThrow() - txHash + extrinsicSubmission.hash } private suspend fun formingSubmission( diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/acala/AcalaContributeInteractor.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/acala/AcalaContributeInteractor.kt index dd0b69430c..9be399dd0c 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/acala/AcalaContributeInteractor.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/acala/AcalaContributeInteractor.kt @@ -42,7 +42,7 @@ class AcalaContributeInteractor( ): Result = runCatching { httpExceptionHandler.wrap { val selectedMetaAccount = accountRepository.getSelectedMetaAccount() - val signer = signerProvider.signerFor(selectedMetaAccount) + val signer = signerProvider.rootSignerFor(selectedMetaAccount) val (chain, chainAsset) = selectedAssetState.chainAndAsset() diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/moonbeam/MoonbeamCrowdloanInteractor.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/moonbeam/MoonbeamCrowdloanInteractor.kt index 30f1236fcd..5d98cb8000 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/moonbeam/MoonbeamCrowdloanInteractor.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/custom/moonbeam/MoonbeamCrowdloanInteractor.kt @@ -6,7 +6,9 @@ import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.asHexString import io.novafoundation.nova.common.utils.sha256 import io.novafoundation.nova.core.model.CryptoType +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn @@ -38,7 +40,6 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import retrofit2.HttpException -import java.math.BigInteger class VerificationError : Exception() @@ -105,10 +106,10 @@ class MoonbeamCrowdloanInteractor( } } - suspend fun calculateTermsFee(): BigInteger = withContext(Dispatchers.Default) { + suspend fun calculateTermsFee(): Fee = withContext(Dispatchers.Default) { val chain = selectedChainAssetState.chain() - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { systemRemark(fakeRemark()) } } @@ -124,7 +125,7 @@ class MoonbeamCrowdloanInteractor( val legalText = httpExceptionHandler.wrap { moonbeamApi.getLegalText() } val legalHash = legalText.encodeToByteArray().sha256().toHexString(withPrefix = false) - val signer = signerProvider.signerFor(metaAccount) + val signer = signerProvider.rootSignerFor(metaAccount) val signerPayload = SignerPayloadRaw.fromUtf8(legalHash, accountId) val signedHash = signer.signRaw(signerPayload).asHexString() @@ -132,9 +133,10 @@ class MoonbeamCrowdloanInteractor( val agreeRemarkRequest = AgreeRemarkRequest(currentAddress, signedHash) val remark = httpExceptionHandler.wrap { moonbeamApi.agreeRemark(parachainMetadata, agreeRemarkRequest) }.remark - val finalizedStatus = extrinsicService.submitAndWatchExtrinsicAnySuitableWallet(chain, metaAccount.accountIdIn(chain)!!) { + val finalizedStatus = extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { systemRemark(remark.encodeToByteArray()) } + .getOrThrow() .filterIsInstance() .first() diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt index caa2080ea1..8c40bc06ee 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt @@ -4,13 +4,14 @@ import android.os.Parcelable import io.novafoundation.nova.feature_crowdloan_impl.domain.main.Crowdloan import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.BonusPayload import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class ContributeValidationPayload( val crowdloan: Crowdloan, val customizationPayload: Parcelable?, val asset: Asset, - val fee: BigDecimal, + val fee: DecimalFee, val bonusPayload: BonusPayload?, val contributionAmount: BigDecimal, ) diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt index c20b75e344..02723cab9d 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class MoonbeamTermsPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset ) diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/CrowdloanRouter.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/CrowdloanRouter.kt index c7c173aecf..92ba6c56c6 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/CrowdloanRouter.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/CrowdloanRouter.kt @@ -32,5 +32,5 @@ interface CrowdloanRouter { fun openSwitchWallet() - fun openAccountDetails(metaId: Long) + fun openWalletDetails(metaId: Long) } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt index bfd6f2b3f1..c580567744 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt @@ -33,6 +33,7 @@ import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetToAssetMod import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.runtime.state.SingleAssetSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.Flow @@ -58,6 +59,8 @@ class ConfirmContributeViewModel( Validatable by validationExecutor, ExternalActions by externalActions { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val chain by lazyAsync { assetSharedState.chain() } override val openBrowserEvent = MutableLiveData>() @@ -82,7 +85,7 @@ class ConfirmContributeViewModel( val selectedAmount = payload.amount.toString() val feeFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(payload.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } @@ -161,7 +164,7 @@ class ConfirmContributeViewModel( private fun maybeGoToNext() = launch { val validationPayload = ContributeValidationPayload( crowdloan = crowdloanFlow.first(), - fee = payload.fee, + fee = decimalFee, asset = assetFlow.first(), customizationPayload = payload.customizationPayload, bonusPayload = payload.bonusPayload, diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/parcel/ConfirmContributePayload.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/parcel/ConfirmContributePayload.kt index ee9ced8b7d..5b58438da5 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/parcel/ConfirmContributePayload.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/parcel/ConfirmContributePayload.kt @@ -4,13 +4,14 @@ import android.os.Parcelable import io.novafoundation.nova.common.data.network.runtime.binding.ParaId import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.BonusPayload import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.select.parcel.ParachainMetadataParcelModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @Parcelize class ConfirmContributePayload( val paraId: ParaId, - val fee: BigDecimal, + val fee: FeeParcelModel, val amount: BigDecimal, val bonusPayload: BonusPayload?, val customizationPayload: Parcelable?, diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt index 50387757f4..9aa1a6871a 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt @@ -20,11 +20,12 @@ import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.sel 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.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import java.math.BigDecimal sealed class SubmitActionState { object Loading : SubmitActionState() @@ -93,13 +94,15 @@ class MoonbeamCrowdloanTermsViewModel( openBrowserEvent.value = Event(interactor.getTermsLink()) } - fun submitClicked() = requireFee { fee -> + fun submitClicked() = launch { submittingInProgressFlow.value = true + val fee = feeLoaderMixin.awaitDecimalFee() + submitAfterValidation(fee) } - private fun submitAfterValidation(fee: BigDecimal) = launch { + private fun submitAfterValidation(fee: DecimalFee) = launch { val validationPayload = MoonbeamTermsPayload( fee = fee, asset = assetUseCase.getCurrentAsset() @@ -128,15 +131,8 @@ class MoonbeamCrowdloanTermsViewModel( private fun loadFee() = launch { feeLoaderMixin.loadFee( coroutineScope = this, - feeConstructor = { - interactor.calculateTermsFee() - }, + feeConstructor = { interactor.calculateTermsFee() }, onRetryCancelled = ::backClicked ) } - - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt index 406d69cf28..89d25edf56 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt @@ -16,7 +16,6 @@ import io.novafoundation.nova.common.validation.CompositeValidation import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.progressConsumer -import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_crowdloan_impl.R import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.hasExtraBonusFlow @@ -40,6 +39,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -276,53 +277,48 @@ class CrowdloanContributeViewModel( customizationPayload, ) - SimpleFee(InlineFee(fee)) + SimpleFee(fee) }, onRetryCancelled = ::backClicked ) } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) + private fun maybeGoToNext() = launch { + _showNextProgress.value = true - private fun maybeGoToNext() = requireFee { fee -> - launch { - val contributionAmount = parsedAmountFlow.firstOrNull() ?: return@launch + val contributionAmount = parsedAmountFlow.firstOrNull() ?: return@launch - val customizationPayload = customizationConfiguration.first()?.let { - val (_, customViewState) = it + val customizationPayload = customizationConfiguration.first()?.let { + val (_, customViewState) = it - customViewState.customizationPayloadFlow.first() - } + customViewState.customizationPayloadFlow.first() + } - val validationPayload = ContributeValidationPayload( - crowdloan = crowdloanFlow.first(), - customizationPayload = customizationPayload, - fee = fee, - asset = assetFlow.first(), - bonusPayload = router.latestCustomBonus, - contributionAmount = contributionAmount - ) + val validationPayload = ContributeValidationPayload( + crowdloan = crowdloanFlow.first(), + customizationPayload = customizationPayload, + fee = feeLoaderMixin.awaitDecimalFee(), + asset = assetFlow.first(), + bonusPayload = router.latestCustomBonus, + contributionAmount = contributionAmount + ) - validationExecutor.requireValid( - validationSystem = customizedValidationSystem.first(), - payload = validationPayload, - validationFailureTransformerCustom = { status, actions -> - contributeValidationFailure( - reason = status.reason, - validationFlowActions = actions, - resourceManager = resourceManager, - onOpenCustomContribute = ::bonusClicked - ) - }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - _showNextProgress.value = false - - openConfirmScreen(it, customizationPayload) - } + validationExecutor.requireValid( + validationSystem = customizedValidationSystem.first(), + payload = validationPayload, + validationFailureTransformerCustom = { status, actions -> + contributeValidationFailure( + reason = status.reason, + validationFlowActions = actions, + resourceManager = resourceManager, + onOpenCustomContribute = ::bonusClicked + ) + }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + _showNextProgress.value = false + + openConfirmScreen(it, customizationPayload) } } @@ -332,7 +328,7 @@ class CrowdloanContributeViewModel( ) = launch { val confirmContributePayload = ConfirmContributePayload( paraId = payload.paraId, - fee = validationPayload.fee, + fee = mapFeeToParcel(validationPayload.fee), amount = validationPayload.contributionAmount, estimatedRewardDisplay = estimatedRewardFlow.first(), bonusPayload = router.latestCustomBonus, diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/main/CrowdloanViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/main/CrowdloanViewModel.kt index b952165a16..8b5126fdbf 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/main/CrowdloanViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/main/CrowdloanViewModel.kt @@ -201,7 +201,7 @@ class CrowdloanViewModel( is MainCrowdloanValidationFailure.NoRelaychainAccount -> handleChainAccountNotFound( failure = failure, resourceManager = resourceManager, - goToWalletDetails = { router.openAccountDetails(failure.account.id) }, + goToWalletDetails = { router.openWalletDetails(failure.account.id) }, addAccountDescriptionRes = R.string.crowdloan_missing_account_message ) } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/sheets/AcknowledgePhishingBottomSheet.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/sheets/AcknowledgePhishingBottomSheet.kt index 84b5f403d2..eb630c6889 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/sheets/AcknowledgePhishingBottomSheet.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/browser/main/sheets/AcknowledgePhishingBottomSheet.kt @@ -17,8 +17,8 @@ class AcknowledgePhishingBottomSheet( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - title.setText(R.string.dapp_phishing_title) - subtitle.setText(R.string.dapp_phishing_subtitle) + titleView.setText(R.string.dapp_phishing_title) + subtitleView.setText(R.string.dapp_phishing_subtitle) applySolidIconStyle(R.drawable.ic_warning_filled) } diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt index 0c4109c18f..8a04d5fe09 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_external_sign_impl.data.evmApi +import io.novafoundation.nova.common.utils.toEcdsaSignatureData import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProvider import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource @@ -7,7 +8,6 @@ import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.Ev import io.novafoundation.nova.runtime.ethereum.sendSuspend import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.findEvmCallApi -import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper import jp.co.soramitsu.fearless_utils.extensions.toHexString import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer @@ -166,7 +166,7 @@ private class Web3JEvmApi( ): String { val encodedTx = TransactionEncoder.encode(transaction, ethereumChainId) val signerPayload = SignerPayloadRaw(encodedTx, accountId) - val signatureData = signer.signRaw(signerPayload).toSignatureData() + val signatureData = signer.signRaw(signerPayload).toEcdsaSignatureData() val eip155SignatureData: SignatureData = TransactionEncoder.createEip155SignatureData(signatureData, ethereumChainId) @@ -195,12 +195,6 @@ private class Web3JEvmApi( return web3.ethEstimateGas(tx).sendSuspend().amountUsed } - private fun SignatureWrapper.toSignatureData(): SignatureData { - require(this is SignatureWrapper.Ecdsa) - - return SignatureData(v, r, s) - } - private fun RawTransaction.encodeWith(signatureData: SignatureData): ByteArray { val values = TransactionEncoder.asRlpValues(this, signatureData) val rlpList = RlpList(values) 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 cc6c9bea3a..b02246d59c 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 @@ -19,6 +19,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoade import io.novafoundation.nova.runtime.di.ExtrinsicSerialization import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.network.rpc.RpcCalls import okhttp3.OkHttpClient interface ExternalSignFeatureDependencies { @@ -61,4 +62,6 @@ interface ExternalSignFeatureDependencies { val signerProvider: SignerProvider val gasPriceProviderFactory: GasPriceProviderFactory + + val rpcCalls: RpcCalls } diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/BaseExternalSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/BaseExternalSignInteractor.kt index 2934dbe26d..2dd09eb242 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/BaseExternalSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/BaseExternalSignInteractor.kt @@ -4,7 +4,7 @@ import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignWallet -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner abstract class BaseExternalSignInteractor( private val accountRepository: AccountRepository, @@ -12,10 +12,10 @@ abstract class BaseExternalSignInteractor( private val signerProvider: SignerProvider, ) : ExternalSignInteractor { - protected suspend fun resolveWalletSigner(): Signer { + protected suspend fun resolveWalletSigner(): NovaSigner { val metaAccount = resolveMetaAccount() - return signerProvider.signerFor(metaAccount) + return signerProvider.rootSignerFor(metaAccount) } protected suspend fun resolveMetaAccount(): MetaAccount { diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt index 91cda664a0..f4090fcf22 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt @@ -13,10 +13,10 @@ import io.novafoundation.nova.common.utils.lazyAsync import io.novafoundation.nova.common.utils.parseArbitraryObject import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi import io.novafoundation.nova.feature_account_api.data.model.EvmFee import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.zero import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel @@ -63,6 +63,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext import org.web3j.crypto.RawTransaction import org.web3j.crypto.TransactionDecoder +import java.math.BigInteger class EvmSignInteractorFactory( private val chainRegistry: ChainRegistry, @@ -118,7 +119,7 @@ class EvmSignInteractor( if (payload is ConfirmTx) { checkForSimpleFeeChanges( calculateFee = { calculateFee() }, - currentFee = { it.decimalFee!!.decimalAmount }, + currentFee = { it.decimalFee }, chainAsset = { it.token!!.configuration }, error = ConfirmDAppOperationValidationFailure::FeeSpikeDetected ) @@ -159,9 +160,11 @@ class EvmSignInteractor( } override suspend fun calculateFee(): Fee = withContext(Dispatchers.Default) { - if (payload !is ConfirmTx) return@withContext Fee.zero() + if (payload !is ConfirmTx) return@withContext zeroFee() - val api = ethereumApi() ?: return@withContext Fee.zero() + resolveWalletSigner() + + val api = ethereumApi() ?: return@withContext zeroFee() val tx = api.formTransaction(payload.transaction, feeOverride = null) mostRecentFormedTx.emit(tx) @@ -195,6 +198,10 @@ class EvmSignInteractor( ethereumApi()?.shutdown() } + private fun zeroFee(): Fee { + return EvmFee(gasLimit = BigInteger.ZERO, gasPrice = BigInteger.ZERO, submissionOrigin()) + } + private suspend fun confirmTx(basedOn: EvmTransaction, upToDateFee: Fee?, evmChainId: Long, action: ConfirmTx.Action): ExternalSignCommunicator.Response { val api = requireNotNull(ethereumApi()) @@ -314,7 +321,9 @@ class EvmSignInteractor( ) } - private fun RawTransaction.fee(): Fee = EvmFee(gasLimit = gasLimit, gasPrice = gasPrice) + private fun RawTransaction.fee(): Fee = EvmFee(gasLimit = gasLimit, gasPrice = gasPrice, submissionOrigin = submissionOrigin()) + + private fun submissionOrigin() = SubmissionOrigin.singleOrigin(originAccountId()) private fun originAccountId() = payload.originAddress.asEthereumAddress().toAccountId().value 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 4b50e5ee7c..d7d87c6316 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 @@ -28,6 +28,8 @@ 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.signer.FeeSigner +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getChainOrNull @@ -39,6 +41,7 @@ import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.EraType import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.Nonce import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadRaw import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.fromHex import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.fromUtf8 @@ -144,7 +147,8 @@ class PolkadotExternalSignInteractor( val chain = signPayload.chainOrNull() ?: return@withContext null - val extrinsicBuilder = signPayload.toExtrinsicBuilderWithoutCall(forFee = true) + val signer = signPayload.feeSigner() + val extrinsicBuilder = signPayload.toExtrinsicBuilderWithoutCall(signer) val runtime = chainRegistry.getRuntime(chain.id) val extrinsic = when (val callRepresentation = signPayload.callRepresentation(runtime)) { @@ -152,7 +156,7 @@ class PolkadotExternalSignInteractor( is CallRepresentation.Bytes -> extrinsicBuilder.build(rawCallBytes = callRepresentation.bytes) } - extrinsicService.estimateFee(chain, extrinsic) + extrinsicService.estimateFee(chain, extrinsic, signer) } private fun readableBytesContent(signBytesPayload: PolkadotSignPayload.Raw): String { @@ -182,7 +186,8 @@ class PolkadotExternalSignInteractor( private suspend fun signExtrinsic(extrinsicPayload: PolkadotSignPayload.Json): String { val runtime = chainRegistry.getRuntime(extrinsicPayload.chain().id) - val extrinsicBuilder = extrinsicPayload.toExtrinsicBuilderWithoutCall(forFee = false) + val signer = resolveWalletSigner() + val extrinsicBuilder = extrinsicPayload.toExtrinsicBuilderWithoutCall(signer) return when (val callRepresentation = extrinsicPayload.callRepresentation(runtime)) { is CallRepresentation.Instance -> extrinsicBuilder.call(callRepresentation.call).buildSignature() @@ -190,25 +195,23 @@ class PolkadotExternalSignInteractor( } } - private suspend fun PolkadotSignPayload.Json.toExtrinsicBuilderWithoutCall( - forFee: Boolean - ): ExtrinsicBuilder { + private suspend fun PolkadotSignPayload.Json.feeSigner(): FeeSigner { + val chain = chain() + + return signerProvider.feeSigner(resolveMetaAccount(), chain) + } + + private suspend fun PolkadotSignPayload.Json.toExtrinsicBuilderWithoutCall(signer: NovaSigner): ExtrinsicBuilder { val chain = chain() val runtime = chainRegistry.getRuntime(genesisHash) val parsedExtrinsic = parseDAppExtrinsic(runtime, this) val accountId = chain.accountIdOf(address) - val signer = if (forFee) { - signerProvider.feeSigner(chain) - } else { - resolveWalletSigner() - } - return with(parsedExtrinsic) { ExtrinsicBuilder( runtime = runtime, - nonce = nonce, + nonce = Nonce.singleTx(nonce), runtimeVersion = RuntimeVersion( specVersion = specVersion, transactionVersion = transactionVersion diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt index ab4c35b2ef..9b678e0d37 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt @@ -112,7 +112,7 @@ class ExternaSignViewModel( autoFixPayload = ::autoFixPayload, progressConsumer = _performingOperationInProgress.progressConsumer() ) { - performOperation(it.decimalFee?.fee) + performOperation(it.decimalFee?.networkFee) } } @@ -127,7 +127,7 @@ class ExternaSignViewModel( } private fun maybeLoadFee() { - originFeeMixin?.loadFeeV2( + originFeeMixin?.loadFee( coroutineScope = this, feeConstructor = { interactor.calculateFee() }, onRetryCancelled = {} diff --git a/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountInteractor.kt b/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountInteractor.kt index 43fd41feba..d8d4b1dacf 100644 --- a/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountInteractor.kt +++ b/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountInteractor.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.create.chooseAmount import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus @@ -21,7 +22,7 @@ interface NewDelegationChooseAmountInteractor { delegate: AccountId, tracks: Collection, shouldRemoveOtherTracks: Boolean, - ): Balance + ): Fee suspend fun delegate( amount: Balance, diff --git a/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/removeVotes/RemoveTrackVotesInteractor.kt b/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/removeVotes/RemoveTrackVotesInteractor.kt index fdc9dec661..40c32e01a0 100644 --- a/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/removeVotes/RemoveTrackVotesInteractor.kt +++ b/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/delegation/delegation/removeVotes/RemoveTrackVotesInteractor.kt @@ -1,12 +1,12 @@ package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.removeVotes +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus interface RemoveTrackVotesInteractor { - suspend fun calculateFee(trackIds: Collection): Balance + suspend fun calculateFee(trackIds: Collection): Fee suspend fun removeTrackVotes(trackIds: Collection): Result } diff --git a/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/referendum/vote/VoteReferendumInteractor.kt b/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/referendum/vote/VoteReferendumInteractor.kt index 47cbc3b2ff..1012f78863 100644 --- a/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/referendum/vote/VoteReferendumInteractor.kt +++ b/feature-governance-api/src/main/java/io/novafoundation/nova/feature_governance_api/domain/referendum/vote/VoteReferendumInteractor.kt @@ -1,5 +1,7 @@ package io.novafoundation.nova.feature_governance_api.domain.referendum.vote +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -14,10 +16,10 @@ interface VoteReferendumInteractor { scope: CoroutineScope ): Flow - suspend fun estimateFee(amount: Balance, conviction: Conviction, referendumId: ReferendumId): Balance + suspend fun estimateFee(amount: Balance, conviction: Conviction, referendumId: ReferendumId): Fee suspend fun vote( vote: AccountVote.Standard, referendumId: ReferendumId, - ): Result + ): Result } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/network/blockchain/extrinsic/ExtrinsicBuilderExt.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/network/blockchain/extrinsic/ExtrinsicBuilderExt.kt index 993de8bc59..b53f3effef 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/network/blockchain/extrinsic/ExtrinsicBuilderExt.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/network/blockchain/extrinsic/ExtrinsicBuilderExt.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_governance_impl.data.network.blockchain.extrinsic import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.argumentType +import io.novafoundation.nova.common.utils.democracy import io.novafoundation.nova.common.utils.structOf import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId @@ -8,10 +10,12 @@ import io.novafoundation.nova.feature_governance_api.data.network.blockhain.mode import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction +import io.novafoundation.nova.runtime.util.constructAccountLookupInstance import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum import jp.co.soramitsu.fearless_utils.runtime.definitions.types.instances.AddressInstanceConstructor import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import jp.co.soramitsu.fearless_utils.runtime.metadata.call fun ExtrinsicBuilder.convictionVotingVote( referendumId: ReferendumId, @@ -70,11 +74,13 @@ fun ExtrinsicBuilder.democracyVote( } fun ExtrinsicBuilder.democracyUnlock(accountId: AccountId): ExtrinsicBuilder { + val accountLookupType = runtime.metadata.democracy().call("unlock").argumentType("target") + return call( moduleName = Modules.DEMOCRACY, callName = "unlock", arguments = mapOf( - "target" to AddressInstanceConstructor.constructInstance(runtime.typeRegistry, accountId) + "target" to accountLookupType.constructAccountLookupInstance(accountId) ) ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt index 2ba1c51886..c123ba100f 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt @@ -2,7 +2,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegat import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId @@ -54,11 +56,11 @@ class RealNewDelegationChooseAmountInteractor( delegate: AccountId, tracks: Collection, shouldRemoveOtherTracks: Boolean, - ): Balance { + ): Fee { val (chain, governanceSource) = useSelectedGovernance() val origin = accountRepository.requireIdOfSelectedMetaAccountIn(chain) - return extrinsicService.estimateMultiFee(chain) { + return extrinsicService.estimateMultiFee(chain, TransactionOrigin.SelectedWallet) { delegate(governanceSource, amount, conviction, delegate, origin, chain, tracks, shouldRemoveOtherTracks) } } @@ -72,8 +74,17 @@ class RealNewDelegationChooseAmountInteractor( ): RetriableMultiResult { val (chain, governanceSource) = useSelectedGovernance() - return extrinsicService.submitMultiExtrinsicWithSelectedWalletAwaitingInclusion(chain) { origin -> - delegate(governanceSource, amount, conviction, delegate, origin, chain, tracks, shouldRemoveOtherTracks) + return extrinsicService.submitMultiExtrinsicAwaitingInclusion(chain, TransactionOrigin.SelectedWallet) { origin -> + delegate( + governanceSource = governanceSource, + amount = amount, + conviction = conviction, + delegate = delegate, + user = origin.requestedOrigin, + chain = chain, + tracks = tracks, + shouldRemoveOtherTracks = shouldRemoveOtherTracks + ) } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt index 2a25942a3d..cc1b5200e5 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt @@ -1,11 +1,12 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.create.chooseAmount.validation import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import jp.co.soramitsu.fearless_utils.runtime.AccountId import java.math.BigDecimal class ChooseDelegationAmountValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, val amount: BigDecimal, val delegate: AccountId diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationSystem.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationSystem.kt index e8c9445549..ee76c93548 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationSystem.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationSystem.kt @@ -20,11 +20,11 @@ fun ValidationSystem.Companion.chooseDelegationAmount( sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> ChooseDelegationAmountValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt index d6411b2b63..e91be3669d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt @@ -1,7 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.removeVotes +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId @@ -10,7 +12,6 @@ import io.novafoundation.nova.feature_governance_api.data.source.GovernanceSourc import io.novafoundation.nova.feature_governance_api.data.source.GovernanceSourceRegistry import io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.removeVotes.RemoveTrackVotesInteractor import io.novafoundation.nova.feature_governance_impl.data.GovernanceSharedState -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId @@ -27,11 +28,11 @@ class RealRemoveTrackVotesInteractor( private val accountRepository: AccountRepository, ) : RemoveTrackVotesInteractor { - override suspend fun calculateFee(trackIds: Collection): Balance = withContext(Dispatchers.IO) { + override suspend fun calculateFee(trackIds: Collection): Fee = withContext(Dispatchers.IO) { val (chain, governance) = useSelectedGovernance() val accountId = accountRepository.requireIdOfSelectedMetaAccountIn(chain) - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { governance.removeVotes(trackIds, extrinsicBuilder = this, chain.id, accountId) } } @@ -39,18 +40,18 @@ class RealRemoveTrackVotesInteractor( override suspend fun removeTrackVotes(trackIds: Collection): Result = withContext(Dispatchers.IO) { val (chain, governance) = useSelectedGovernance() - extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(chain) { origin -> - governance.removeVotes(trackIds, extrinsicBuilder = this, chain.id, origin) - } + extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { origin -> + governance.removeVotes(trackIds, extrinsicBuilder = this, chain.id, accountIdToRemoveVotes = origin.requestedOrigin) + }.awaitInBlock() } private suspend fun GovernanceSource.removeVotes( trackIds: Collection, extrinsicBuilder: ExtrinsicBuilder, chainId: ChainId, - accountId: AccountId + accountIdToRemoveVotes: AccountId ) { - val votings = convictionVoting.votingFor(accountId, chainId, trackIds) + val votings = convictionVoting.votingFor(accountIdToRemoveVotes, chainId, trackIds) votings.entries.onEach { (trackId, voting) -> voting.votedReferenda().onEach { referendumId -> diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt index c042d58037..57e0eb1ed6 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.removeVotes.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class RemoveVotesValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationSystem.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationSystem.kt index 3fba0b1eeb..076c4245dc 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationSystem.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationSystem.kt @@ -9,11 +9,11 @@ fun ValidationSystem.Companion.removeVotesValidationSystem(): RemoteVotesValidat sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> RemoveVotesValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/RevokeDelegationsInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/RevokeDelegationsInteractor.kt index 80c72e72bd..ffb0b49812 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/RevokeDelegationsInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/RevokeDelegationsInteractor.kt @@ -3,7 +3,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegat import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId @@ -16,7 +18,6 @@ import io.novafoundation.nova.feature_governance_api.domain.track.matchWith import io.novafoundation.nova.feature_governance_impl.data.GovernanceSharedState import io.novafoundation.nova.feature_governance_impl.domain.track.TracksUseCase import io.novafoundation.nova.feature_governance_impl.domain.track.tracksByIdOf -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -27,7 +28,7 @@ import kotlinx.coroutines.flow.Flow interface RevokeDelegationsInteractor { - suspend fun calculateFee(trackIds: Collection): Balance + suspend fun calculateFee(trackIds: Collection): Fee suspend fun revokeDelegations(trackIds: Collection): RetriableMultiResult @@ -43,10 +44,10 @@ class RealRevokeDelegationsInteractor( private val tracksUseCase: TracksUseCase, ) : RevokeDelegationsInteractor { - override suspend fun calculateFee(trackIds: Collection): Balance { + override suspend fun calculateFee(trackIds: Collection): Fee { val (chain, source) = useSelectedGovernance() - return extrinsicService.estimateMultiFee(chain) { + return extrinsicService.estimateMultiFee(chain, TransactionOrigin.SelectedWallet) { revokeDelegations(source, trackIds) } } @@ -54,7 +55,7 @@ class RealRevokeDelegationsInteractor( override suspend fun revokeDelegations(trackIds: Collection): RetriableMultiResult { val (chain, source) = useSelectedGovernance() - return extrinsicService.submitMultiExtrinsicWithSelectedWalletAwaitingInclusion(chain) { + return extrinsicService.submitMultiExtrinsicAwaitingInclusion(chain, TransactionOrigin.SelectedWallet) { revokeDelegations(source, trackIds) } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt index 75ab785ca5..aecd5446f5 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.revoke.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class RevokeDelegationValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationSystem.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationSystem.kt index 2e553725c4..285a4d789b 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationSystem.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationSystem.kt @@ -9,11 +9,11 @@ fun ValidationSystem.Companion.revokeDelegationValidationSystem(): RevokeDelegat sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> RevokeDelegationValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt index dca9f662de..29220dcf47 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt @@ -3,8 +3,10 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.unlock import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum @@ -49,7 +51,7 @@ import kotlinx.coroutines.withContext interface GovernanceUnlockInteractor { - suspend fun calculateFee(claimable: UnlockChunk.Claimable?): Balance + suspend fun calculateFee(claimable: UnlockChunk.Claimable?): Fee suspend fun unlock(claimable: UnlockChunk.Claimable?): Result @@ -77,16 +79,16 @@ class RealGovernanceUnlockInteractor( private val extrinsicService: ExtrinsicService, ) : GovernanceUnlockInteractor { - override suspend fun calculateFee(claimable: UnlockChunk.Claimable?): Balance { - if (claimable == null) return Balance.ZERO - + override suspend fun calculateFee(claimable: UnlockChunk.Claimable?): Fee { val governanceSelectedOption = selectedAssetState.selectedOption() val chain = governanceSelectedOption.assetWithChain.chain + if (claimable == null) return extrinsicService.zeroFee(chain, TransactionOrigin.SelectedWallet) + val metaAccount = accountRepository.getSelectedMetaAccount() val origin = metaAccount.accountIdIn(chain)!! - return extrinsicService.estimateFee(chain) { + return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { executeUnlock(origin, governanceSelectedOption, claimable) } } @@ -95,22 +97,22 @@ class RealGovernanceUnlockInteractor( val governanceSelectedOption = selectedAssetState.selectedOption() val chain = governanceSelectedOption.assetWithChain.chain - extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(chain) { origin -> + extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { origin -> if (claimable == null) error("Nothing to claim") - executeUnlock(origin, governanceSelectedOption, claimable) - } + executeUnlock(accountIdToUnlock = origin.requestedOrigin, governanceSelectedOption, claimable) + }.awaitInBlock() } private suspend fun ExtrinsicBuilder.executeUnlock( - origin: AccountId, + accountIdToUnlock: AccountId, selectedGovernanceOption: SupportedGovernanceOption, claimable: UnlockChunk.Claimable ) { val governanceSource = governanceSourceRegistry.sourceFor(selectedGovernanceOption) with(governanceSource.convictionVoting) { - unlock(origin, claimable) + unlock(accountIdToUnlock, claimable) } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt index a956cb2da4..087dffe4a7 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.unlock.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class UnlockReferendumValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/VoteReferendumValidationSystem.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/VoteReferendumValidationSystem.kt index 8b1fd4d6c6..10dc1400fa 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/VoteReferendumValidationSystem.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/VoteReferendumValidationSystem.kt @@ -9,11 +9,11 @@ fun ValidationSystem.Companion.unlockReferendumValidationSystem(): UnlockReferen sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> UnlockGovernanceValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt index 30ce631b43..3a503bb9ed 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt @@ -3,7 +3,10 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.vote import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote @@ -68,14 +71,14 @@ class RealVoteReferendumInteractor( } } - override suspend fun estimateFee(amount: Balance, conviction: Conviction, referendumId: ReferendumId): Balance { + override suspend fun estimateFee(amount: Balance, conviction: Conviction, referendumId: ReferendumId): Fee { val governanceOption = selectedChainState.selectedOption() val chain = governanceOption.assetWithChain.chain val vote = AyeVote(amount, conviction) // vote direction does not influence fee estimation val governanceSource = governanceSourceRegistry.sourceFor(governanceOption) - return extrinsicService.estimateFee(chain) { + return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { with(governanceSource.convictionVoting) { vote(referendumId, vote) } @@ -85,11 +88,11 @@ class RealVoteReferendumInteractor( override suspend fun vote( vote: AccountVote.Standard, referendumId: ReferendumId, - ): Result { + ): Result { val governanceSelectedOption = selectedChainState.selectedOption() val governanceSource = governanceSourceRegistry.sourceFor(governanceSelectedOption) - return extrinsicService.submitExtrinsicWithSelectedWallet(governanceSelectedOption.assetWithChain.chain) { + return extrinsicService.submitExtrinsic(governanceSelectedOption.assetWithChain.chain, TransactionOrigin.SelectedWallet) { with(governanceSource.convictionVoting) { vote(referendumId, vote) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationPayload.kt index eba157e86a..b6e0e828fc 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationPayload.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.va import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class VoteReferendumValidationPayload( @@ -10,5 +11,5 @@ class VoteReferendumValidationPayload( val asset: Asset, val trackVoting: Voting?, val voteAmount: BigDecimal, - val fee: BigDecimal + val fee: DecimalFee ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationSystem.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationSystem.kt index 387654d640..f33fd17562 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationSystem.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/VoteReferendumValidationSystem.kt @@ -26,11 +26,11 @@ fun ValidationSystem.Companion.voteReferendumValidationSystem( sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> VoteReferendumValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/GovernanceRouter.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/GovernanceRouter.kt index bfc13acf45..5def937a83 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/GovernanceRouter.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/GovernanceRouter.kt @@ -45,7 +45,7 @@ interface GovernanceRouter : ReturnableRouter { fun finishUnlockFlow(shouldCloseLocksScreen: Boolean) - fun openAccountDetails(id: Long) + fun openWalletDetails(id: Long) fun openAddDelegation() diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegate/detail/main/DelegateDetailsViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegate/detail/main/DelegateDetailsViewModel.kt index 6cef15552e..7eddf9c460 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegate/detail/main/DelegateDetailsViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegate/detail/main/DelegateDetailsViewModel.kt @@ -270,7 +270,7 @@ class DelegateDetailsViewModel( is AddDelegationValidationFailure.NoChainAccountFailure -> handleChainAccountNotFound( failure = failure, resourceManager = resourceManager, - goToWalletDetails = { router.openAccountDetails(failure.account.id) }, + goToWalletDetails = { router.openWalletDetails(failure.account.id) }, addAccountDescriptionRes = R.string.add_delegation_missing_account_message ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt index 4e7dae9a53..6c3cffde48 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt @@ -31,8 +31,10 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmountInput import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -156,10 +158,10 @@ class NewDelegationChooseAmountViewModel( private fun openConfirmIfValid() = launch { validationInProgressFlow.value = true - val fee = originFeeMixin.awaitFee() + val payload = ChooseDelegationAmountValidationPayload( asset = selectedAsset.first(), - fee = fee, + fee = originFeeMixin.awaitDecimalFee(), amount = amountChooserMixin.amount.first(), delegate = payload.delegate ) @@ -182,7 +184,7 @@ class NewDelegationChooseAmountViewModel( trackIdsRaw = payload.trackIdsRaw, amount = validationPayload.amount, conviction = selectedConvictionFlow.first(), - fee = validationPayload.fee, + fee = mapFeeToParcel(validationPayload.fee), isEditMode = payload.isEditMode ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmPayload.kt index 44061442cb..3d228b07a9 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmPayload.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_governance_impl.presentation.delegation.d import android.os.Parcelable import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId import io.novafoundation.nova.feature_governance_api.domain.referendum.voters.GenericVoter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.android.parcel.Parcelize @@ -15,7 +16,7 @@ class NewDelegationConfirmPayload( @Suppress("CanBeParameter") val trackIdsRaw: List, val amount: BigDecimal, val conviction: Conviction, - val fee: BigDecimal, + val fee: FeeParcelModel, val isEditMode: Boolean, ) : Parcelable { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt index 525ba5bc7c..37b3eb9e35 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt @@ -43,6 +43,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.chain import io.novafoundation.nova.runtime.state.chainAsset @@ -138,6 +139,8 @@ class NewDelegationConfirmViewModel( private val _showTracksEvent = MutableLiveData>>() val showTracksEvent: LiveData>> = _showTracksEvent + private val decimalFee = mapFeeFromParcel(payload.fee) + init { setFee() } @@ -166,7 +169,7 @@ class NewDelegationConfirmViewModel( val amountPlanks = asset.token.planksFromAmount(payload.amount) val validationPayload = ChooseDelegationAmountValidationPayload( asset = asset, - fee = payload.fee, + fee = decimalFee, amount = payload.amount, delegate = payload.delegate ) @@ -186,7 +189,7 @@ class NewDelegationConfirmViewModel( } private fun setFee() = launch { - originFeeMixin.setFee(payload.fee) + originFeeMixin.setFee(decimalFee.genericFee) } private fun performDelegate(amountPlanks: Balance) = launch { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt index 47c2afd542..61f3b5a42e 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt @@ -27,8 +27,8 @@ import io.novafoundation.nova.feature_governance_impl.presentation.track.formatT import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee import io.novafoundation.nova.runtime.state.chain import io.novafoundation.nova.runtime.state.chainAsset import kotlinx.coroutines.flow.Flow @@ -103,18 +103,21 @@ class RemoveVotesViewModel( externalActions.showExternalActions(type, governanceSharedState.chain()) } - private fun removeVotesIfValid(): Unit = originFeeMixin.requireFee(viewModel = this) { fee -> - launch { - val validationPayload = RemoveVotesValidationPayload(fee, assetFlow.first()) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = validationPayload, - progressConsumer = _showNextProgress.progressConsumer(), - validationFailureTransformer = { handleRemoveVotesValidationFailure(it, resourceManager) } - ) { - removeVotes() - } + private fun removeVotesIfValid() = launch { + _showNextProgress.value = true + + val validationPayload = RemoveVotesValidationPayload( + fee = originFeeMixin.awaitDecimalFee(), + asset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = validationPayload, + progressConsumer = _showNextProgress.progressConsumer(), + validationFailureTransformer = { handleRemoveVotesValidationFailure(it, resourceManager) } + ) { + removeVotes() } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt index b0ec2677ac..f63e39812e 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt @@ -35,6 +35,7 @@ import io.novafoundation.nova.feature_governance_impl.presentation.track.TrackFo import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.runtime.state.chain import io.novafoundation.nova.runtime.state.chainAsset @@ -146,10 +147,12 @@ class RevokeDelegationConfirmViewModel( } fun confirmClicked() = launch { - val asset = assetFlow.first() - val fee = originFeeMixin.awaitFee() + _showNextProgress.value = true - val validationPayload = RevokeDelegationValidationPayload(fee, asset) + val validationPayload = RevokeDelegationValidationPayload( + fee = originFeeMixin.awaitDecimalFee(), + asset = assetFlow.first() + ) validationExecutor.requireValid( validationSystem = validationSystem, diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/details/ReferendumDetailsViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/details/ReferendumDetailsViewModel.kt index ffcaa3e641..efbc8b0148 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/details/ReferendumDetailsViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/details/ReferendumDetailsViewModel.kt @@ -443,7 +443,7 @@ class ReferendumDetailsViewModel( is ReferendumPreVoteValidationFailure.NoRelaychainAccount -> handleChainAccountNotFound( failure = failure, resourceManager = resourceManager, - goToWalletDetails = { router.openAccountDetails(failure.account.id) }, + goToWalletDetails = { router.openWalletDetails(failure.account.id) }, addAccountDescriptionRes = R.string.referendum_missing_account_message ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt index 238e229819..70a9cc1b96 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt @@ -32,6 +32,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Vote import io.novafoundation.nova.runtime.state.chain @@ -120,6 +121,8 @@ class ConfirmReferendumVoteViewModel( } .shareInBackground() + private val decimalFee = mapFeeFromParcel(payload.fee) + init { setFee() } @@ -139,7 +142,7 @@ class ConfirmReferendumVoteViewModel( asset = assetFlow.first(), trackVoting = voteAssistant.trackVoting, voteAmount = payload.vote.amount, - fee = payload.fee + fee = decimalFee ) validationExecutor.requireValid( @@ -157,7 +160,7 @@ class ConfirmReferendumVoteViewModel( } private fun setFee() = launch { - originFeeMixin.setFee(payload.fee) + originFeeMixin.setFee(decimalFee.genericFee) } private fun performVote() = launch { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmVoteReferendumPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmVoteReferendumPayload.kt index 80b93958eb..b23fdac54d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmVoteReferendumPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmVoteReferendumPayload.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_governance_impl.presentation.referenda.vo import android.os.Parcelable import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @@ -10,7 +11,7 @@ import java.math.BigInteger @Parcelize class ConfirmVoteReferendumPayload( val _referendumId: BigInteger, - val fee: BigDecimal, + val fee: FeeParcelModel, val vote: AccountVoteParcelModel ) : Parcelable diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/SetupVoteReferendumViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/SetupVoteReferendumViewModel.kt index 05c65f15a9..6fbfc2ef27 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/SetupVoteReferendumViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/SetupVoteReferendumViewModel.kt @@ -33,8 +33,10 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmountInput import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -156,7 +158,6 @@ class SetupVoteReferendumViewModel( private fun openConfirmIfValid(voteType: VoteType) = launch { validatingVoteType.value = voteType - val fee = originFeeMixin.awaitFee() val voteAssistant = voteAssistantFlow.first() val payload = VoteReferendumValidationPayload( @@ -164,7 +165,7 @@ class SetupVoteReferendumViewModel( asset = selectedAsset.first(), trackVoting = voteAssistant.trackVoting, voteAmount = amountChooserMixin.amount.first(), - fee = fee + fee = originFeeMixin.awaitDecimalFee() ) validationExecutor.requireValid( @@ -184,7 +185,7 @@ class SetupVoteReferendumViewModel( val confirmPayload = ConfirmVoteReferendumPayload( _referendumId = payload._referendumId, - fee = validationPayload.fee, + fee = mapFeeToParcel(validationPayload.fee), vote = AccountVoteParcelModel( amount = validationPayload.voteAmount, conviction = conviction, diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt index 62db89d046..00de568851 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt @@ -30,9 +30,9 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.Flow @@ -135,24 +135,24 @@ class ConfirmGovernanceUnlockViewModel( router.back() } - fun confirmClicked() = originFeeMixin.requireFee(this) { fee -> - launch { - val claimable = unlockAffectsFlow.first().claimableChunk - val locksChange = unlockAffectsFlow.first().governanceLockChange - - val validationPayload = UnlockReferendumValidationPayload( - asset = assetFlow.first(), - fee = fee - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = validationPayload, - validationFailureTransformer = { handleUnlockReferendumValidationFailure(it, resourceManager) }, - progressConsumer = submissionInProgress.progressConsumer(), - ) { - executeUnlock(claimable, locksChange) - } + fun confirmClicked() = launch { + submissionInProgress.value = true + + val claimable = unlockAffectsFlow.first().claimableChunk + val locksChange = unlockAffectsFlow.first().governanceLockChange + + val validationPayload = UnlockReferendumValidationPayload( + asset = assetFlow.first(), + fee = originFeeMixin.awaitDecimalFee() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = validationPayload, + validationFailureTransformer = { handleUnlockReferendumValidationFailure(it, resourceManager) }, + progressConsumer = submissionInProgress.progressConsumer(), + ) { + executeUnlock(claimable, locksChange) } } diff --git a/feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerDerivationPath.kt b/feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerDerivationPath.kt new file mode 100644 index 0000000000..28c0fca935 --- /dev/null +++ b/feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerDerivationPath.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_ledger_api.data.repository + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +object LedgerDerivationPath { + + private const val LEDGER_DERIVATION_PATH_KEY = "LedgerChainAccount.derivationPath" + + fun derivationPathSecretKey(chainId: ChainId): String { + return "$LEDGER_DERIVATION_PATH_KEY.$chainId" + } +} diff --git a/feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerRepository.kt b/feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerRepository.kt new file mode 100644 index 0000000000..1fdfbd2c50 --- /dev/null +++ b/feature-ledger-api/src/main/java/io/novafoundation/nova/feature_ledger_api/data/repository/LedgerRepository.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.feature_ledger_api.data.repository + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +interface LedgerRepository { + + suspend fun getChainAccountDerivationPath( + metaId: Long, + chainId: ChainId + ): String +} diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/LedgerRepository.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/LedgerRepository.kt deleted file mode 100644 index 50bf0e2f3a..0000000000 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/LedgerRepository.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.novafoundation.nova.feature_ledger_impl.data.repository - -import io.novafoundation.nova.common.data.mappers.mapEncryptionToCryptoType -import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 -import io.novafoundation.nova.core_db.dao.MetaAccountDao -import io.novafoundation.nova.core_db.model.chain.ChainAccountLocal -import io.novafoundation.nova.core_db.model.chain.MetaAccountLocal -import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount -import io.novafoundation.nova.runtime.ext.accountIdOf -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId - -interface LedgerRepository { - - suspend fun insertLedgerMetaAccount( - name: String, - ledgerChainAccounts: Map - ): Long - - suspend fun getChainAccountDerivationPath( - metaId: Long, - chainId: ChainId - ): String - - suspend fun insertLedgerChainAccount( - metaId: Long, - chainId: ChainId, - ledgerChainAccount: LedgerSubstrateAccount - ) -} - -private const val LEDGER_DERIVATION_PATH_KEY = "LedgerChainAccount.derivationPath" - -class RealLedgerRepository( - private val metaAccountDao: MetaAccountDao, - private val chainRegistry: ChainRegistry, - private val secretStoreV2: SecretStoreV2, -) : LedgerRepository { - - override suspend fun insertLedgerMetaAccount( - name: String, - ledgerChainAccounts: Map - ): Long { - val metaAccount = MetaAccountLocal( - substratePublicKey = null, - substrateCryptoType = null, - substrateAccountId = null, - ethereumPublicKey = null, - ethereumAddress = null, - name = name, - isSelected = false, - position = metaAccountDao.nextAccountPosition(), - type = MetaAccountLocal.Type.LEDGER - ) - - val metaId = metaAccountDao.insertMetaAndChainAccounts(metaAccount) { metaId -> - ledgerChainAccounts.map { (chainId, account) -> - val chain = chainRegistry.getChain(chainId) - - ChainAccountLocal( - metaId = metaId, - chainId = chainId, - publicKey = account.publicKey, - accountId = chain.accountIdOf(account.publicKey), - cryptoType = mapEncryptionToCryptoType(account.encryptionType) - ) - } - } - - ledgerChainAccounts.onEach { (chainId, ledgerAccount) -> - val derivationPathKey = derivationPathSecretKey(chainId) - secretStoreV2.putAdditionalMetaAccountSecret(metaId, derivationPathKey, ledgerAccount.derivationPath) - } - - return metaId - } - - override suspend fun getChainAccountDerivationPath(metaId: Long, chainId: ChainId): String { - val key = derivationPathSecretKey(chainId) - - return secretStoreV2.getAdditionalMetaAccountSecret(metaId, key) - ?: throw IllegalStateException("Cannot find Ledger derivation path for chain $chainId in meta account $metaId") - } - - override suspend fun insertLedgerChainAccount(metaId: Long, chainId: ChainId, ledgerChainAccount: LedgerSubstrateAccount) { - val chain = chainRegistry.getChain(chainId) - - val chainAccount = ChainAccountLocal( - metaId = metaId, - chainId = chainId, - publicKey = ledgerChainAccount.publicKey, - accountId = chain.accountIdOf(ledgerChainAccount.publicKey), - cryptoType = mapEncryptionToCryptoType(ledgerChainAccount.encryptionType) - ) - - metaAccountDao.insertChainAccount(chainAccount) - val derivationPathKey = derivationPathSecretKey(chainId) - secretStoreV2.putAdditionalMetaAccountSecret(metaId, derivationPathKey, ledgerChainAccount.derivationPath) - } - - private fun derivationPathSecretKey(chainId: ChainId): String { - return "$LEDGER_DERIVATION_PATH_KEY.$chainId" - } -} diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/RealLedgerRepository.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/RealLedgerRepository.kt new file mode 100644 index 0000000000..62d6bfa763 --- /dev/null +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/data/repository/RealLedgerRepository.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_ledger_impl.data.repository + +import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 +import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerDerivationPath +import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerRepository +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +class RealLedgerRepository( + private val secretStoreV2: SecretStoreV2, +) : LedgerRepository { + + override suspend fun getChainAccountDerivationPath(metaId: Long, chainId: ChainId): String { + val key = LedgerDerivationPath.derivationPathSecretKey(chainId) + + return secretStoreV2.getAdditionalMetaAccountSecret(metaId, key) + ?: throw IllegalStateException("Cannot find Ledger derivation path for chain $chainId in meta account $metaId") + } +} diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureDependencies.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureDependencies.kt index 5f230daf04..09f9f867b4 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureDependencies.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureDependencies.kt @@ -7,11 +7,12 @@ import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.resources.ContextManager import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.MutableSharedState import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager import io.novafoundation.nova.common.utils.location.LocationManager import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory import io.novafoundation.nova.core_db.dao.MetaAccountDao +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase @@ -19,7 +20,6 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.A import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic interface LedgerFeatureDependencies { @@ -55,9 +55,11 @@ interface LedgerFeatureDependencies { val secretStoreV2: SecretStoreV2 - val signSharedState: MutableSharedState + val signSharedState: SigningSharedState val extrinsicValidityUseCase: ExtrinsicValidityUseCase val selectedAccountUseCase: SelectedAccountUseCase + + val ledgerAddAccountRepository: LedgerAddAccountRepository } diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureModule.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureModule.kt index a360a61628..dde473b4dd 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureModule.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/di/LedgerFeatureModule.kt @@ -6,11 +6,10 @@ import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2 import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ContextManager import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager -import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateLedgerApplication import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService import io.novafoundation.nova.feature_ledger_api.sdk.transport.LedgerTransport -import io.novafoundation.nova.feature_ledger_impl.data.repository.LedgerRepository +import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerRepository import io.novafoundation.nova.feature_ledger_impl.data.repository.RealLedgerRepository import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.RealSelectAddressLedgerInteractor import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor @@ -21,7 +20,6 @@ import io.novafoundation.nova.feature_ledger_impl.sdk.connection.ble.LedgerBleMa import io.novafoundation.nova.feature_ledger_impl.sdk.discovery.ble.BleLedgerDeviceDiscoveryService import io.novafoundation.nova.feature_ledger_impl.sdk.transport.ChunkedLedgerTransport import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module class LedgerFeatureModule { @@ -56,10 +54,8 @@ class LedgerFeatureModule { @Provides @FeatureScope fun provideRepository( - metaAccountDao: MetaAccountDao, - chainRegistry: ChainRegistry, secretStoreV2: SecretStoreV2 - ): LedgerRepository = RealLedgerRepository(metaAccountDao, chainRegistry, secretStoreV2) + ): LedgerRepository = RealLedgerRepository(secretStoreV2) @Provides fun provideLedgerMessagePresentable(): LedgerMessagePresentable = SingleSheetLedgerMessagePresentable() diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/addChain/AddLedgerChainAccountInteractor.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/addChain/AddLedgerChainAccountInteractor.kt index 30e8df63dd..bb3a5feea7 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/addChain/AddLedgerChainAccountInteractor.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/addChain/AddLedgerChainAccountInteractor.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_ledger_impl.domain.account.addChain +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount -import io.novafoundation.nova.feature_ledger_impl.data.repository.LedgerRepository interface AddLedgerChainAccountInteractor { @@ -9,10 +9,16 @@ interface AddLedgerChainAccountInteractor { } class RealAddLedgerChainAccountInteractor( - private val ledgerRepository: LedgerRepository, + private val ledgerAddAccountRepository: LedgerAddAccountRepository ) : AddLedgerChainAccountInteractor { override suspend fun addChainAccount(metaId: Long, chainId: String, account: LedgerSubstrateAccount): Result = kotlin.runCatching { - ledgerRepository.insertLedgerChainAccount(metaId, chainId, account) + ledgerAddAccountRepository.addAccount( + LedgerAddAccountRepository.Payload.ChainAccount( + metaId, + chainId, + account + ) + ) } } diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/connect/finish/FinishImportLedgerInteractor.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/connect/finish/FinishImportLedgerInteractor.kt index 075929f155..4b84e0c793 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/connect/finish/FinishImportLedgerInteractor.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/connect/finish/FinishImportLedgerInteractor.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_ledger_impl.domain.account.connect.finish +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount -import io.novafoundation.nova.feature_ledger_impl.data.repository.LedgerRepository import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId interface FinishImportLedgerInteractor { @@ -14,12 +14,17 @@ interface FinishImportLedgerInteractor { } class RealFinishImportLedgerInteractor( - private val repository: LedgerRepository, + private val ledgerAddAccountRepository: LedgerAddAccountRepository, private val accountRepository: AccountRepository, ) : FinishImportLedgerInteractor { override suspend fun createWallet(name: String, ledgerChainAccounts: Map) = runCatching { - val metaId = repository.insertLedgerMetaAccount(name, ledgerChainAccounts) + val metaId = ledgerAddAccountRepository.addAccount( + LedgerAddAccountRepository.Payload.MetaAccount( + name, + ledgerChainAccounts + ) + ) accountRepository.selectMetaAccount(metaId) } diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/sign/SignLedgerInteractor.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/sign/SignLedgerInteractor.kt index 8cf77b62d6..555a3ddc42 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/sign/SignLedgerInteractor.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/domain/account/sign/SignLedgerInteractor.kt @@ -1,32 +1,36 @@ package io.novafoundation.nova.feature_ledger_impl.domain.account.sign import io.novafoundation.nova.common.utils.chainId -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState import io.novafoundation.nova.feature_account_api.domain.model.publicKeyIn import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.fearless_utils.encrypt.SignatureVerifier import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper import jp.co.soramitsu.fearless_utils.encrypt.Signer.MessageHashing -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.encodedSignaturePayload interface SignLedgerInteractor { - suspend fun verifySignature(payload: SignerPayloadExtrinsic, signature: SignatureWrapper): Boolean + suspend fun verifySignature( + payload: SeparateFlowSignerState, + signature: SignatureWrapper + ): Boolean } class RealSignLedgerInteractor( - private val metaAccountRepository: AccountRepository, private val chainRegistry: ChainRegistry, ) : SignLedgerInteractor { - override suspend fun verifySignature(payload: SignerPayloadExtrinsic, signature: SignatureWrapper): Boolean = runCatching { - val payloadBytes = payload.encodedSignaturePayload(hashBigPayloads = true) - val metaAccount = metaAccountRepository.getSelectedMetaAccount() - val chainId = payload.chainId + override suspend fun verifySignature( + payload: SeparateFlowSignerState, + signature: SignatureWrapper + ): Boolean = runCatching { + val extrinsic = payload.extrinsic + val payloadBytes = extrinsic.encodedSignaturePayload(hashBigPayloads = true) + val chainId = extrinsic.chainId val chain = chainRegistry.getChain(chainId) - val publicKey = metaAccount.publicKeyIn(chain) ?: throw IllegalStateException("No public key for chain $chainId") + val publicKey = payload.metaAccount.publicKeyIn(chain) ?: throw IllegalStateException("No public key for chain $chainId") SignatureVerifier.verify(signature, MessageHashing.SUBSTRATE, payloadBytes, publicKey) }.getOrDefault(false) diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/addChain/selectAddress/di/AddLedgerChainAccountSelectAddressModule.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/addChain/selectAddress/di/AddLedgerChainAccountSelectAddressModule.kt index 85a17e98d8..1c9bae9d2f 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/addChain/selectAddress/di/AddLedgerChainAccountSelectAddressModule.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/addChain/selectAddress/di/AddLedgerChainAccountSelectAddressModule.kt @@ -11,7 +11,7 @@ import io.novafoundation.nova.common.di.scope.ScreenScope 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.feature_ledger_impl.data.repository.LedgerRepository +import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.AddLedgerChainAccountInteractor import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.RealAddLedgerChainAccountInteractor import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor @@ -27,8 +27,8 @@ class AddLedgerChainAccountSelectAddressModule { @Provides @ScreenScope fun provideInteractor( - repository: LedgerRepository - ): AddLedgerChainAccountInteractor = RealAddLedgerChainAccountInteractor(repository) + ledgerAddAccountRepository: LedgerAddAccountRepository + ): AddLedgerChainAccountInteractor = RealAddLedgerChainAccountInteractor(ledgerAddAccountRepository) @Provides @ScreenScope diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerFragment.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerFragment.kt index 934835eac1..ebaba4cb84 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerFragment.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerFragment.kt @@ -6,10 +6,12 @@ import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.recyclerview.widget.ConcatAdapter +import coil.ImageLoader import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.utils.applyStatusBarInsets -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountsAdapter +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountHolder import io.novafoundation.nova.feature_ledger_impl.R import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessagePresentable import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.setupLedgerMessages @@ -20,7 +22,7 @@ import javax.inject.Inject abstract class SelectAddressLedgerFragment : BaseFragment(), - AccountsAdapter.AccountItemHandler, + AccountHolder.AccountItemHandler, LedgerSelectAddressLoadMoreAdapter.Handler { companion object { @@ -30,7 +32,17 @@ abstract class SelectAddressLedgerFragment : fun getBundle(payload: SelectLedgerAddressPayload) = bundleOf(PAYLOAD_KEY to payload) } - private val addressesAdapter = AccountsAdapter(this, AccountsAdapter.Mode.VIEW) + @Inject + protected lateinit var imageLoader: ImageLoader + + private val addressesAdapter by lazy(LazyThreadSafetyMode.NONE) { + AccountsAdapter( + this, + imageLoader, + chainBorderColor = R.color.secondary_screen_background, + AccountHolder.Mode.SELECT + ) + } private val loadMoreAdapter = LedgerSelectAddressLoadMoreAdapter(handler = this, lifecycleOwner = this) @Inject diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerViewModel.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerViewModel.kt index 8f940f1a9c..687fa01765 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerViewModel.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/common/selectAddress/SelectAddressLedgerViewModel.kt @@ -15,7 +15,7 @@ import io.novafoundation.nova.common.utils.mapList import io.novafoundation.nova.common.utils.withFlagSet import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel -import io.novafoundation.nova.feature_account_api.presenatation.account.listing.AccountUi +import io.novafoundation.nova.feature_account_api.presenatation.account.listing.items.AccountUi import io.novafoundation.nova.feature_ledger_impl.R import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.LedgerAccountWithBalance import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor @@ -157,7 +157,10 @@ abstract class SelectAddressLedgerViewModel( isSelected = false, isClickable = true, picture = addressModel.image, - subtitleIconRes = null + chainIconUrl = null, + updateIndicator = false, + subtitleIconRes = null, + isEditable = false ) } } diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/connect/finish/di/FinishImportLedgerModule.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/connect/finish/di/FinishImportLedgerModule.kt index 228a7052c7..8248dc1c61 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/connect/finish/di/FinishImportLedgerModule.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/connect/finish/di/FinishImportLedgerModule.kt @@ -10,9 +10,9 @@ import io.novafoundation.nova.common.di.scope.ScreenScope 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.feature_account_api.data.repository.addAccount.ledger.LedgerAddAccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_ledger_impl.data.repository.LedgerRepository import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.finish.FinishImportLedgerInteractor import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.finish.RealFinishImportLedgerInteractor import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter @@ -25,9 +25,9 @@ class FinishImportLedgerModule { @Provides @ScreenScope fun provideInteractor( - ledgerRepository: LedgerRepository, + ledgerAddAccountRepository: LedgerAddAccountRepository, accountRepository: AccountRepository - ): FinishImportLedgerInteractor = RealFinishImportLedgerInteractor(ledgerRepository, accountRepository) + ): FinishImportLedgerInteractor = RealFinishImportLedgerInteractor(ledgerAddAccountRepository, accountRepository) @Provides @IntoMap diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/SignLedgerViewModel.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/SignLedgerViewModel.kt index 49a4a83439..5ce750c37a 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/SignLedgerViewModel.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/SignLedgerViewModel.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_ledger_impl.presentation.account.sign import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.SharedState import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager import io.novafoundation.nova.common.utils.event import io.novafoundation.nova.common.utils.flowOf @@ -9,7 +8,7 @@ import io.novafoundation.nova.common.utils.getOrThrow import io.novafoundation.nova.common.utils.invoke import io.novafoundation.nova.common.utils.location.LocationManager import io.novafoundation.nova.common.utils.permissions.PermissionsAsker -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenCommunicator import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenResponder import io.novafoundation.nova.feature_account_api.presenatation.sign.cancelled @@ -30,7 +29,6 @@ import io.novafoundation.nova.runtime.extrinsic.closeToExpire import io.novafoundation.nova.runtime.extrinsic.remainingTime import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.fearless_utils.encrypt.SignatureWrapper -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -51,9 +49,8 @@ class SignLedgerViewModel( private val selectLedgerPayload: SelectLedgerPayload, private val router: LedgerRouter, private val resourceManager: ResourceManager, - private val signPayloadState: SharedState, + private val signPayloadState: SigningSharedState, private val extrinsicValidityUseCase: ExtrinsicValidityUseCase, - private val selectedAccountUseCase: SelectedAccountUseCase, private val request: SignInterScreenCommunicator.Request, private val responder: SignInterScreenResponder, private val interactor: SignLedgerInteractor, @@ -74,7 +71,7 @@ class SignLedgerViewModel( ) { private val validityPeriod = flowOf { - extrinsicValidityUseCase.extrinsicValidityPeriod(signPayloadState.getOrThrow()) + extrinsicValidityUseCase.extrinsicValidityPeriod(signPayloadState.getOrThrow().extrinsic) }.shareInBackground() private var signingJob: Deferred? = null @@ -105,6 +102,7 @@ class SignLedgerViewModel( override suspend fun verifyConnection(device: LedgerDevice) { val validityPeriod = validityPeriod.first() + val signState = signPayloadState.getOrThrow() ledgerMessageCommands.value = LedgerMessageCommand.Show.Info( title = resourceManager.getString(R.string.ledger_review_approve_title), @@ -118,21 +116,21 @@ class SignLedgerViewModel( ) ).event() - val selectedMetaAccount = selectedAccountUseCase.getSelectedMetaAccount() + val signingMetaAccount = signState.metaAccount signingJob?.cancel() signingJob = async { substrateApplication.getSignature( device = device, - metaId = selectedMetaAccount.id, + metaId = signingMetaAccount.id, chainId = selectLedgerPayload.chainId, - payload = signPayloadState.getOrThrow() + payload = signState.extrinsic ) } val signature = signingJob!!.await() - if (interactor.verifySignature(signPayloadState.getOrThrow(), signature)) { + if (interactor.verifySignature(signState, signature)) { responder.respond(request.signed(signature)) hideBottomSheet() router.finishSignFlow() diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/di/SignLedgerModule.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/di/SignLedgerModule.kt index a8ae0fd1a2..b3f2e944f1 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/di/SignLedgerModule.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/presentation/account/sign/di/SignLedgerModule.kt @@ -10,15 +10,13 @@ import io.novafoundation.nova.common.di.scope.ScreenScope 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.utils.MutableSharedState import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager import io.novafoundation.nova.common.utils.chainId import io.novafoundation.nova.common.utils.getOrThrow import io.novafoundation.nova.common.utils.location.LocationManager import io.novafoundation.nova.common.utils.permissions.PermissionsAsker import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState import io.novafoundation.nova.feature_account_api.presenatation.sign.LedgerSignCommunicator import io.novafoundation.nova.feature_account_api.presenatation.sign.SignInterScreenCommunicator import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateLedgerApplication @@ -30,17 +28,13 @@ import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.se import io.novafoundation.nova.feature_ledger_impl.presentation.account.sign.SignLedgerViewModel import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.SignerPayloadExtrinsic @Module(includes = [ViewModelModule::class]) class SignLedgerModule { @Provides @ScreenScope - fun provideInteractor( - accountRepository: AccountRepository, - chainRegistry: ChainRegistry, - ): SignLedgerInteractor = RealSignLedgerInteractor(accountRepository, chainRegistry) + fun provideInteractor(chainRegistry: ChainRegistry): SignLedgerInteractor = RealSignLedgerInteractor(chainRegistry) @Provides @ScreenScope @@ -53,9 +47,9 @@ class SignLedgerModule { @Provides @ScreenScope fun provideSelectLedgerPayload( - signPayloadState: MutableSharedState, + signPayloadState: SigningSharedState, ): SelectLedgerPayload = SelectLedgerPayload( - chainId = signPayloadState.getOrThrow().chainId + chainId = signPayloadState.getOrThrow().extrinsic.chainId ) @Provides @@ -71,9 +65,8 @@ class SignLedgerModule { router: LedgerRouter, resourceManager: ResourceManager, chainRegistry: ChainRegistry, - signPayloadState: MutableSharedState, + signPayloadState: SigningSharedState, extrinsicValidityUseCase: ExtrinsicValidityUseCase, - selectedAccountUseCase: SelectedAccountUseCase, request: SignInterScreenCommunicator.Request, interactor: SignLedgerInteractor, responder: LedgerSignCommunicator, @@ -90,7 +83,6 @@ class SignLedgerModule { chainRegistry = chainRegistry, signPayloadState = signPayloadState, extrinsicValidityUseCase = extrinsicValidityUseCase, - selectedAccountUseCase = selectedAccountUseCase, request = request, responder = responder, interactor = interactor diff --git a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/sdk/application/substrate/RealSubstrateLedgerApplication.kt b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/sdk/application/substrate/RealSubstrateLedgerApplication.kt index 4e1df2ea14..b78f58c365 100644 --- a/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/sdk/application/substrate/RealSubstrateLedgerApplication.kt +++ b/feature-ledger-impl/src/main/java/io/novafoundation/nova/feature_ledger_impl/sdk/application/substrate/RealSubstrateLedgerApplication.kt @@ -13,7 +13,7 @@ import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.Subst import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice import io.novafoundation.nova.feature_ledger_api.sdk.transport.LedgerTransport import io.novafoundation.nova.feature_ledger_api.sdk.transport.send -import io.novafoundation.nova.feature_ledger_impl.data.repository.LedgerRepository +import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerRepository import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.DisplayVerificationDialog.NO import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.DisplayVerificationDialog.YES import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt index 7db9ad91e2..c382e92c9e 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.cast +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.data.network.runtime.binding.getTyped import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.uniques @@ -21,7 +22,9 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novafoundation.nova.runtime.storage.source.query.singleValueOf +import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder +import io.novafoundation.nova.runtime.storage.source.multi.singleValueOf +import io.novafoundation.nova.runtime.storage.source.query.multi import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct import jp.co.soramitsu.fearless_utils.runtime.metadata.storage @@ -49,29 +52,31 @@ class UniquesNftProvider( val classesIds = classesWithInstances.map { (collection, _) -> collection }.distinct() - val classMetadataStorage = runtime.metadata.uniques().storage("ClassMetadataOf") - val instanceMetadataStorage = runtime.metadata.uniques().storage("InstanceMetadataOf") - val classStorage = runtime.metadata.uniques().storage("Class") + val classMetadataDescriptor: MultiQueryBuilder.Descriptor + val totalIssuanceDescriptor: MultiQueryBuilder.Descriptor + val instanceMetadataDescriptor: MultiQueryBuilder.Descriptor, ByteArray?> val multiQueryResults = multi { - classStorage.querySingleArgKeys(classesIds) - classMetadataStorage.querySingleArgKeys(classesIds) - instanceMetadataStorage.queryKeys(classesWithInstances) + classMetadataDescriptor = runtime.metadata.uniques().storage("ClassMetadataOf").querySingleArgKeys( + keysArgs = classesIds, + keyExtractor = { (classId: BigInteger) -> classId }, + binding = ::bindMetadata + ) + instanceMetadataDescriptor = runtime.metadata.uniques().storage("InstanceMetadataOf").querySingleArgKeys( + keysArgs = classesIds, + keyExtractor = { (classId: BigInteger, instance: BigInteger) -> classId to instance }, + binding = ::bindMetadata + ) + totalIssuanceDescriptor = runtime.metadata.uniques().storage("Class").queryKeys( + keysArgs = classesWithInstances, + keyExtractor = { (classId: BigInteger) -> classId }, + binding = { bindNumber(it.castToStruct()["items"]) } + ) } - val classMetadatas = multiQueryResults.getValue(classMetadataStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() } - .mapValues { (_, parsedValue) -> bindMetadata(parsedValue) } - - val totalIssuances = multiQueryResults.getValue(classStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() } - .mapValues { (_, parsedValue) -> - bindNumber(parsedValue.cast()["items"]) - } - - val instancesMetadatas = multiQueryResults.getValue(instanceMetadataStorage) - .mapKeys { (keyComponents, _) -> keyComponents.component1() to keyComponents.component2() } - .mapValues { (_, parsedValue) -> bindMetadata(parsedValue) } + val classMetadatas = multiQueryResults[classMetadataDescriptor] + val totalIssuances = multiQueryResults[totalIssuanceDescriptor] + val instancesMetadatas = multiQueryResults[instanceMetadataDescriptor] classesWithInstances.map { (collectionId, instanceId) -> val instanceKey = collectionId to instanceId @@ -136,14 +141,15 @@ class UniquesNftProvider( val classId = nftLocal.collectionId.toBigInteger() remoteStorage.query(chain.id) { - val classMetadataStorage = runtime.metadata.uniques().storage("ClassMetadataOf") - val classStorage = runtime.metadata.uniques().storage("Class") + var classMetadataDescriptor: MultiQueryBuilder.Descriptor<*, ByteArray?> + var classDescriptor: MultiQueryBuilder.Descriptor<*, AccountId> val queryResults = multi { - classMetadataStorage.queryKey(classId) - classStorage.queryKey(classId) + classMetadataDescriptor = runtime.metadata.uniques().storage("ClassMetadataOf").queryKey(classId, binding = ::bindMetadata) + classDescriptor = runtime.metadata.uniques().storage("Class").queryKey(classId, binding = ::bindIssuer) } - val classMetadataPointer = bindMetadata(queryResults.singleValueOf(classMetadataStorage)) + + val classMetadataPointer = queryResults.singleValueOf(classMetadataDescriptor) val collection = if (classMetadataPointer == null) { NftDetails.Collection(nftLocal.collectionId) @@ -158,8 +164,7 @@ class UniquesNftProvider( ) } - val classIssuerRaw = queryResults.singleValueOf(classStorage) - val classIssuer = bindAccountId(classIssuerRaw.cast()["issuer"]) + val classIssuer = queryResults.singleValueOf(classDescriptor) NftDetails( identifier = nftLocal.identifier, @@ -177,6 +182,8 @@ class UniquesNftProvider( } } + private fun bindIssuer(dynamic: Any?): AccountId = bindAccountId(dynamic.castToStruct()["issuer"]) + private fun bindMetadata(dynamic: Any?): ByteArray? = dynamic?.cast()?.getTyped("data") private fun identifier(chainId: ChainId, collectionId: BigInteger, instanceId: BigInteger): String { diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/SettingsRouter.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/SettingsRouter.kt index 1783cc5a17..992c696f96 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/SettingsRouter.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/SettingsRouter.kt @@ -10,7 +10,7 @@ interface SettingsRouter { fun openChangePinCode() - fun openAccountDetails(metaId: Long) + fun openWalletDetails(metaId: Long) fun openSwitchWallet() diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt index 91200bea58..37561785af 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/presentation/settings/SettingsViewModel.kt @@ -206,7 +206,7 @@ class SettingsViewModel( fun accountActionsClicked() = launch { val selectedWalletId = selectedAccountUseCase.getSelectedMetaAccount().id - router.openAccountDetails(selectedWalletId) + router.openWalletDetails(selectedWalletId) } fun selectedWalletClicked() { diff --git a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt index 352058061b..bdc4e631e9 100644 --- a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt +++ b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt @@ -47,7 +47,7 @@ interface StakingRepository { fun stakingStoriesFlow(): Flow> - suspend fun ledgerFlow(stakingState: StakingState.Stash): Flow + fun ledgerFlow(stakingState: StakingState.Stash): Flow suspend fun ledger(chainId: ChainId, accountId: AccountId): StakingLedger? diff --git a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/Exposure.kt b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/Exposure.kt index 70d48bcfb5..adcfe00cd2 100644 --- a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/Exposure.kt +++ b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/Exposure.kt @@ -5,3 +5,12 @@ import java.math.BigInteger class IndividualExposure(val who: ByteArray, val value: BigInteger) class Exposure(val total: BigInteger, val own: BigInteger, val others: List) + +class ExposureOverview(val total: BigInteger, val own: BigInteger, val pageCount: BigInteger, val nominatorCount: BigInteger) + +class ExposurePage(val others: List) + +class PagedExposure( + val overview: ExposureOverview, + val pages: List +) diff --git a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/relaychain/StakingState.kt b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/relaychain/StakingState.kt index 5131513717..2a89071eb4 100644 --- a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/relaychain/StakingState.kt +++ b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/relaychain/StakingState.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_api.domain.model.relaychain +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_staking_api.domain.model.Nominations import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs import io.novafoundation.nova.runtime.ext.addressOf @@ -53,3 +54,7 @@ sealed class StakingState( ) : Stash(chain, chainAsset, accountId, controllerId, stashId) } } + +fun StakingState.Stash.stashTransactionOrigin(): TransactionOrigin = TransactionOrigin.WalletWithAccount(stashId) + +fun StakingState.Stash.controllerTransactionOrigin(): TransactionOrigin = TransactionOrigin.WalletWithAccount(controllerId) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/model/Payout.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/model/Payout.kt index ed1cba6945..91bc945e51 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/model/Payout.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/model/Payout.kt @@ -1,9 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.data.model +import io.novafoundation.nova.common.address.AccountIdKey import java.math.BigInteger class Payout( - val validatorAddress: String, + val validatorStash: AccountIdKey, val era: BigInteger, - val amount: BigInteger + val amount: BigInteger, + val pagesToClaim: List ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ClaimedRewardPages.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ClaimedRewardPages.kt new file mode 100644 index 0000000000..292d8c71af --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ClaimedRewardPages.kt @@ -0,0 +1,15 @@ +package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings + +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.castToList +import io.novafoundation.nova.common.utils.mapToSet + +typealias ClaimedRewardsPages = Set + +fun bindClaimedPages(decoded: Any?): Set { + val asList = decoded.castToList() + + return asList.mapToSet { item -> + bindNumber(item).toInt() + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/EraRewardPoints.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/EraRewardPoints.kt index 6f98799bc1..ba43d9b6bd 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/EraRewardPoints.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/EraRewardPoints.kt @@ -4,33 +4,27 @@ import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber -import io.novafoundation.nova.common.data.network.runtime.binding.cast +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.data.network.runtime.binding.getList import io.novafoundation.nova.common.data.network.runtime.binding.requireType import io.novafoundation.nova.common.utils.second import jp.co.soramitsu.fearless_utils.runtime.AccountId -import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.Type -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHexOrNull import java.math.BigInteger -typealias RewardPoint = BigInteger +typealias RewardPoints = BigInteger class EraRewardPoints( - val totalPoints: RewardPoint, + val totalPoints: RewardPoints, val individual: List ) { - class Individual(val accountId: AccountId, val rewardPoints: RewardPoint) + class Individual(val accountId: AccountId, val rewardPoints: RewardPoints) } @UseCaseBinding fun bindEraRewardPoints( - scale: String?, - runtime: RuntimeSnapshot, - type: Type<*>, + decoded: Any? ): EraRewardPoints { - val dynamicInstance = scale?.let { type.fromHexOrNull(runtime, it) }.cast() + val dynamicInstance = decoded.castToStruct() return EraRewardPoints( totalPoints = bindRewardPoint(dynamicInstance["total"]), @@ -46,4 +40,4 @@ fun bindEraRewardPoints( } @HelperBinding -fun bindRewardPoint(dynamicInstance: Any?): RewardPoint = bindNumber(dynamicInstance) +fun bindRewardPoint(dynamicInstance: Any?): RewardPoints = bindNumber(dynamicInstance) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/Exposure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/Exposure.kt index f4ecaaf67f..eee60b9bf8 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/Exposure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/Exposure.kt @@ -2,17 +2,16 @@ package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindi import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding +import io.novafoundation.nova.common.data.network.runtime.binding.bindList +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.data.network.runtime.binding.incompatible import io.novafoundation.nova.common.data.network.runtime.binding.requireType -import io.novafoundation.nova.common.data.network.runtime.binding.returnType -import io.novafoundation.nova.common.utils.staking import io.novafoundation.nova.feature_staking_api.domain.model.Exposure +import io.novafoundation.nova.feature_staking_api.domain.model.ExposureOverview +import io.novafoundation.nova.feature_staking_api.domain.model.ExposurePage import io.novafoundation.nova.feature_staking_api.domain.model.IndividualExposure -import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHexOrNull -import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import java.math.BigInteger /* @@ -31,22 +30,6 @@ private fun bindIndividualExposure(dynamicInstance: Any?): IndividualExposure { return IndividualExposure(who, value) } -/* - Exposure: { - total: Compact; // total stake of the validator - own: Compact; // own stake of the validator - others: Vec; // nominators stakes -} - */ -@UseCaseBinding -fun bindExposure(scale: String, runtime: RuntimeSnapshot): Exposure { - val type = runtime.metadata.staking().storage("ErasStakers").returnType() - - val decoded = type.fromHexOrNull(runtime, scale) - - return bindExposure(decoded) -} - @UseCaseBinding fun bindExposure(instance: Any?): Exposure { val decoded = instance.castToStruct() @@ -58,3 +41,24 @@ fun bindExposure(instance: Any?): Exposure { return Exposure(total, own, others) } + +@UseCaseBinding +fun bindExposureOverview(instance: Any?): ExposureOverview { + val decoded = instance.castToStruct() + + return ExposureOverview( + total = bindNumber(decoded["total"]), + own = bindNumber(decoded["own"]), + nominatorCount = bindNumber(decoded["nominatorCount"]), + pageCount = bindNumber(decoded["pageCount"]) + ) +} + +@UseCaseBinding +fun bindExposurePage(instance: Any?): ExposurePage { + val decoded = instance.castToStruct() + + return ExposurePage( + others = bindList(decoded["others"], ::bindIndividualExposure) + ) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/StakingLedger.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/StakingLedger.kt index 4242218a3f..f9940add7f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/StakingLedger.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/StakingLedger.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindi import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding +import io.novafoundation.nova.common.data.network.runtime.binding.bindList import io.novafoundation.nova.common.data.network.runtime.binding.getList import io.novafoundation.nova.common.data.network.runtime.binding.getTyped import io.novafoundation.nova.common.data.network.runtime.binding.incompatible @@ -24,15 +25,18 @@ fun bindStakingLedger(scale: String, runtime: RuntimeSnapshot): StakingLedger { } @UseCaseBinding -fun bindStakingLedger(dynamicInstance: Any): StakingLedger { - requireType(dynamicInstance) +fun bindStakingLedger(decoded: Any): StakingLedger { + requireType(decoded) return StakingLedger( - stashId = dynamicInstance.getTyped("stash"), - total = dynamicInstance.getTyped("total"), - active = dynamicInstance.getTyped("active"), - unlocking = dynamicInstance.getList("unlocking").map(::bindUnlockChunk), - claimedRewards = dynamicInstance.getList("claimedRewards").map(::bindEraIndex) + stashId = decoded.getTyped("stash"), + total = decoded.getTyped("total"), + active = decoded.getTyped("active"), + unlocking = decoded.getList("unlocking").map(::bindUnlockChunk), + claimedRewards = bindList( + dynamicInstance = decoded["claimedRewards"] ?: decoded["legacyClaimedRewards"] ?: emptyList(), + itemBinder = ::bindEraIndex + ) ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ValidatorPrefs.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ValidatorPrefs.kt index 84760f42f6..e2179cad31 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ValidatorPrefs.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/bindings/ValidatorPrefs.kt @@ -1,15 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings -import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding import io.novafoundation.nova.common.data.network.runtime.binding.bindPerbillNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.data.network.runtime.binding.getTyped -import io.novafoundation.nova.common.data.network.runtime.binding.returnType -import io.novafoundation.nova.common.utils.staking import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs -import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHexOrNull -import jp.co.soramitsu.fearless_utils.runtime.metadata.storage private const val BLOCKED_DEFAULT = false @@ -21,11 +15,3 @@ fun bindValidatorPrefs(decoded: Any?): ValidatorPrefs { blocked = asStruct["blocked"] ?: BLOCKED_DEFAULT ) } - -@UseCaseBinding -fun bindValidatorPrefs(scale: String, runtime: RuntimeSnapshot): ValidatorPrefs { - val type = runtime.metadata.staking().storage("Validators").returnType() - val decoded = type.fromHexOrNull(runtime, scale) - - return bindValidatorPrefs(decoded) -} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/calls/ExtrinsicBuilderExt.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/calls/ExtrinsicBuilderExt.kt index 2f903dd784..2684b1d0f5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/calls/ExtrinsicBuilderExt.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/calls/ExtrinsicBuilderExt.kt @@ -50,17 +50,6 @@ fun ExtrinsicBuilder.nominate(targets: List): ExtrinsicBuilder { ) } -fun ExtrinsicBuilder.payoutStakers(era: BigInteger, validatorId: AccountId): ExtrinsicBuilder { - return call( - "Staking", - "payout_stakers", - mapOf( - "validator_stash" to validatorId, - "era" to era - ) - ) -} - fun ExtrinsicBuilder.bondMore(amount: BigInteger): ExtrinsicBuilder { return call( "Staking", diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/Common.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/Common.kt index 7d488089b1..6ec56a6789 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/Common.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/Common.kt @@ -4,23 +4,13 @@ import io.novafoundation.nova.common.data.network.rpc.BulkRetriever import io.novafoundation.nova.common.utils.staking import io.novafoundation.nova.core.model.StorageEntry import io.novafoundation.nova.core.storage.StorageCache -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindActiveEra -import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey import jp.co.soramitsu.fearless_utils.wsrpc.SocketService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import java.math.BigInteger fun RuntimeMetadata.activeEraStorageKey() = staking().storage("ActiveEra").storageKey() -suspend fun StorageCache.observeActiveEraIndex(runtime: RuntimeSnapshot, chainId: String): Flow { - return observeEntry(runtime.metadata.activeEraStorageKey(), chainId) - .map { bindActiveEra(it.content!!, runtime) } -} - suspend fun BulkRetriever.fetchValuesToCache( socketService: SocketService, keys: List, @@ -34,13 +24,20 @@ suspend fun BulkRetriever.fetchValuesToCache( storageCache.insert(toInsert, chainId) } +/** + * @return number of fetched keys + */ suspend fun BulkRetriever.fetchPrefixValuesToCache( socketService: SocketService, prefix: String, storageCache: StorageCache, chainId: String -) { +): Int { val allKeys = retrieveAllKeys(socketService, prefix) - fetchValuesToCache(socketService, allKeys, storageCache, chainId) + if (allKeys.isNotEmpty()) { + fetchValuesToCache(socketService, allKeys, storageCache, chainId) + } + + return allKeys.size } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/ValidatorExposureUpdater.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/ValidatorExposureUpdater.kt index b5d0fcbe8d..a93cc16001 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/ValidatorExposureUpdater.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/ValidatorExposureUpdater.kt @@ -1,72 +1,197 @@ package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters import io.novafoundation.nova.common.data.network.rpc.BulkRetriever +import io.novafoundation.nova.common.utils.hasStorage import io.novafoundation.nova.common.utils.staking +import io.novafoundation.nova.core.model.StorageEntry import io.novafoundation.nova.core.storage.StorageCache -import io.novafoundation.nova.core.updater.GlobalScopeUpdater import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.core.updater.Updater +import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.ValidatorExposureUpdater.Companion.STORAGE_KEY_PAGED_EXPOSURES import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.base.StakingUpdater +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.ActiveEraScope import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.getRuntime import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey import jp.co.soramitsu.fearless_utils.wsrpc.SocketService -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.flow import java.math.BigInteger +/** + * Manages sync for validators exposures + * Depending on the version of the staking pallet, exposures might be stored differently: + * + * 1. Legacy version - exposures are stored in `EraStakers` storage + * 2. Current version - exposures are paged and stored in two storages: `EraStakersPaged` and `EraStakersOverview` + * + * Note that during the transition from Legacy to Current version during next [Staking.historyDepth] + * eras older storage will still be present and filled with previous era information. Old storage will be cleared on era-by-era basis once new era happens. + * Also, right after chain has just upgraded, `EraStakersPaged` will be empty untill next era happens. + * + * The updater takes care of that all also storing special [STORAGE_KEY_PAGED_EXPOSURES] indicating whether + * paged exposures are actually present for the latest era or not + */ class ValidatorExposureUpdater( private val bulkRetriever: BulkRetriever, private val stakingSharedState: StakingSharedState, private val chainRegistry: ChainRegistry, - private val storageCache: StorageCache -) : GlobalScopeUpdater, StakingUpdater { + private val storageCache: StorageCache, + override val scope: ActiveEraScope, +) : StakingUpdater { + + companion object { + + const val STORAGE_KEY_PAGED_EXPOSURES = "NovaWallet.Staking.PagedExposuresUsed" + + fun isPagedExposuresValue(enabled: Boolean) = enabled.toString() + + fun decodeIsPagedExposuresValue(value: String?) = value.toBoolean() + } override suspend fun listenForUpdates( storageSubscriptionBuilder: SharedRequestsBuilder, - scopeValue: Unit, + scopeValue: EraIndex, ): Flow { + @Suppress("UnnecessaryVariable") + val activeEra = scopeValue val socketService = storageSubscriptionBuilder.socketService ?: return emptyFlow() - val chainId = stakingSharedState.chainId() - val runtime = chainRegistry.getRuntime(chainId) + return flow { + val chainId = stakingSharedState.chainId() + val runtime = chainRegistry.getRuntime(chainId) + + if (checkValuesInCache(activeEra, chainId, runtime)) { + return@flow + } + + cleanupOutdatedEras(chainId, runtime) - return storageCache.observeActiveEraIndex(runtime, chainId) - .map { activeEraIndex -> runtime.eraStakersPrefixFor(activeEraIndex) } - .filterNot { storageCache.isPrefixInCache(it, chainId) } - .onEach { cleanupPreviousEras(runtime, chainId) } - .onEach { updateNominatorsForEra(it, socketService, chainId) } - .flowOn(Dispatchers.IO) - .noSideAffects() + syncNewExposures(activeEra, runtime, socketService, chainId) + }.noSideAffects() } - private suspend fun cleanupPreviousEras(runtimeSnapshot: RuntimeSnapshot, chainId: String) { - val prefix = runtimeSnapshot.eraStakersPrefix() + private suspend fun checkValuesInCache(era: BigInteger, chainId: String, runtimeSnapshot: RuntimeSnapshot): Boolean { + if (runtimeSnapshot.pagedExposuresEnabled()) { + return isPagedExposuresInCache(era, chainId, runtimeSnapshot) || isLegacyExposuresInCache(era, chainId, runtimeSnapshot) + } - storageCache.removeByPrefix(prefix, chainId) + return isLegacyExposuresInCache(era, chainId, runtimeSnapshot) } - private fun RuntimeSnapshot.eraStakersPrefixFor(era: BigInteger): String { - return metadata.staking().storage("ErasStakers").storageKey(this, era) + private suspend fun isPagedExposuresInCache(era: BigInteger, chainId: String, runtimeSnapshot: RuntimeSnapshot): Boolean { + val prefix = runtimeSnapshot.eraStakersOverviewPrefixFor(era) + + return storageCache.isPrefixInCache(prefix, chainId) } - private fun RuntimeSnapshot.eraStakersPrefix(): String { - return metadata.staking().storage("ErasStakers").storageKey(this) + private suspend fun isLegacyExposuresInCache(era: BigInteger, chainId: String, runtimeSnapshot: RuntimeSnapshot): Boolean { + val prefix = runtimeSnapshot.eraStakersPrefixFor(era) + + return storageCache.isPrefixInCache(prefix, chainId) } - private suspend fun updateNominatorsForEra( - eraStakersPrefix: String, + private suspend fun cleanupOutdatedEras(chainId: String, runtimeSnapshot: RuntimeSnapshot) { + if (runtimeSnapshot.pagedExposuresEnabled()) { + cleanupPagedExposures(runtimeSnapshot, chainId) + } + + cleanupLegacyExposures(runtimeSnapshot, chainId) + } + + private suspend fun cleanupLegacyExposures(runtimeSnapshot: RuntimeSnapshot, chainId: String) { + storageCache.removeByPrefix(runtimeSnapshot.eraStakersPrefix(), chainId) + } + + private suspend fun cleanupPagedExposures(runtimeSnapshot: RuntimeSnapshot, chainId: String) { + storageCache.removeByPrefix(runtimeSnapshot.eraStakersPagedPrefix(), chainId) + storageCache.removeByPrefix(runtimeSnapshot.eraStakersOverviewPrefix(), chainId) + } + + private suspend fun syncNewExposures(era: BigInteger, runtimeSnapshot: RuntimeSnapshot, socketService: SocketService, chainId: String) { + var pagedExposureUsed = false + + if (runtimeSnapshot.pagedExposuresEnabled()) { + val pagedExposuresWerePresent = tryFetchingPagedExposures(era, runtimeSnapshot, socketService, chainId) + + if (!pagedExposuresWerePresent) { + fetchLegacyExposures(era, runtimeSnapshot, socketService, chainId) + } else { + pagedExposureUsed = true + } + } else { + fetchLegacyExposures(era, runtimeSnapshot, socketService, chainId) + } + + saveIsExposuresUsedFlag(pagedExposureUsed, chainId) + } + + private suspend fun tryFetchingPagedExposures( + era: BigInteger, + runtimeSnapshot: RuntimeSnapshot, + socketService: SocketService, + chainId: String + ): Boolean = runCatching { + val overviewPrefix = runtimeSnapshot.eraStakersOverviewPrefixFor(era) + val numberOfKeysSynced = bulkRetriever.fetchPrefixValuesToCache(socketService, overviewPrefix, storageCache, chainId) + + val pagedExposuresPresent = numberOfKeysSynced > 0 + + if (pagedExposuresPresent) { + val pagedExposuresPrefix = runtimeSnapshot.eraStakersPagedPrefixFor(era) + bulkRetriever.fetchPrefixValuesToCache(socketService, pagedExposuresPrefix, storageCache, chainId) + } + + pagedExposuresPresent + }.getOrDefault(false) + + private suspend fun fetchLegacyExposures( + era: BigInteger, + runtimeSnapshot: RuntimeSnapshot, socketService: SocketService, chainId: String - ) = runCatching { - bulkRetriever.fetchPrefixValuesToCache(socketService, eraStakersPrefix, storageCache, chainId) + ): Result = runCatching { + val prefix = runtimeSnapshot.eraStakersPrefixFor(era) + bulkRetriever.fetchPrefixValuesToCache(socketService, prefix, storageCache, chainId) + } + + private suspend fun saveIsExposuresUsedFlag(isUsed: Boolean, chainId: String) { + val encodedValue = isPagedExposuresValue(isUsed) + val entry = StorageEntry(STORAGE_KEY_PAGED_EXPOSURES, encodedValue) + + storageCache.insert(entry, chainId) + } + + private fun RuntimeSnapshot.pagedExposuresEnabled(): Boolean { + return metadata.staking().hasStorage("ErasStakersPaged") + } + + private fun RuntimeSnapshot.eraStakersPrefixFor(era: BigInteger): String { + return metadata.staking().storage("ErasStakers").storageKey(this, era) + } + + private fun RuntimeSnapshot.eraStakersOverviewPrefixFor(era: BigInteger): String { + return metadata.staking().storage("ErasStakersOverview").storageKey(this, era) + } + + private fun RuntimeSnapshot.eraStakersOverviewPrefix(): String { + return metadata.staking().storage("ErasStakersOverview").storageKey(this) + } + + private fun RuntimeSnapshot.eraStakersPagedPrefixFor(era: BigInteger): String { + return metadata.staking().storage("ErasStakersPaged").storageKey(this, era) + } + + private fun RuntimeSnapshot.eraStakersPagedPrefix(): String { + return metadata.staking().storage("ErasStakersPaged").storageKey(this) + } + + private fun RuntimeSnapshot.eraStakersPrefix(): String { + return metadata.staking().storage("ErasStakers").storageKey(this) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalTotalValidatorRewardUpdater.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalTotalValidatorRewardUpdater.kt index 5ed3102f66..5f8b284469 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalTotalValidatorRewardUpdater.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalTotalValidatorRewardUpdater.kt @@ -4,14 +4,9 @@ import io.novafoundation.nova.common.utils.staking import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey -import java.math.BigInteger class HistoricalTotalValidatorRewardUpdater : HistoricalUpdater { - override fun constructHistoricalKey(runtime: RuntimeSnapshot, era: BigInteger): String { - return runtime.metadata.staking().storage("ErasValidatorReward").storageKey(runtime, era) - } - override fun constructKeyPrefix(runtime: RuntimeSnapshot): String { return runtime.metadata.staking().storage("ErasValidatorReward").storageKey(runtime) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt index 7b4d0aba1b..33a92e9a6d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt @@ -1,31 +1,22 @@ package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.historical -import io.novafoundation.nova.common.data.network.rpc.BulkRetriever +import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.core.updater.Updater -import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository -import io.novafoundation.nova.feature_staking_api.domain.api.historicalEras import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.base.StakingUpdater -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.fetchValuesToCache import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.ActiveEraScope import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.getRuntime import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.onEach -import java.math.BigInteger interface HistoricalUpdater { - fun constructHistoricalKey(runtime: RuntimeSnapshot, era: BigInteger): String - fun constructKeyPrefix(runtime: RuntimeSnapshot): String } @@ -33,41 +24,35 @@ class HistoricalUpdateMediator( override val scope: ActiveEraScope, private val historicalUpdaters: List, private val stakingSharedState: StakingSharedState, - private val bulkRetriever: BulkRetriever, - private val stakingRepository: StakingRepository, private val storageCache: StorageCache, private val chainRegistry: ChainRegistry, + private val preferences: Preferences, ) : Updater, StakingUpdater { override suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder, scopeValue: EraIndex): Flow { val chainId = stakingSharedState.chainId() val runtime = chainRegistry.getRuntime(chainId) - val socketService = storageSubscriptionBuilder.socketService ?: return emptyFlow() - return flowOf { - val allKeysNeeded = constructHistoricalKeys(chainId, runtime) - val keysInDataBase = storageCache.filterKeysInCache(allKeysNeeded, chainId).toSet() + if (isHistoricalDataCleared(chainId)) return@flowOf null - val missingKeys = allKeysNeeded.filter { it !in keysInDataBase } + val prefixes = historicalUpdaters.map { it.constructKeyPrefix(runtime) } + prefixes.onEach { storageCache.removeByPrefix(prefixKey = it, chainId) } - allKeysNeeded to missingKeys + setHistoricalDataCleared(chainId) } - .filter { (_, missing) -> missing.isNotEmpty() } - .onEach { (allNeeded, missing) -> - val prefixes = historicalUpdaters.map { it.constructKeyPrefix(runtime) } - prefixes.onEach { storageCache.removeByPrefixExcept(prefixKey = it, fullKeyExceptions = allNeeded, chainId) } - - bulkRetriever.fetchValuesToCache(socketService, missing, storageCache, chainId) - } .noSideAffects() } - private suspend fun constructHistoricalKeys(chainId: ChainId, runtime: RuntimeSnapshot): List { - val historicalRange = stakingRepository.historicalEras(chainId) + private fun isHistoricalDataCleared(chainId: ChainId): Boolean { + return preferences.contains(isHistoricalDataClearedKey(chainId)) + } - return historicalUpdaters.flatMap { updater -> - historicalRange.map { updater.constructHistoricalKey(runtime, it) } - } + private fun setHistoricalDataCleared(chainId: ChainId) { + preferences.putBoolean(isHistoricalDataClearedKey(chainId), true) + } + + private fun isHistoricalDataClearedKey(chainId: ChainId): String { + return "HistoricalUpdateMediator.HistoricalDataCleared::$chainId" } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalValidatorRewardPointsUpdater.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalValidatorRewardPointsUpdater.kt index 17c3cb13b0..aadee6820d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalValidatorRewardPointsUpdater.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/updaters/historical/HistoricalValidatorRewardPointsUpdater.kt @@ -4,14 +4,9 @@ import io.novafoundation.nova.common.utils.staking import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey -import java.math.BigInteger class HistoricalValidatorRewardPointsUpdater : HistoricalUpdater { - override fun constructHistoricalKey(runtime: RuntimeSnapshot, era: BigInteger): String { - return runtime.metadata.staking().storage("ErasRewardPoints").storageKey(runtime, era) - } - override fun constructKeyPrefix(runtime: RuntimeSnapshot): String { return runtime.metadata.staking().storage("ErasRewardPoints").storageKey(runtime) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/StakingApi.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/StakingApi.kt index 8bd4014328..7df2dca154 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/StakingApi.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/StakingApi.kt @@ -4,7 +4,8 @@ import io.novafoundation.nova.common.data.network.subquery.EraValidatorInfoQuery import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.DirectStakingPeriodRewardsRequest import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.PoolStakingPeriodRewardsRequest -import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.StakingEraValidatorInfosRequest +import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.StakingNominatorEraInfosRequest +import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.StakingValidatorEraInfosRequest import io.novafoundation.nova.feature_staking_impl.data.network.subquery.response.StakingPeriodRewardsResponse import retrofit2.http.Body import retrofit2.http.POST @@ -25,8 +26,14 @@ interface StakingApi { ): SubQueryResponse @POST - suspend fun getValidatorsInfo( + suspend fun getNominatorEraInfos( @Url url: String, - @Body body: StakingEraValidatorInfosRequest + @Body body: StakingNominatorEraInfosRequest + ): SubQueryResponse + + @POST + suspend fun getValidatorEraInfos( + @Url url: String, + @Body body: StakingValidatorEraInfosRequest ): SubQueryResponse } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/SubQueryValidatorSetFetcher.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/SubQueryValidatorSetFetcher.kt index 96784bac4f..184a3a7e61 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/SubQueryValidatorSetFetcher.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/SubQueryValidatorSetFetcher.kt @@ -1,33 +1,69 @@ package io.novafoundation.nova.feature_staking_impl.data.network.subquery -import io.novafoundation.nova.common.data.network.subquery.EraValidatorInfoQueryResponse.EraValidatorInfo.Nodes.Node -import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository -import io.novafoundation.nova.feature_staking_api.domain.api.historicalEras +import io.novafoundation.nova.common.address.AccountIdKey +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.data.network.subquery.EraValidatorInfoQueryResponse +import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse +import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex import io.novafoundation.nova.feature_staking_impl.data.model.stakingExternalApi -import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.StakingEraValidatorInfosRequest +import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.StakingNominatorEraInfosRequest +import io.novafoundation.nova.feature_staking_impl.data.network.subquery.request.StakingValidatorEraInfosRequest import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId +import java.math.BigInteger + +data class PayoutTarget(val validatorStash: AccountIdKey, val era: BigInteger) class SubQueryValidatorSetFetcher( private val stakingApi: StakingApi, - private val stakingRepository: StakingRepository, ) { - suspend fun fetchAllValidators(chain: Chain, stashAccountAddress: String): List { - val historicalRange = stakingRepository.historicalEras(chain.id) + suspend fun findNominatorPayoutTargets( + chain: Chain, + stashAccountAddress: String, + eraRange: List, + ): List { + return findPayoutTargets(chain) { apiUrl -> + stakingApi.getNominatorEraInfos( + apiUrl, + StakingNominatorEraInfosRequest( + eraFrom = eraRange.first(), + eraTo = eraRange.last(), + nominatorStashAddress = stashAccountAddress + ) + ) + } + } + + suspend fun findValidatorPayoutTargets( + chain: Chain, + stashAccountAddress: String, + eraRange: List, + ): List { + return findPayoutTargets(chain) { apiUrl -> + stakingApi.getValidatorEraInfos( + apiUrl, + StakingValidatorEraInfosRequest( + eraFrom = eraRange.first(), + eraTo = eraRange.last(), + validatorStashAddress = stashAccountAddress + ) + ) + } + } + private suspend fun findPayoutTargets( + chain: Chain, + apiCall: suspend (url: String) -> SubQueryResponse + ): List { val stakingExternalApi = chain.stakingExternalApi() ?: return emptyList() - val validatorsInfos = stakingApi.getValidatorsInfo( - stakingExternalApi.url, - StakingEraValidatorInfosRequest( - eraFrom = historicalRange.first(), - eraTo = historicalRange.last(), - accountAddress = stashAccountAddress - ) - ) + val validatorsInfos = apiCall(stakingExternalApi.url) + + val nodes = validatorsInfos.data.eraValidatorInfos?.nodes.orEmpty() - return validatorsInfos.data.query?.eraValidatorInfos?.nodes?.map( - Node::address - )?.distinct().orEmpty() + return nodes.map { + PayoutTarget(it.address.toAccountId().intoKey(), it.era) + } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingEraValidatorInfosRequest.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingNominatorEraInfosRequest.kt similarity index 75% rename from feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingEraValidatorInfosRequest.kt rename to feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingNominatorEraInfosRequest.kt index 46763e4976..d3b11f6e9e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingEraValidatorInfosRequest.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingNominatorEraInfosRequest.kt @@ -2,14 +2,13 @@ package io.novafoundation.nova.feature_staking_impl.data.network.subquery.reques import java.math.BigInteger -class StakingEraValidatorInfosRequest(eraFrom: BigInteger, eraTo: BigInteger, accountAddress: String) { +class StakingNominatorEraInfosRequest(eraFrom: BigInteger, eraTo: BigInteger, nominatorStashAddress: String) { val query = """ - { query { eraValidatorInfos( filter:{ era:{ greaterThanOrEqualTo: $eraFrom, lessThanOrEqualTo: $eraTo}, - others:{ contains:[{who: "$accountAddress"}]} + others:{ contains:[{who: "$nominatorStashAddress"}]} } ) { nodes { @@ -21,6 +20,5 @@ class StakingEraValidatorInfosRequest(eraFrom: BigInteger, eraTo: BigInteger, ac } } } - } """.trimIndent() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingValidatorEraInfosRequest.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingValidatorEraInfosRequest.kt new file mode 100644 index 0000000000..eb63c57372 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/subquery/request/StakingValidatorEraInfosRequest.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.feature_staking_impl.data.network.subquery.request + +import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters +import java.math.BigInteger + +class StakingValidatorEraInfosRequest(eraFrom: BigInteger, eraTo: BigInteger, validatorStashAddress: String) : SubQueryFilters { + val query = """ + query { + eraValidatorInfos( + filter:{ + era:{ greaterThanOrEqualTo: $eraFrom, lessThanOrEqualTo: $eraTo}, + ${"address" equalTo validatorStashAddress} + } + ) { + nodes { + id + address + era + total + own + } + } + } + """.trimIndent() +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/PayoutRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/PayoutRepository.kt index c90e76a3e6..91db20510c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/PayoutRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/PayoutRepository.kt @@ -1,86 +1,84 @@ package io.novafoundation.nova.feature_staking_impl.data.repository -import io.novafoundation.nova.common.data.network.rpc.BulkRetriever -import io.novafoundation.nova.common.data.network.runtime.binding.BinderWithType -import io.novafoundation.nova.common.data.network.runtime.binding.returnType -import io.novafoundation.nova.common.utils.mapValuesNotNull +import android.util.Log +import io.novafoundation.nova.common.address.AccountIdKey +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.utils.findPartitionPoint +import io.novafoundation.nova.common.utils.hasStorage +import io.novafoundation.nova.common.utils.mapToSet +import io.novafoundation.nova.common.utils.reversed import io.novafoundation.nova.common.utils.staking -import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_api.domain.api.historicalEras +import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex import io.novafoundation.nova.feature_staking_api.domain.model.Exposure -import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger +import io.novafoundation.nova.feature_staking_api.domain.model.ExposureOverview +import io.novafoundation.nova.feature_staking_api.domain.model.ExposurePage +import io.novafoundation.nova.feature_staking_api.domain.model.PagedExposure import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState import io.novafoundation.nova.feature_staking_impl.data.model.Payout +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.ClaimedRewardsPages import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.EraRewardPoints +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindClaimedPages import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindEraRewardPoints import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindExposure +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindExposureOverview +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindExposurePage import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindStakingLedger -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindTotalValidatorEraReward import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindValidatorPrefs +import io.novafoundation.nova.feature_staking_impl.data.network.subquery.PayoutTarget import io.novafoundation.nova.feature_staking_impl.data.network.subquery.SubQueryValidatorSetFetcher -import io.novafoundation.nova.runtime.ext.accountIdOf +import io.novafoundation.nova.feature_staking_impl.data.repository.PayoutRepository.ValidatorEraStake.NominatorInfo +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.getRuntime -import io.novafoundation.nova.runtime.multiNetwork.getSocket -import jp.co.soramitsu.fearless_utils.extensions.fromHex -import jp.co.soramitsu.fearless_utils.extensions.toHexString +import io.novafoundation.nova.runtime.network.rpc.RpcCalls +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder +import io.novafoundation.nova.runtime.storage.source.query.DynamicInstanceBinder +import io.novafoundation.nova.runtime.storage.source.query.metadata +import io.novafoundation.nova.runtime.storage.source.query.multi +import io.novafoundation.nova.runtime.storage.source.query.wrapSingleArgumentKeys +import jp.co.soramitsu.fearless_utils.hash.isPositive import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.metadata.module.StorageEntry import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey -import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId -import jp.co.soramitsu.fearless_utils.wsrpc.SocketService import java.math.BigInteger typealias HistoricalMapping = Map // EraIndex -> T -class ValidatorHistoricalStats( - val validatorAddress: String, - val ledger: StakingLedger, - val infoHistory: HistoricalMapping, -) { - - class ValidatorEraStats( - val prefs: ValidatorPrefs, - val exposure: Exposure, - ) -} - -private class RewardCalculationContext( - val validatorEraStats: ValidatorHistoricalStats.ValidatorEraStats, - val eraValidatorPointsDistribution: EraRewardPoints, - val validatorAccountId: AccountId, - val totalEraReward: BigInteger -) - class PayoutRepository( private val stakingRepository: StakingRepository, - private val bulkRetriever: BulkRetriever, private val validatorSetFetcher: SubQueryValidatorSetFetcher, - private val storageCache: StorageCache, private val chainRegistry: ChainRegistry, + private val remoteStorage: StorageDataSource, + private val rpcCalls: RpcCalls, ) { suspend fun calculateUnpaidPayouts(stakingState: StakingState.Stash): List { return when (stakingState) { is StakingState.Stash.Nominator -> calculateUnpaidPayouts( chain = stakingState.chain, - retrieveValidatorAddresses = { - validatorSetFetcher.fetchAllValidators(stakingState.chain, stakingState.stashAddress) + retrievePayoutTargets = { historicalRange -> + validatorSetFetcher.findNominatorPayoutTargets(stakingState.chain, stakingState.stashAddress, historicalRange) }, - calculatePayoutReward = { + calculatePayout = { calculateNominatorReward(stakingState.stashId, it) } ) is StakingState.Stash.Validator -> calculateUnpaidPayouts( chain = stakingState.chain, - retrieveValidatorAddresses = { listOf(stakingState.stashAddress) }, - calculatePayoutReward = ::calculateValidatorReward + retrievePayoutTargets = { historicalRange -> + validatorSetFetcher.findValidatorPayoutTargets(stakingState.chain, stakingState.stashAddress, historicalRange) + }, + calculatePayout = ::calculateValidatorReward ) else -> throw IllegalStateException("Cannot calculate payouts for ${stakingState::class.simpleName} state") } @@ -88,77 +86,208 @@ class PayoutRepository( private suspend fun calculateUnpaidPayouts( chain: Chain, - retrieveValidatorAddresses: suspend () -> List, - calculatePayoutReward: (RewardCalculationContext) -> BigInteger? + retrievePayoutTargets: suspend (historicalRange: List) -> List, + calculatePayout: (RewardCalculationContext) -> Payout? ): List { val chainId = chain.id val runtime = chainRegistry.getRuntime(chain.id) - val socketService = chainRegistry.getSocket(chain.id) - - val validatorAddresses = retrieveValidatorAddresses() val historicalRange = stakingRepository.historicalEras(chainId) - val validatorStats = getValidatorHistoricalStats(runtime, socketService, historicalRange, validatorAddresses) - val historicalRangeSet = historicalRange.toSet() - - val historicalTotalEraRewards = retrieveTotalEraReward(chainId, runtime, historicalRange) - val historicalRewardDistribution = retrieveEraPointsDistribution(chainId, runtime, historicalRange) - - return validatorStats.map { stats -> - val claimedRewardsHoles = historicalRangeSet - stats.ledger.claimedRewards.toSet() - val validatorAddress = stats.validatorAddress - - claimedRewardsHoles.mapNotNull { holeEra -> - val rewardCalculationContext = RewardCalculationContext( - validatorEraStats = stats.infoHistory[holeEra] ?: return@mapNotNull null, - validatorAccountId = chain.accountIdOf(validatorAddress), - totalEraReward = historicalTotalEraRewards[holeEra]!!, - eraValidatorPointsDistribution = historicalRewardDistribution[holeEra]!! - ) + val payoutTargets = retrievePayoutTargets(historicalRange) + + Log.d("PayoutRepository", "Fetched payoutTargets") + + val involvedEras = payoutTargets.map { it.era }.distinct() + + val validatorEraStakes = getValidatorHistoricalStats(chainId, runtime, historicalRange, payoutTargets) + + val historicalTotalEraRewards = retrieveTotalEraReward(chainId, runtime, involvedEras) + Log.d("PayoutRepository", "Fetched historicalTotalEraRewards") + + val historicalRewardDistribution = retrieveEraPointsDistribution(chainId, runtime, involvedEras) + Log.d("PayoutRepository", "Fetched historicalRewardDistribution") + + val eraClaims = fetchValidatorEraClaims(chain, payoutTargets) + Log.d("PayoutRepository", "Fetched eraClaims") + + return validatorEraStakes.mapNotNull { validatorEraStake -> + val key = validatorEraStake.era to validatorEraStake.stash + val eraPointsDistribution = historicalRewardDistribution[validatorEraStake.era] ?: return@mapNotNull null + + val rewardCalculationContext = RewardCalculationContext( + eraStake = validatorEraStake, + eraClaim = eraClaims[key] ?: return@mapNotNull null, + totalEraReward = historicalTotalEraRewards[validatorEraStake.era] ?: return@mapNotNull null, + eraPoints = eraPointsDistribution + ) + + calculatePayout(rewardCalculationContext) + }.also { + Log.d("PayoutRepository", "Constructed payouts") + } + } + + private suspend fun partitionHistoricalRangeByPagedExposurePresence( + historicalRange: List, + runtime: RuntimeSnapshot, + chainId: ChainId + ): PartitionedHistoricalRange { + val firstPagedExposureEra = findFirstPagedExposureIndex(historicalRange, runtime, chainId) + ?: return PartitionedHistoricalRange(legacy = historicalRange, paged = emptyList()) + + return PartitionedHistoricalRange( + legacy = historicalRange.subList(0, firstPagedExposureEra), + paged = historicalRange.subList(firstPagedExposureEra, historicalRange.size) + ) + } + + private suspend fun findFirstPagedExposureIndex( + historicalRange: List, + runtime: RuntimeSnapshot, + chainId: ChainId + ): Int? { + val oldestEra = historicalRange.first() + + if (!runtime.metadata.staking().hasStorage("ErasStakersOverview")) return null + if (!runtime.metadata.staking().hasStorage("ErasStakersClipped")) return 0 + + // We expect certain delay between all legacy exposures are gone and runtime upgrade that removes storages completely + // This small optimizations reduces the number of requests needed to 1 for such period despite adding 1 extra request during migration period + if (isExposurePaged(oldestEra, runtime, chainId)) return 0 + + return historicalRange.findPartitionPoint { era -> isExposurePaged(era, runtime, chainId) }.also { + Log.d("PayoutRepository", "Fount first paged exposures era: ${it?.let { historicalRange[it] }}") + } + } - val reward = calculatePayoutReward(rewardCalculationContext) + private suspend fun isExposurePaged(eraIndex: EraIndex, runtime: RuntimeSnapshot, chainId: ChainId): Boolean { + val pagedEraPrefix = runtime.metadata.staking().storage("ErasStakersOverview").storageKey(runtime, eraIndex) + val storageSize = rpcCalls.getStorageSize(chainId, pagedEraPrefix) - reward?.let { Payout(validatorAddress, holeEra, it) } + Log.d("PayoutRepository", "Fetched storage size for era $eraIndex: $storageSize") + + return storageSize.isPositive() + } + + private suspend fun fetchValidatorEraClaims( + chain: Chain, + payoutTargets: List, + ): Map, ValidatorEraClaim> { + return remoteStorage.query(chain.id) { + var validatorsPrefsDescriptor: MultiQueryBuilder.Descriptor, ValidatorPrefs?> + var controllersDescriptor: MultiQueryBuilder.Descriptor + var claimedPagesDescriptor: MultiQueryBuilder.Descriptor, ClaimedRewardsPages?>? = null + + val multiQueryResults = multi { + val eraAndValidatorKeys = payoutTargets.map { listOf(it.era, it.validatorStash.value) } + val validatorKeys = payoutTargets.mapToSet { it.validatorStash }.map { listOf(it.value) } + + validatorsPrefsDescriptor = runtime.metadata.staking().storage("ErasValidatorPrefs").queryKeys( + keysArgs = eraAndValidatorKeys, + keyExtractor = { (era: EraIndex, validatorStash: AccountId) -> era to validatorStash.intoKey() }, + binding = { decoded -> decoded?.let { bindValidatorPrefs(decoded) } } + ) + controllersDescriptor = runtime.metadata.staking().storage("Bonded").queryKeys( + keysArgs = validatorKeys, + keyExtractor = { (validatorStash: AccountId) -> validatorStash.intoKey() }, + binding = { decoded -> decoded?.let { bindAccountId(decoded).intoKey() } } + ) + if (metadata.staking().hasStorage("ClaimedRewards")) { + claimedPagesDescriptor = runtime.metadata.staking().storage("ClaimedRewards").queryKeys( + keysArgs = eraAndValidatorKeys, + keyExtractor = { (era: EraIndex, validatorStash: AccountId) -> era to validatorStash.intoKey() }, + binding = { decoded -> decoded?.let { bindClaimedPages(decoded) } } + ) + } } - }.flatten() + + Log.d("PayoutRepository", "Fetched prefs, controllers and claimed pages") + + val validatorsPrefs = multiQueryResults[validatorsPrefsDescriptor] + val controllers = multiQueryResults[controllersDescriptor] + + val stashByController = controllers + .mapValues { (stash, controller) -> controller ?: stash } + .reversed() + + val claimedLegacyRewards = metadata.staking().storage("Ledger").entries( + keysArguments = stashByController.keys.map { listOf(it.value) }, + keyExtractor = { (controller: AccountId) -> stashByController.getValue(controller.intoKey()) }, + binding = { decoded, _ -> decoded?.let { bindStakingLedger(decoded).claimedRewards.toSet() } } + ) + + Log.d("PayoutRepository", "Fetched claimedLegacyRewards") + + val allClaimedPages = claimedPagesDescriptor?.let(multiQueryResults::get) + + payoutTargets.mapNotNull { payoutTarget -> + val key = payoutTarget.era to payoutTarget.validatorStash + + val prefs = validatorsPrefs[key] ?: return@mapNotNull null + val claimedLegacyRewardsForValidator = claimedLegacyRewards[payoutTarget.validatorStash] ?: return@mapNotNull null + val claimedPages = allClaimedPages?.get(key).orEmpty() + + key to ValidatorEraClaim(prefs, claimedLegacyRewardsForValidator, payoutTarget.era, claimedPages) + }.toMap() + } } + // Nominator only pays out its own reward page private fun calculateNominatorReward( nominatorAccountId: AccountId, rewardCalculationContext: RewardCalculationContext - ): BigInteger? = with(rewardCalculationContext) { - val nominatorIdHex = nominatorAccountId.toHexString() + ): Payout? = with(rewardCalculationContext) { + val nominatorEraInfo = rewardCalculationContext.eraStake.findNominatorInfo(nominatorAccountId) ?: return null + val nominatorPage = nominatorEraInfo.pageNumber - val nominatorStakeInEra = validatorEraStats.exposure.others.firstOrNull { - it.who.toHexString() == nominatorIdHex - }?.value?.toDouble() ?: return null + if (rewardCalculationContext.eraClaim.pageClaimed(nominatorPage)) return null // already did the payout - val validatorTotalStake = validatorEraStats.exposure.total.toDouble() - val validatorCommission = validatorEraStats.prefs.commission.toDouble() + val nominatorStakeInEra = nominatorEraInfo.stake.toDouble() - val validatorTotalReward = calculateValidatorTotalReward(totalEraReward, eraValidatorPointsDistribution, validatorAccountId) ?: return null + val validatorTotalStake = eraStake.totalStake.toDouble() + val validatorCommission = eraClaim.validatorPrefs.commission.toDouble() - val nominatorReward = validatorTotalReward * (1 - validatorCommission) * (nominatorStakeInEra / validatorTotalStake) + val validatorTotalReward = calculateValidatorTotalReward(totalEraReward, eraPoints, eraStake.stash.value) ?: return null - return nominatorReward.toBigDecimal().toBigInteger() + val nominatorReward = validatorTotalReward * (1 - validatorCommission) * (nominatorStakeInEra / validatorTotalStake) + val nominatorRewardPlanks = nominatorReward.toBigDecimal().toBigInteger() + + Payout( + validatorStash = rewardCalculationContext.eraStake.stash, + era = rewardCalculationContext.eraStake.era, + amount = nominatorRewardPlanks, + pagesToClaim = listOf(nominatorPage) + ) } + // Validator pays out whole payout private fun calculateValidatorReward( rewardCalculationContext: RewardCalculationContext - ): BigInteger? = with(rewardCalculationContext) { - val validatorTotalStake = validatorEraStats.exposure.total.toDouble() - val validatorOwnStake = validatorEraStats.exposure.own.toDouble() - val validatorCommission = validatorEraStats.prefs.commission.toDouble() + ): Payout? = with(rewardCalculationContext) { + val totalPayoutPages = eraStake.totalPages + val unpaidPages = eraClaim.remainingPagesToClaim(totalPayoutPages) - val validatorTotalReward = calculateValidatorTotalReward(totalEraReward, eraValidatorPointsDistribution, validatorAccountId) ?: return null + if (unpaidPages.isEmpty()) return null + + val validatorTotalStake = eraStake.totalStake.toDouble() + val validatorOwnStake = eraStake.validatorStake.toDouble() + val validatorCommission = eraClaim.validatorPrefs.commission.toDouble() + + val validatorTotalReward = calculateValidatorTotalReward(totalEraReward, eraPoints, eraStake.stash.value) ?: return null val validatorRewardFromCommission = validatorTotalReward * validatorCommission val validatorRewardFromStake = validatorTotalReward * (1 - validatorCommission) * (validatorOwnStake / validatorTotalStake) val validatorOwnReward = validatorRewardFromStake + validatorRewardFromCommission - - validatorOwnReward.toBigDecimal().toBigInteger() + val validatorRewardPlanks = validatorOwnReward.toBigDecimal().toBigInteger() + + Payout( + validatorStash = rewardCalculationContext.eraStake.stash, + era = rewardCalculationContext.eraStake.era, + amount = validatorRewardPlanks, + pagesToClaim = unpaidPages + ) } private fun calculateValidatorTotalReward( @@ -166,112 +295,253 @@ class PayoutRepository( eraValidatorPointsDistribution: EraRewardPoints, validatorAccountId: AccountId, ): Double? { - val validatorIdHex = validatorAccountId.toHexString() - val totalMinted = totalEraReward.toDouble() val totalPoints = eraValidatorPointsDistribution.totalPoints.toDouble() val validatorPoints = eraValidatorPointsDistribution.individual.firstOrNull { - it.accountId.toHexString() == validatorIdHex + it.accountId.contentEquals(validatorAccountId) }?.rewardPoints?.toDouble() ?: return null return totalMinted * validatorPoints / totalPoints } private suspend fun getValidatorHistoricalStats( + chainId: ChainId, runtime: RuntimeSnapshot, - socketService: SocketService, historicalRange: List, - validatorAddresses: List, - ): List { - val stakingModule = runtime.metadata.staking() - - val exposureClippedStorage = stakingModule.storage("ErasStakersClipped") - val exposureKeyMapping = validatorAddresses.associateWith { validatorAddress -> - historicalRange.associateWith { era -> - exposureClippedStorage.storageKey(runtime, era, validatorAddress.toAccountId()) - } + payoutTargets: List, + ): List { + val (legacyEras, _) = partitionHistoricalRangeByPagedExposurePresence(historicalRange, runtime, chainId) + val legacyEraSet = legacyEras.toSet() + + val (legacyPayoutTargets, pagedPayoutTargets) = payoutTargets.partition { it.era in legacyEraSet } + + return getLegacyValidatorEraStake(chainId, legacyPayoutTargets) + getPagedValidatorHistoricalStats(chainId, pagedPayoutTargets) + } + + private suspend fun getPagedValidatorHistoricalStats( + chainId: ChainId, + payoutTargets: List, + ): List { + if (payoutTargets.isEmpty()) return emptyList() + + val pagedExposures = remoteStorage.fetchPagedExposures(chainId, payoutTargets) + + return pagedExposures.mapNotNull { (key, pagedExposure) -> + if (pagedExposure == null) return@mapNotNull null + + val (era, accountId) = key + PagedValidatorEraStake(accountId, era, pagedExposure) + }.also { + Log.d("PayoutRepository", "Fetched getPagedValidatorHistoricalStats for ${payoutTargets.size} targets") } + } - val validatorPrefsStorage = stakingModule.storage("ErasValidatorPrefs") - val prefsKeyMapping = validatorAddresses.associateWith { validatorAddress -> - historicalRange.associateWith { era -> - validatorPrefsStorage.storageKey(runtime, era, validatorAddress.toAccountId()) + private suspend fun StorageDataSource.fetchPagedExposures( + chainId: ChainId, + payoutTargets: List + ): Map, PagedExposure?> { + return query(chainId) { + val eraAndValidatorKeys = payoutTargets.map { listOf(it.era, it.validatorStash.value) } + + val overview = runtime.metadata.staking().storage("ErasStakersOverview").entries( + keysArguments = eraAndValidatorKeys, + keyExtractor = { (era: EraIndex, validatorStash: AccountId) -> era to validatorStash.intoKey() }, + binding = { decoded, _ -> decoded?.let { bindExposureOverview(decoded) } }, + ) + + val pageKeys = overview.flatMap { (key, exposureOverview) -> + if (exposureOverview == null) return@flatMap emptyList() + + (0 until exposureOverview.pageCount.toInt()).map { page -> + listOf(key.first, key.second.value, page.toBigInteger()) + } } - } - val controllerStorage = stakingModule.storage("Bonded") - val controllerMapping = validatorAddresses.associateWith { validatorAddress -> - controllerStorage.storageKey(runtime, validatorAddress.toAccountId()) + val pages = runtime.metadata.staking().storage("ErasStakersPaged").entries( + keysArguments = pageKeys, + keyExtractor = { (era: EraIndex, validatorStash: AccountId, page: BigInteger) -> Triple(era, validatorStash.intoKey(), page.toInt()) }, + binding = { decoded, _ -> decoded?.let { bindExposurePage(decoded) } }, + ) + + mergeIntoPagedExposure(overview, pages) } + } - val exposureClippedKeys = exposureKeyMapping.values.map(Map::values).flatten() - val prefsKeys = prefsKeyMapping.values.map(Map::values).flatten() - val ledgerKeys = controllerMapping.values.toList() + private fun mergeIntoPagedExposure( + overviews: Map, ExposureOverview?>, + pages: Map, ExposurePage?> + ): Map, PagedExposure?> { + return overviews.mapValues { (key, exposureOverview) -> + if (exposureOverview == null) return@mapValues null - val allResults = bulkRetriever.queryKeys(socketService, exposureClippedKeys + prefsKeys + ledgerKeys) + val totalPages = exposureOverview.pageCount.toInt() + val exposurePages = (0 until totalPages).map { page -> + pages[Triple(key.first, key.second, page)] ?: return@mapValues null + } - val ledgerStorage = stakingModule.storage("Ledger") - val ledgerKeysMapping = controllerMapping.mapValuesNotNull { (_, key) -> - allResults[key]?.fromHex()?.let { ledgerStorage.storageKey(runtime, it) } + PagedExposure(exposureOverview, exposurePages) } + } - val ledgerResults = bulkRetriever.queryKeys(socketService, ledgerKeysMapping.values.toList()) + private suspend fun getLegacyValidatorEraStake( + chainId: ChainId, + payoutTargets: List, + ): List { + if (payoutTargets.isEmpty()) return emptyList() - return validatorAddresses.mapNotNull { validatorAddress -> - val ledger = ledgerResults[ledgerKeysMapping[validatorAddress]]?.let { bindStakingLedger(it, runtime) } ?: return@mapNotNull null + return remoteStorage.query(chainId) { + val eraAndValidatorKeys = payoutTargets.map { listOf(it.era, it.validatorStash.value) } - val historicalStats = historicalRange.associateWith { era -> - val exposureKey = exposureKeyMapping[validatorAddress]!![era]!! - val prefsKey = prefsKeyMapping[validatorAddress]!![era]!! + val exposuresClipped = runtime.metadata.staking().storage("ErasStakersClipped").entries( + keysArguments = eraAndValidatorKeys, + keyExtractor = { (era: EraIndex, validatorStash: AccountId) -> era to validatorStash.intoKey() }, + binding = { decoded, _ -> decoded?.let { bindExposure(decoded) } }, + ) - val exposure = allResults[exposureKey]?.let { bindExposure(it, runtime) } - val prefs = allResults[prefsKey]?.let { bindValidatorPrefs(it, runtime) } + payoutTargets.mapNotNull { payoutTarget -> + val eraAndAccountIdKey = payoutTarget.era to payoutTarget.validatorStash - if (exposure != null && prefs != null) { - ValidatorHistoricalStats.ValidatorEraStats(prefs, exposure) - } else { - null - } - } + val exposure = exposuresClipped[eraAndAccountIdKey] ?: return@mapNotNull null - ValidatorHistoricalStats(validatorAddress, ledger, historicalStats) + LegacyValidatorEraStake(payoutTarget.validatorStash, payoutTarget.era, exposure) + }.also { + Log.d("PayoutRepository", "Fetched LegacyValidatorEraStake for ${payoutTargets.size} targets") + } } } private suspend fun retrieveEraPointsDistribution( chainId: ChainId, runtime: RuntimeSnapshot, - historicalRange: List, + eras: List, ): HistoricalMapping { val storage = runtime.metadata.staking().storage("ErasRewardPoints") - return retrieveHistoricalInfo(chainId, runtime, historicalRange, storage, ::bindEraRewardPoints) + return retrieveHistoricalInfoForValidator(chainId, eras, storage, ::bindEraRewardPoints) } private suspend fun retrieveTotalEraReward( chainId: ChainId, runtime: RuntimeSnapshot, - historicalRange: List, + eras: List, ): HistoricalMapping { val storage = runtime.metadata.staking().storage("ErasValidatorReward") - return retrieveHistoricalInfo(chainId, runtime, historicalRange, storage, ::bindTotalValidatorEraReward) + return retrieveHistoricalInfoForValidator(chainId, eras, storage, ::bindNumber) } - private suspend fun retrieveHistoricalInfo( + private suspend fun retrieveHistoricalInfoForValidator( chainId: ChainId, - runtime: RuntimeSnapshot, - historicalRange: List, + eras: List, storage: StorageEntry, - binder: BinderWithType, + binding: DynamicInstanceBinder, ): HistoricalMapping { - val historicalKeysMapping = historicalRange.associateBy { storage.storageKey(runtime, it) } - val storageReturnType = storage.returnType() + return remoteStorage.query(chainId) { + storage.entries( + keysArguments = eras.wrapSingleArgumentKeys(), + keyExtractor = { (era: EraIndex) -> era }, + binding = { decoded, _ -> binding(decoded) } + ) + } + } + + private data class PartitionedHistoricalRange( + val legacy: List, + val paged: List + ) + + private class RewardCalculationContext( + val eraStake: ValidatorEraStake, + val eraClaim: ValidatorEraClaim, + val eraPoints: EraRewardPoints, + val totalEraReward: BigInteger + ) + + private interface ValidatorEraStake { + + class NominatorInfo(val stake: Balance, val pageNumber: Int) + + val stash: AccountIdKey - return storageCache.getEntries(historicalKeysMapping.keys.toList(), chainId) - .associate { - historicalKeysMapping[it.storageKey]!! to binder(it.content, runtime, storageReturnType) + val era: EraIndex + + val totalStake: BigInteger + + val totalPages: Int + + val validatorStake: BigInteger + + fun findNominatorInfo(accountId: AccountId): NominatorInfo? + } + + private class LegacyValidatorEraStake( + override val stash: AccountIdKey, + override val era: EraIndex, + private val exposure: Exposure + ) : ValidatorEraStake { + + override val totalPages: Int = 1 + + override val totalStake: BigInteger = exposure.total + + override val validatorStake: BigInteger = exposure.own + override fun findNominatorInfo(accountId: AccountId): NominatorInfo? { + val individualExposure = exposure.others.find { it.who.contentEquals(accountId) } + + return individualExposure?.let { + NominatorInfo(individualExposure.value, pageNumber = 0) + } + } + } + + private class PagedValidatorEraStake( + override val stash: AccountIdKey, + override val era: EraIndex, + private val exposure: PagedExposure, + ) : ValidatorEraStake { + + override val totalStake: BigInteger = exposure.overview.total + + override val validatorStake: BigInteger = exposure.overview.own + + override val totalPages: Int = exposure.overview.pageCount.toInt() + + override fun findNominatorInfo(accountId: AccountId): NominatorInfo? { + var result: NominatorInfo? = null + + exposure.pages.forEachIndexed { index, exposurePage -> + val individualExposure = exposurePage.others.find { it.who.contentEquals(accountId) } + + if (individualExposure != null) { + result = NominatorInfo(individualExposure.value, index) + } } + + return result + } + } + + private class ValidatorEraClaim( + val validatorPrefs: ValidatorPrefs, + legacyClaimedEras: Set, + era: EraIndex, + private val claimedPages: ClaimedRewardsPages + ) { + + private val claimedFromLegacy = era in legacyClaimedEras + + fun pageClaimed(page: Int): Boolean { + return claimedFromLegacy || page in claimedPages + } + + fun remainingPagesToClaim(totalPages: Int): List { + if (claimedFromLegacy) return emptyList() + + val allPages = (0 until totalPages).toSet() + val unpaidPages = allPages - claimedPages + + return unpaidPages.toList() + } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingConstantsRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingConstantsRepository.kt index e2acbae816..56da12aa67 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingConstantsRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingConstantsRepository.kt @@ -24,7 +24,12 @@ class StakingConstantsRepository( ?: MAX_UNLOCK_CHUNKS_FALLBACK.toBigInteger() } - suspend fun maxRewardedNominatorPerValidator(chainId: ChainId): Int = getNumberConstant(chainId, "MaxNominatorRewardedPerValidator").toInt() + /** + * Returns maxRewardedNominatorPerValidator or null in case there is no limitation on rewarded nominators per validator + */ + suspend fun maxRewardedNominatorPerValidator(chainId: ChainId): Int? { + return getOptionalNumberConstant(chainId, "MaxNominatorRewardedPerValidator")?.toInt() + } suspend fun lockupPeriodInEras(chainId: ChainId): BigInteger = getNumberConstant(chainId, "BondingDuration") diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt index d61b41bba6..1d1409e22c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt @@ -6,11 +6,16 @@ import io.novafoundation.nova.common.utils.constant import io.novafoundation.nova.common.utils.numberConstant import io.novafoundation.nova.common.utils.numberConstantOrNull import io.novafoundation.nova.common.utils.staking +import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.AccountStakingDao import io.novafoundation.nova.core_db.model.AccountStakingLocal import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex +import io.novafoundation.nova.feature_staking_api.domain.model.Exposure +import io.novafoundation.nova.feature_staking_api.domain.model.ExposureOverview +import io.novafoundation.nova.feature_staking_api.domain.model.ExposurePage +import io.novafoundation.nova.feature_staking_api.domain.model.IndividualExposure import io.novafoundation.nova.feature_staking_api.domain.model.Nominations import io.novafoundation.nova.feature_staking_api.domain.model.SlashingSpans import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger @@ -23,6 +28,8 @@ import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.st import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindActiveEra import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindCurrentEra import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindExposure +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindExposureOverview +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindExposurePage import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindHistoryDepth import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindMaxNominators import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindMinBond @@ -32,6 +39,7 @@ import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindin import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlashDeferDuration import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlashingSpans import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindValidatorPrefs +import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.ValidatorExposureUpdater import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.activeEraStorageKey import io.novafoundation.nova.feature_staking_impl.data.repository.datasource.StakingStoriesDataSource import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants @@ -66,6 +74,7 @@ class StakingRepositoryImpl( private val walletConstants: WalletConstants, private val chainRegistry: ChainRegistry, private val stakingStoriesDataSource: StakingStoriesDataSource, + private val storageCache: StorageCache, ) : StakingRepository { override suspend fun eraStartSessionIndex(chainId: ChainId, currentEra: BigInteger): EraIndex { @@ -109,7 +118,54 @@ class StakingRepositoryImpl( binding = { scale, runtime -> bindActiveEra(scale, runtime) } ) - override suspend fun getElectedValidatorsExposure(chainId: ChainId, eraIndex: EraIndex) = localStorage.query(chainId) { + override suspend fun getElectedValidatorsExposure(chainId: ChainId, eraIndex: EraIndex): AccountIdMap { + val isPagedExposures = isPagedExposuresUsed(chainId) + + return if (isPagedExposures) { + fetchPagedEraStakers(chainId, eraIndex) + } else { + fetchLegacyEraStakers(chainId, eraIndex) + } + } + + private suspend fun fetchPagedEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap = localStorage.query(chainId) { + val eraStakersOverview = metadata.staking().storage("ErasStakersOverview").entries( + eraIndex, + keyExtractor = { (_: BigInteger, accountId: ByteArray) -> accountId.toHexString() }, + binding = { instance, _ -> bindExposureOverview(instance) } + ) + + val eraStakersPaged = runtime.metadata.staking().storage("ErasStakersPaged").entries( + eraIndex, + keyExtractor = { (_: BigInteger, accountId: ByteArray, page: BigInteger) -> accountId.toHexString() to page.toInt() }, + binding = { instance, _ -> bindExposurePage(instance) } + ) + + mergeOverviewsAndPagedOthers(eraStakersOverview, eraStakersPaged) + } + + private fun mergeOverviewsAndPagedOthers( + eraStakerOverviews: AccountIdMap, + othersPaged: Map, ExposurePage> + ): AccountIdMap { + return eraStakerOverviews.mapValues { (accountId, overview) -> + // avoid unnecessary growth allocations by pre-allocating exact needed number of elements + val others = ArrayList(overview.nominatorCount.toInt()) + + (0 until overview.pageCount.toInt()).forEach { pageNumber -> + val page = othersPaged[accountId to pageNumber]?.others.orEmpty() + others.addAll(page) + } + + Exposure( + total = overview.total, + own = overview.own, + others = others + ) + } + } + + private suspend fun fetchLegacyEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap = localStorage.query(chainId) { runtime.metadata.staking().storage("ErasStakers").entries( eraIndex, keyExtractor = { (_: BigInteger, accountId: ByteArray) -> accountId.toHexString() }, @@ -224,8 +280,8 @@ class StakingRepositoryImpl( return stakingStoriesDataSource.getStoriesFlow() } - override suspend fun ledgerFlow(stakingState: StakingState.Stash): Flow { - return localStorage.query(stakingState.chain.id) { + override fun ledgerFlow(stakingState: StakingState.Stash): Flow { + return localStorage.subscribe(stakingState.chain.id) { metadata.staking.ledger.observe(stakingState.controllerId) }.filterNotNull() } @@ -270,14 +326,19 @@ class StakingRepositoryImpl( } } + private suspend fun isPagedExposuresUsed(chainId: ChainId): Boolean { + val isPagedExposuresValue = storageCache.getEntry(ValidatorExposureUpdater.STORAGE_KEY_PAGED_EXPOSURES, chainId) + + return ValidatorExposureUpdater.decodeIsPagedExposuresValue(isPagedExposuresValue.content) + } + private fun observeAccountValidatorPrefs(chainId: ChainId, stashId: AccountId): Flow { - return localStorage.observe( - chainId = chainId, - keyBuilder = { it.metadata.staking().storage("Validators").storageKey(it, stashId) }, - binder = { scale, runtime -> - scale?.let { bindValidatorPrefs(it, runtime) } - } - ) + return localStorage.subscribe(chainId) { + runtime.metadata.staking().storage("Validators").observe( + stashId, + binding = { decoded -> decoded?.let { bindValidatorPrefs(decoded) } } + ) + } } private fun observeAccountNominations(chainId: ChainId, stashId: AccountId): Flow { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureDependencies.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureDependencies.kt index c8495e2207..8703881f96 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureDependencies.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureDependencies.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.mixin.hints.ResourcesHintsMixinFactory import io.novafoundation.nova.common.resources.ContextManager import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.multiResult.PartialRetriableMixin import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.AccountStakingDao @@ -160,4 +161,6 @@ interface StakingFeatureDependencies { val locksRepository: BalanceLocksRepository val externalBalanceDao: ExternalBalanceDao + + val partialRetriableMixinFactory: PartialRetriableMixin.Factory } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt index bc0b262540..d768208a63 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt @@ -103,6 +103,7 @@ import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.network.rpc.RpcCalls import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState @@ -155,13 +156,15 @@ class StakingFeatureModule { stakingStoriesDataSource: StakingStoriesDataSource, walletConstants: WalletConstants, chainRegistry: ChainRegistry, + storageCache: StorageCache ): StakingRepository = StakingRepositoryImpl( accountStakingDao = accountStakingDao, remoteStorage = remoteStorageSource, localStorage = localStorageSource, stakingStoriesDataSource = stakingStoriesDataSource, walletConstants = walletConstants, - chainRegistry = chainRegistry + chainRegistry = chainRegistry, + storageCache = storageCache ) @Provides @@ -329,13 +332,11 @@ class StakingFeatureModule { @Provides @FeatureScope fun provideRecommendationSettingsProviderFactory( - stakingConstantsRepository: StakingConstantsRepository, computationalCache: ComputationalCache, sharedState: StakingSharedState, chainRegistry: ChainRegistry, ) = RecommendationSettingsProviderFactory( computationalCache, - stakingConstantsRepository, chainRegistry, sharedState ) @@ -416,12 +417,8 @@ class StakingFeatureModule { @FeatureScope fun provideValidatorSetFetcher( stakingApi: StakingApi, - stakingRepository: StakingRepository, ): SubQueryValidatorSetFetcher { - return SubQueryValidatorSetFetcher( - stakingApi, - stakingRepository - ) + return SubQueryValidatorSetFetcher(stakingApi) } @Provides @@ -443,11 +440,11 @@ class StakingFeatureModule { fun providePayoutRepository( stakingRepository: StakingRepository, validatorSetFetcher: SubQueryValidatorSetFetcher, - @PayoutsBulkRetriever bulkRetriever: BulkRetriever, - storageCache: StorageCache, chainRegistry: ChainRegistry, + rpcCalls: RpcCalls, + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource ): PayoutRepository { - return PayoutRepository(stakingRepository, bulkRetriever, validatorSetFetcher, storageCache, chainRegistry) + return PayoutRepository(stakingRepository, validatorSetFetcher, chainRegistry, remoteStorageSource, rpcCalls) } @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/relaychain/RelaychainStakingUpdatersModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/relaychain/RelaychainStakingUpdatersModule.kt index 53e869c9b4..7b8aa8fe4e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/relaychain/RelaychainStakingUpdatersModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/relaychain/RelaychainStakingUpdatersModule.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.di.staking.relaychain import dagger.Module import dagger.Provides import io.novafoundation.nova.common.data.network.rpc.BulkRetriever +import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.AccountStakingDao @@ -86,11 +87,13 @@ class RelaychainStakingUpdatersModule { chainRegistry: ChainRegistry, @DefaultBulkRetriever bulkRetriever: BulkRetriever, storageCache: StorageCache, + scope: ActiveEraScope ) = ValidatorExposureUpdater( bulkRetriever, sharedState, chainRegistry, - storageCache + storageCache, + scope ) @Provides @@ -186,10 +189,9 @@ class RelaychainStakingUpdatersModule { fun provideHistoricalMediator( sharedState: StakingSharedState, chainRegistry: ChainRegistry, - @DefaultBulkRetriever bulkRetriever: BulkRetriever, - stakingRepository: StakingRepository, storageCache: StorageCache, activeEraScope: ActiveEraScope, + preferences: Preferences ) = HistoricalUpdateMediator( historicalUpdaters = listOf( HistoricalTotalValidatorRewardUpdater(), @@ -197,10 +199,9 @@ class RelaychainStakingUpdatersModule { ), stakingSharedState = sharedState, chainRegistry = chainRegistry, - bulkRetriever = bulkRetriever, - stakingRepository = stakingRepository, storageCache = storageCache, - scope = activeEraScope + scope = activeEraScope, + preferences = preferences ) @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/MakePayoutValidationsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/MakePayoutValidationsModule.kt index 548537ea01..abeefe5806 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/MakePayoutValidationsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/MakePayoutValidationsModule.kt @@ -22,14 +22,14 @@ class MakePayoutValidationsModule { return EnoughAmountToTransferValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.asset.transferable }, - errorProducer = { _, _ -> PayoutValidationFailure.CannotPayFee } + errorProducer = { PayoutValidationFailure.CannotPayFee } ) } @FeatureScope @Provides fun provideProfitableValidation() = ProfitablePayoutValidation( - fee = { fee }, + fee = { it.fee }, amount = { totalReward }, error = { PayoutValidationFailure.UnprofitablePayout } ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RebondValidationsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RebondValidationsModule.kt index 161befea9c..d507c46358 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RebondValidationsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RebondValidationsModule.kt @@ -18,7 +18,7 @@ class RebondValidationsModule { fun provideFeeValidation() = RebondFeeValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.controllerAsset.transferable }, - errorProducer = { _, _ -> RebondValidationFailure.CANNOT_PAY_FEE } + errorProducer = { RebondValidationFailure.CANNOT_PAY_FEE } ) @FeatureScope diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RedeemValidationsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RedeemValidationsModule.kt index ddf87fd537..90517a70aa 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RedeemValidationsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RedeemValidationsModule.kt @@ -16,7 +16,7 @@ class RedeemValidationsModule { fun provideFeeValidation() = RedeemFeeValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.asset.transferable }, - errorProducer = { _, _ -> RedeemValidationFailure.CANNOT_PAY_FEES } + errorProducer = { RedeemValidationFailure.CANNOT_PAY_FEES } ) @FeatureScope diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RewardDestinationValidationsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RewardDestinationValidationsModule.kt index ac22da1cc0..4b553b04c5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RewardDestinationValidationsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/RewardDestinationValidationsModule.kt @@ -19,7 +19,7 @@ class RewardDestinationValidationsModule { fun provideFeeValidation() = RewardDestinationFeeValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.availableControllerBalance }, - errorProducer = { _, _ -> RewardDestinationValidationFailure.CannotPayFees } + errorProducer = { RewardDestinationValidationFailure.CannotPayFees } ) @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/SetControllerValidationsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/SetControllerValidationsModule.kt index f3bc28aecf..cab15bab83 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/SetControllerValidationsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/SetControllerValidationsModule.kt @@ -23,7 +23,7 @@ class SetControllerValidationsModule { return EnoughAmountToTransferValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.transferable }, - errorProducer = { _, _ -> SetControllerValidationFailure.NOT_ENOUGH_TO_PAY_FEES } + errorProducer = { SetControllerValidationFailure.NOT_ENOUGH_TO_PAY_FEES } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/UnbondValidationsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/UnbondValidationsModule.kt index 7f0a39a38f..34414fe503 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/UnbondValidationsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/UnbondValidationsModule.kt @@ -26,7 +26,7 @@ class UnbondValidationsModule { fun provideFeeValidation() = UnbondFeeValidation( feeExtractor = { it.fee }, availableBalanceProducer = { it.asset.transferable }, - errorProducer = { _, _ -> UnbondValidationFailure.CannotPayFees } + errorProducer = { UnbondValidationFailure.CannotPayFees } ) @FeatureScope diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractor.kt index 25677c65ed..165a8de2c1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractor.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.domain -import io.novafoundation.nova.common.address.AccountIdKey -import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.sumByBigInteger @@ -10,7 +9,6 @@ import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentit import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_api.domain.model.Exposure -import io.novafoundation.nova.feature_staking_api.domain.model.IndividualExposure import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination import io.novafoundation.nova.feature_staking_api.domain.model.StakingAccount import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState @@ -18,7 +16,6 @@ import io.novafoundation.nova.feature_staking_impl.data.StakingOption import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.fullId import io.novafoundation.nova.feature_staking_impl.data.mappers.mapAccountToStakingAccount -import io.novafoundation.nova.feature_staking_impl.data.model.Payout import io.novafoundation.nova.feature_staking_impl.data.repository.PayoutRepository import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConstantsRepository import io.novafoundation.nova.feature_staking_impl.data.repository.StakingRewardsRepository @@ -41,6 +38,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.runtime.ext.accountIdOf +import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.state.assetWithChain import io.novafoundation.nova.runtime.state.chain @@ -52,9 +50,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @@ -89,8 +87,8 @@ class StakingInteractor( val payouts = payoutRepository.calculateUnpaidPayouts(currentStakingState) - val allValidatorAddresses = payouts.map(Payout::validatorAddress).distinct() - val identityMapping = identityRepository.getIdentitiesFromAddresses(currentStakingState.chain, allValidatorAddresses) + val allValidatorStashes = payouts.map { it.validatorStash.value }.distinct() + val identityMapping = identityRepository.getIdentitiesFromIds(allValidatorStashes, chainId) val pendingPayouts = payouts.map { val erasLeft = remainingEras(createdAtEra = it.era, activeEraIndex, historyDepth) @@ -100,9 +98,12 @@ class StakingInteractor( val leftTime = calculator.calculateTillEraSet(destinationEra = it.era + historyDepth + ERA_OFFSET).toLong() val currentTimestamp = System.currentTimeMillis() with(it) { - val validatorIdentity = identityMapping[validatorAddress] + val validatorIdentity = identityMapping[validatorStash] - val validatorInfo = PendingPayout.ValidatorInfo(validatorAddress, validatorIdentity?.display) + val validatorInfo = PendingPayout.ValidatorInfo( + address = currentStakingState.chain.addressOf(validatorStash.value), + identityName = validatorIdentity?.display + ) PendingPayout( validatorInfo = validatorInfo, @@ -110,7 +111,8 @@ class StakingInteractor( amountInPlanks = amount, timeLeft = leftTime, timeLeftCalculatedAt = currentTimestamp, - closeToExpire = closeToExpire + closeToExpire = closeToExpire, + pagesToClaim = pagesToClaim ) } }.sortedBy { it.era } @@ -227,7 +229,7 @@ class StakingInteractor( suspend fun getAccountProjectionsInSelectedChains() = withContext(Dispatchers.Default) { val chain = stakingSharedState.chain() - accountRepository.allMetaAccounts().mapNotNull { + accountRepository.getActiveMetaAccounts().mapNotNull { mapAccountToStakingAccount(chain, it) } } @@ -262,7 +264,7 @@ class StakingInteractor( stakingConstantsRepository.maxValidatorsPerNominator(stakingSharedState.chainId(), stake) } - suspend fun maxRewardedNominators(): Int = withContext(Dispatchers.Default) { + suspend fun maxRewardedNominators(): Int? = withContext(Dispatchers.Default) { stakingConstantsRepository.maxRewardedNominatorPerValidator(stakingSharedState.chainId()) } @@ -292,10 +294,10 @@ class StakingInteractor( val chainAsset = stakingSharedState.chainAsset() val chainId = chainAsset.chainId - combineTransform( + combineToPair( stakingSharedComputation.activeEraInfo(chainId, scope), walletRepository.assetFlow(state.accountId, chainAsset) - ) { activeEraInfo, asset -> + ).flatMapLatest { (activeEraInfo, asset) -> val activeStake = asset.bondedInPlanks val rewardedNominatorsPerValidator = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId) @@ -307,14 +309,12 @@ class StakingInteractor( activeStake = activeStake ) - val summary = flow { statusResolver(statusResolutionContext) }.map { status -> + flow { statusResolver(statusResolutionContext) }.map { status -> StakeSummary( status = status, activeStake = activeStake ) } - - emitAll(summary) } } @@ -327,13 +327,14 @@ class StakingInteractor( private suspend fun activeNominators(chainId: ChainId, exposures: Collection): Int { val activeNominatorsPerValidator = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId) - return exposures.fold(mutableSetOf()) { acc, exposure -> - acc += exposure.others.sortedByDescending(IndividualExposure::value) - .take(activeNominatorsPerValidator) - .map { it.who.intoKey() } - - acc - }.size + return exposures.fold(0) { acc, exposure -> + val othersSize = exposure.others.size + acc + if (activeNominatorsPerValidator != null) { + othersSize.coerceAtMost(activeNominatorsPerValidator) + } else { + othersSize + } + } } private fun totalStake(exposures: Collection): BigInteger { @@ -350,7 +351,7 @@ class StakingInteractor( private class StatusResolutionContext( val activeEraInfo: ActiveEraInfo, val asset: Asset, - val rewardedNominatorsPerValidator: Int, + val rewardedNominatorsPerValidator: Int?, val activeStake: Balance, ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt index d7cc81b8da..6f2460d0e2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt @@ -24,7 +24,7 @@ val NominationStatus.isOversubscribed: Boolean fun nominationStatus( stashId: AccountId, exposures: Collection, - rewardedNominatorsPerValidator: Int + rewardedNominatorsPerValidator: Int? ): NominationStatus { val comparator = { accountId: IndividualExposure -> accountId.who.contentEquals(stashId) @@ -37,7 +37,9 @@ fun nominationStatus( return NominationStatus.NOT_PRESENT } - val willBeRewarded = validatorsWithOurStake.any { it.willAccountBeRewarded(stashId, rewardedNominatorsPerValidator) } + val willBeRewarded = rewardedNominatorsPerValidator == null || validatorsWithOurStake.any { + it.willAccountBeRewarded(stashId, rewardedNominatorsPerValidator) + } return if (willBeRewarded) NominationStatus.ACTIVE else NominationStatus.OVERSUBSCRIBED } @@ -63,7 +65,6 @@ fun minimumStake( exposures: Collection, minimumNominatorBond: BigInteger, bagListLocator: BagListLocator?, - maxNominatorsInValidator: Int, bagListScoreConverter: BagListScoreConverter, bagListSize: BigInteger?, maxElectingVoters: BigInteger? diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/alerts/AlertsInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/alerts/AlertsInteractor.kt index d392cda145..5a19f0b074 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/alerts/AlertsInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/alerts/AlertsInteractor.kt @@ -42,7 +42,7 @@ class AlertsInteractor( class AlertContext( val exposures: Map, val stakingState: StakingState, - val maxRewardedNominatorsPerValidator: Int, + val maxRewardedNominatorsPerValidator: Int?, val minRecommendedStake: Balance, val activeEra: BigInteger, val asset: Asset, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagInteractor.kt index a7757846cf..75d081d349 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagInteractor.kt @@ -3,12 +3,14 @@ package io.novafoundation.nova.feature_staking_impl.domain.bagList.rebag import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.common.utils.map import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.stashTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.rebag import io.novafoundation.nova.feature_staking_impl.data.repository.BagListRepository import io.novafoundation.nova.feature_staking_impl.data.repository.bagListLocatorOrThrow import io.novafoundation.nova.feature_staking_impl.domain.bagList.BagListScoreConverter -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository import kotlinx.coroutines.flow.Flow @@ -17,9 +19,9 @@ import kotlinx.coroutines.flow.filterNotNull interface RebagInteractor { - suspend fun calculateFee(stakingState: StakingState.Stash): Balance + suspend fun calculateFee(stakingState: StakingState.Stash): Fee - suspend fun rebag(stakingState: StakingState.Stash): Result + suspend fun rebag(stakingState: StakingState.Stash): Result fun rebagMovementFlow(stakingState: StakingState.Stash): Flow } @@ -31,14 +33,14 @@ class RealRebagInteractor( private val extrinsicService: ExtrinsicService, ) : RebagInteractor { - override suspend fun calculateFee(stakingState: StakingState.Stash): Balance { - return extrinsicService.estimateFee(stakingState.chain) { + override suspend fun calculateFee(stakingState: StakingState.Stash): Fee { + return extrinsicService.estimateFee(stakingState.chain, stakingState.stashTransactionOrigin()) { rebag(stakingState.stashId) } } - override suspend fun rebag(stakingState: StakingState.Stash): Result { - return extrinsicService.submitExtrinsicWithAnySuitableWallet(stakingState.chain, stakingState.stashId) { + override suspend fun rebag(stakingState: StakingState.Stash): Result { + return extrinsicService.submitExtrinsic(stakingState.chain, stakingState.stashTransactionOrigin()) { rebag(stakingState.stashId) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt index 05662d7117..6dbd99c370 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.bagList.rebag.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class RebagValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationSystem.kt index fd5fac2aae..d871e1a84d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationSystem.kt @@ -9,11 +9,11 @@ fun ValidationSystem.Companion.rebagValidationSystem(): RebagValidationSystem = sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> RebagValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt index 351de81bdc..e3e862338f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt @@ -86,14 +86,12 @@ class StakingSharedComputation( val bagListScoreConverter = BagListScoreConverter.U128(totalIssuance) val maxElectingVoters = bagListRepository.maxElectingVotes(chainId) val bagListSize = bagListRepository.bagListSize(chainId) - val maxNominatorsInValidator = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId) electedExposuresWithActiveEraFlow(chainId, scope).map { (exposures, activeEraIndex) -> val minStake = minimumStake( exposures = exposures.values, minimumNominatorBond = minBond, bagListLocator = bagListLocator, - maxNominatorsInValidator = maxNominatorsInValidator, bagListScoreConverter = bagListScoreConverter, bagListSize = bagListSize, maxElectingVoters = maxElectingVoters diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt index 5f746dedcf..b15fe9f596 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt @@ -4,16 +4,19 @@ import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.isTrueOrWarning +import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeProducer +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeDecimalAmount import java.math.BigDecimal class ProfitableActionValidation( val amount: P.() -> BigDecimal, - val fee: P.() -> BigDecimal, + val fee: FeeProducer

, val error: (P) -> E ) : Validation { override suspend fun validate(value: P): ValidationStatus { - val isProfitable = value.fee() < value.amount() + // No matter who paid the fee we check that it is profitable + val isProfitable = fee(value).networkFeeDecimalAmount < value.amount() return isProfitable isTrueOrWarning { error(value) @@ -23,7 +26,7 @@ class ProfitableActionValidation( fun ValidationSystemBuilder.profitableAction( amount: P.() -> BigDecimal, - fee: P.() -> BigDecimal, + fee: FeeProducer

, error: (P) -> E ) { validate(ProfitableActionValidation(amount, fee, error)) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/model/PendingPayoutsStatistics.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/model/PendingPayoutsStatistics.kt index 3c1ec92789..41121f15fb 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/model/PendingPayoutsStatistics.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/model/PendingPayoutsStatistics.kt @@ -14,6 +14,7 @@ data class PendingPayout( val timeLeft: Long, val timeLeftCalculatedAt: Long, val closeToExpire: Boolean, + val pagesToClaim: List, ) { class ValidatorInfo( val address: String, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt index f40d4a20b5..8a0f4b66f5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt @@ -1,6 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.bondExtra import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.nominationPools @@ -11,9 +14,9 @@ import kotlinx.coroutines.withContext interface NominationPoolsBondMoreInteractor { - suspend fun estimateFee(bondMoreAmount: Balance): Balance + suspend fun estimateFee(bondMoreAmount: Balance): Fee - suspend fun bondMore(bondMoreAmount: Balance): Result + suspend fun bondMore(bondMoreAmount: Balance): Result } class RealNominationPoolsBondMoreInteractor( @@ -21,17 +24,17 @@ class RealNominationPoolsBondMoreInteractor( private val stakingSharedState: StakingSharedState, ) : NominationPoolsBondMoreInteractor { - override suspend fun estimateFee(bondMoreAmount: Balance): Balance { + override suspend fun estimateFee(bondMoreAmount: Balance): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stakingSharedState.chain()) { + extrinsicService.estimateFee(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { nominationPools.bondExtra(bondMoreAmount) } } } - override suspend fun bondMore(bondMoreAmount: Balance): Result { + override suspend fun bondMore(bondMoreAmount: Balance): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithSelectedWallet(stakingSharedState.chain()) { + extrinsicService.submitExtrinsic(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { nominationPools.bondExtra(bondMoreAmount) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt index 8039594413..3002b7b804 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt @@ -37,7 +37,7 @@ private fun NominationPoolsBondMoreValidationSystemBuilder.poolIsNotDestroying(f private fun NominationPoolsBondMoreValidationSystemBuilder.enoughAvailableToStakeInPool(factory: PoolAvailableBalanceValidationFactory) { factory.enoughAvailableBalanceToStake( asset = { it.asset }, - fee = { it.fee.fee.amount }, + fee = { it.fee }, amount = { it.amount }, error = NominationPoolsBondMoreValidationFailure::NotEnoughToBond ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt index 900213fdf6..9215becac1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt @@ -1,6 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimRewards +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.NominationPoolBondExtraSource import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.bondExtra @@ -22,9 +25,9 @@ interface NominationPoolsClaimRewardsInteractor { fun pendingRewardsFlow(): Flow - suspend fun estimateFee(shouldRestake: Boolean): Balance + suspend fun estimateFee(shouldRestake: Boolean): Fee - suspend fun claimRewards(shouldRestake: Boolean): Result + suspend fun claimRewards(shouldRestake: Boolean): Result } class RealNominationPoolsClaimRewardsInteractor( @@ -43,17 +46,17 @@ class RealNominationPoolsClaimRewardsInteractor( } } - override suspend fun estimateFee(shouldRestake: Boolean): Balance { + override suspend fun estimateFee(shouldRestake: Boolean): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stakingSharedState.chain()) { + extrinsicService.estimateFee(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { claimRewards(shouldRestake) } } } - override suspend fun claimRewards(shouldRestake: Boolean): Result { + override suspend fun claimRewards(shouldRestake: Boolean): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithSelectedWallet(stakingSharedState.chain()) { + extrinsicService.submitExtrinsic(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { claimRewards(shouldRestake) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt index abfd1ab8f9..d38c4dd878 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt @@ -41,11 +41,11 @@ private fun NominationPoolsClaimRewardsValidationSystemBuilder.enoughToPayFees() sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> NominationPoolsClaimRewardsValidationFailure.NotEnoughBalanceToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) @@ -54,7 +54,7 @@ private fun NominationPoolsClaimRewardsValidationSystemBuilder.enoughToPayFees() private fun NominationPoolsClaimRewardsValidationSystemBuilder.profitableClaim() { profitableAction( amount = { pendingRewards }, - fee = { fee }, + fee = { it.fee }, error = { NominationPoolsClaimRewardsValidationFailure.NonProfitableClaim } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt index ad2271e1e0..123515249c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt @@ -3,11 +3,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claim import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal class NominationPoolsClaimRewardsValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val pendingRewardsPlanks: Balance, val asset: Asset, val chain: Chain diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt index 7f6ff08315..a6d159f54d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt @@ -3,18 +3,21 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.commo import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload.DialogAction import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationFlowActions import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.isTrueOrWarning +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_staking_impl.R import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.NominationPoolsAvailableBalanceResolver import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount +import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeProducer import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -28,7 +31,7 @@ class PoolAvailableBalanceValidationFactory( context(ValidationSystemBuilder) fun enoughAvailableBalanceToStake( asset: (P) -> Asset, - fee: (P) -> Balance, + fee: FeeProducer

, amount: (P) -> BigDecimal, error: (PoolAvailableBalanceValidation.ValidationError.Context) -> E ) { @@ -47,7 +50,7 @@ class PoolAvailableBalanceValidationFactory( class PoolAvailableBalanceValidation( private val poolsAvailableBalanceResolver: NominationPoolsAvailableBalanceResolver, private val asset: (P) -> Asset, - private val fee: (P) -> Balance, + private val fee: FeeProducer

, private val amount: (P) -> BigDecimal, private val error: (ValidationError.Context) -> E ) : Validation { @@ -69,7 +72,7 @@ class PoolAvailableBalanceValidation( val asset = asset(value) val chainAsset = asset.token.configuration - val fee = fee(value) + val fee = fee(value)?.networkFee?.amountByRequestedAccount.orZero() val availableBalance = poolsAvailableBalanceResolver.availableBalanceToStartStaking(asset) val maxToStake = poolsAvailableBalanceResolver.maximumBalanceToStake(asset, fee) val enteredAmount = chainAsset.planksFromAmount(amount(value)) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt index aa94870dee..7e7d5ecce8 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt @@ -2,7 +2,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redee import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.common.utils.isZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.bondedAccountOf import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository @@ -25,13 +27,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.withContext -import java.math.BigInteger interface NominationPoolsRedeemInteractor { fun redeemAmountFlow(poolMember: PoolMember, computationScope: CoroutineScope): Flow - suspend fun estimateFee(poolMember: PoolMember): BigInteger + suspend fun estimateFee(poolMember: PoolMember): Fee suspend fun redeem(poolMember: PoolMember): Result } @@ -58,9 +59,9 @@ class RealNominationPoolsRedeemInteractor( } } - override suspend fun estimateFee(poolMember: PoolMember): BigInteger { + override suspend fun estimateFee(poolMember: PoolMember): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stakingSharedState.chain()) { + extrinsicService.estimateFee(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { redeem(poolMember) } } @@ -71,7 +72,7 @@ class RealNominationPoolsRedeemInteractor( val chain = stakingSharedState.chain() val activeEra = stakingRepository.getActiveEraIndex(chain.id) - extrinsicService.submitExtrinsicWithSelectedWalletV2(chain) { + extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet) { redeem(poolMember) }.map { val totalAfterRedeem = poolMember.totalPointsAfterRedeemAt(activeEra) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt index c231994800..122a82aa81 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt @@ -23,11 +23,11 @@ private fun NominationPoolsRedeemValidationSystemBuilder.enoughToPayFees() { sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> NominationPoolsRedeemValidationFailure.NotEnoughBalanceToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt index b6be071f90..9069b16696 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigDecimal class NominationPoolsRedeemValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, val chain: Chain ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt index 85be0054c3..909e2e2a79 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt @@ -1,6 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbond +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.bondedAccountOf import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState @@ -28,9 +31,9 @@ interface NominationPoolsUnbondInteractor { fun poolMemberStateFlow(computationScope: CoroutineScope): Flow - suspend fun estimateFee(poolMember: PoolMember, amount: Balance): Balance + suspend fun estimateFee(poolMember: PoolMember, amount: Balance): Fee - suspend fun unbond(poolMember: PoolMember, amount: Balance): Result + suspend fun unbond(poolMember: PoolMember, amount: Balance): Result } class RealNominationPoolsUnbondInteractor( @@ -55,21 +58,21 @@ class RealNominationPoolsUnbondInteractor( } } - override suspend fun estimateFee(poolMember: PoolMember, amount: Balance): Balance { + override suspend fun estimateFee(poolMember: PoolMember, amount: Balance): Fee { return withContext(Dispatchers.IO) { val chain = stakingSharedState.chain() - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { nominationPools.unbond(poolMember, amount, chain.id) } } } - override suspend fun unbond(poolMember: PoolMember, amount: Balance): Result { + override suspend fun unbond(poolMember: PoolMember, amount: Balance): Result { return withContext(Dispatchers.IO) { val chain = stakingSharedState.chain() - extrinsicService.submitExtrinsicWithSelectedWallet(stakingSharedState.chain()) { + extrinsicService.submitExtrinsic(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { nominationPools.unbond(poolMember, amount, chain.id) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt index a35dc438b5..2da505dcf3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt @@ -40,11 +40,11 @@ private fun NominationPoolsUnbondValidationSystemBuilder.enoughToPayFees() { sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, leftForFees -> + error = { context -> NominationPoolsUnbondValidationFailure.NotEnoughBalanceToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = leftForFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) @@ -54,7 +54,7 @@ private fun NominationPoolsUnbondValidationSystemBuilder.enoughToUnbond() { sufficientBalance( available = { it.stakedBalance }, amount = { it.amount }, - error = { _, _ -> NominationPoolsUnbondValidationFailure.NotEnoughToUnbond } + error = { NominationPoolsUnbondValidationFailure.NotEnoughToUnbond } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt index 7bbbedb37e..dbdeadec6b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbon import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import java.math.BigDecimal @@ -10,7 +11,7 @@ data class NominationPoolsUnbondValidationPayload( val poolMember: PoolMember, val stakedBalance: BigDecimal, val amount: BigDecimal, - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset, val chain: Chain, val sharedComputationScope: CoroutineScope, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/ParachainStakingRebondInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/ParachainStakingRebondInteractor.kt index c91556cd7e..5d034426a5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/ParachainStakingRebondInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/ParachainStakingRebondInteractor.kt @@ -1,8 +1,10 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.rebond import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.calls.cancelDelegationRequest import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.repository.DelegatorStateRepository @@ -15,7 +17,7 @@ import java.math.BigInteger interface ParachainStakingRebondInteractor { - suspend fun estimateFee(collatorId: AccountId): BigInteger + suspend fun estimateFee(collatorId: AccountId): Fee suspend fun rebondAmount( delegatorState: DelegatorState, @@ -31,8 +33,8 @@ class RealParachainStakingRebondInteractor( private val selectedAssetState: AnySelectedAssetOptionSharedState, ) : ParachainStakingRebondInteractor { - override suspend fun estimateFee(collatorId: AccountId): BigInteger = withContext(Dispatchers.IO) { - extrinsicService.estimateFee(selectedAssetState.chain()) { + override suspend fun estimateFee(collatorId: AccountId): Fee = withContext(Dispatchers.IO) { + extrinsicService.estimateFee(selectedAssetState.chain(), TransactionOrigin.SelectedWallet) { cancelDelegationRequest(collatorId) } } @@ -49,8 +51,8 @@ class RealParachainStakingRebondInteractor( } override suspend fun rebond(collatorId: AccountId): Result<*> = withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(selectedAssetState.chain()) { + extrinsicService.submitAndWatchExtrinsic(selectedAssetState.chain(), TransactionOrigin.SelectedWallet) { cancelDelegationRequest(collatorId) - } + }.awaitInBlock() } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt index 554d02b407..b4f9c8857b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.rebond.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class ParachainStakingRebondValidationPayload( val asset: Asset, - val fee: BigDecimal, + val fee: DecimalFee, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingUnbondValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingUnbondValidationSystem.kt index 7c927aa664..1450f8e8bf 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingUnbondValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingUnbondValidationSystem.kt @@ -9,6 +9,6 @@ fun ValidationSystem.Companion.parachainStakingRebond(): ParachainStakingRebondV sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { _, _ -> ParachainStakingRebondValidationFailure.NotEnoughBalanceToPayFees } + error = { ParachainStakingRebondValidationFailure.NotEnoughBalanceToPayFees } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/ParachainStakingRedeemInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/ParachainStakingRedeemInteractor.kt index 4fda73d76b..49b528db39 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/ParachainStakingRedeemInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/ParachainStakingRedeemInteractor.kt @@ -2,9 +2,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.rede import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.sumByBigInteger +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_api.domain.model.parachain.ScheduledDelegationRequest import io.novafoundation.nova.feature_staking_api.domain.model.parachain.activeBonded @@ -21,7 +23,7 @@ import java.math.BigInteger interface ParachainStakingRedeemInteractor { - suspend fun estimateFee(delegatorState: DelegatorState): BigInteger + suspend fun estimateFee(delegatorState: DelegatorState): Fee suspend fun redeemableAmount(delegatorState: DelegatorState): BigInteger @@ -34,8 +36,8 @@ class RealParachainStakingRedeemInteractor( private val delegatorStateRepository: DelegatorStateRepository, ) : ParachainStakingRedeemInteractor { - override suspend fun estimateFee(delegatorState: DelegatorState): BigInteger = withContext(Dispatchers.Default) { - extrinsicService.estimateFee(delegatorState.chain) { + override suspend fun estimateFee(delegatorState: DelegatorState): Fee = withContext(Dispatchers.Default) { + extrinsicService.estimateFee(delegatorState.chain, TransactionOrigin.SelectedWallet) { redeem(delegatorState) } } @@ -47,11 +49,13 @@ class RealParachainStakingRedeemInteractor( } override suspend fun redeem(delegatorState: DelegatorState): Result = withContext(Dispatchers.Default) { - extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(delegatorState.chain) { + extrinsicService.submitAndWatchExtrinsic(delegatorState.chain, TransactionOrigin.SelectedWallet) { redeem(delegatorState) - }.map { - RedeemConsequences(willKillStash = delegatorState.activeBonded.isZero) } + .awaitInBlock() + .map { + RedeemConsequences(willKillStash = delegatorState.activeBonded.isZero) + } } private suspend fun ExtrinsicBuilder.redeem(delegatorState: DelegatorState) { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt index 10f66b9b8f..a6fa8e5b20 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.redeem.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class ParachainStakingRedeemValidationPayload( val asset: Asset, - val fee: BigDecimal, + val fee: DecimalFee, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingUnbondValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingUnbondValidationSystem.kt index 156f2ff257..4add7a9455 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingUnbondValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingUnbondValidationSystem.kt @@ -9,6 +9,6 @@ fun ValidationSystem.Companion.parachainStakingRedeem(): ParachainStakingRedeemV sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { _, _ -> ParachainStakingRedeemValidationFailure.NotEnoughBalanceToPayFees } + error = { ParachainStakingRedeemValidationFailure.NotEnoughBalanceToPayFees } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/StartParachainStakingInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/StartParachainStakingInteractor.kt index b42dd703ed..c5e39c4578 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/StartParachainStakingInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/StartParachainStakingInteractor.kt @@ -1,9 +1,13 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.start import io.novafoundation.nova.common.utils.parachainStaking +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_api.domain.model.parachain.delegationsCount import io.novafoundation.nova.feature_staking_api.domain.model.parachain.hasDelegation @@ -23,16 +27,14 @@ import jp.co.soramitsu.fearless_utils.runtime.definitions.types.primitives.Fixed import jp.co.soramitsu.fearless_utils.runtime.definitions.types.skipAliases import jp.co.soramitsu.fearless_utils.runtime.metadata.call import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import java.math.BigInteger interface StartParachainStakingInteractor { - suspend fun estimateFee(amount: BigInteger, collatorId: AccountId?): BigInteger + suspend fun estimateFee(amount: BigInteger, collatorId: AccountId?): Fee - suspend fun delegate(amount: BigInteger, collator: AccountId): Result<*> + suspend fun delegate(amount: BigInteger, collator: AccountId): Result suspend fun checkDelegationsLimit(delegatorState: DelegatorState): DelegationsLimit } @@ -47,14 +49,14 @@ class RealStartParachainStakingInteractor( private val candidatesRepository: CandidatesRepository, ) : StartParachainStakingInteractor { - override suspend fun estimateFee(amount: BigInteger, collatorId: AccountId?): BigInteger { + override suspend fun estimateFee(amount: BigInteger, collatorId: AccountId?): Fee { val (chain, chainAsset) = singleAssetSharedState.chainAndAsset() val metaAccount = accountRepository.getSelectedMetaAccount() val accountId = metaAccount.accountIdIn(chain)!! val currentDelegationState = delegatorStateRepository.getDelegationState(chain, chainAsset, accountId) - return extrinsicService.estimateFee(chain) { + return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { if (collatorId != null && currentDelegationState.hasDelegation(collatorId)) { delegatorBondMore( candidate = collatorId, @@ -71,15 +73,15 @@ class RealStartParachainStakingInteractor( } } - override suspend fun delegate(amount: BigInteger, collator: AccountId) = withContext(Dispatchers.Default) { + override suspend fun delegate(amount: BigInteger, collator: AccountId): Result = withContext(Dispatchers.Default) { runCatching { val (chain, chainAsset) = singleAssetSharedState.chainAndAsset() val metaAccount = accountRepository.getSelectedMetaAccount() - val accountId = metaAccount.accountIdIn(chain)!! + val accountId = metaAccount.requireAccountIdIn(chain) val currentDelegationState = delegatorStateRepository.getDelegationState(chain, chainAsset, accountId) - extrinsicService.submitAndWatchExtrinsicAnySuitableWallet(chain, accountId) { + extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { if (currentDelegationState.hasDelegation(collator)) { delegatorBondMore( candidate = collator, @@ -95,9 +97,7 @@ class RealStartParachainStakingInteractor( delegationCount = currentDelegationState.delegationsCount.toBigInteger() ) } - } - .filterIsInstance() - .first() + }.awaitInBlock().getOrThrow() } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt index 0f8303f0c7..e1f7b3fa8d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt @@ -3,11 +3,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.star import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.Collator import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class StartParachainStakingValidationPayload( val amount: BigDecimal, - val fee: BigDecimal, + val fee: DecimalFee, val collator: Collator, val asset: Asset, val delegatorState: DelegatorState, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationSystem.kt index ec46c5e34e..3764a532e2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationSystem.kt @@ -40,7 +40,7 @@ private fun StartParachainStakingValidationSystemBuilder.enoughToPayFees() { sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { _, _ -> StartParachainStakingValidationFailure.NotEnoughBalanceToPayFees } + error = { StartParachainStakingValidationFailure.NotEnoughBalanceToPayFees } ) } @@ -49,7 +49,7 @@ private fun StartParachainStakingValidationSystemBuilder.enoughStakeable() { fee = { it.fee }, available = { it.stakeableAmount() }, amount = { it.amount }, - error = { _, _ -> StartParachainStakingValidationFailure.NotEnoughBalanceToPayFees } + error = { StartParachainStakingValidationFailure.NotEnoughBalanceToPayFees } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/ParachainStakingUnbondInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/ParachainStakingUnbondInteractor.kt index 9032fbefaa..248a208f23 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/ParachainStakingUnbondInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/ParachainStakingUnbondInteractor.kt @@ -1,8 +1,10 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.unbond import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_api.domain.model.parachain.delegationAmountTo import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.calls.scheduleBondLess @@ -20,7 +22,7 @@ import java.math.BigInteger interface ParachainStakingUnbondInteractor { - suspend fun estimateFee(amount: BigInteger, collatorId: AccountId): BigInteger + suspend fun estimateFee(amount: BigInteger, collatorId: AccountId): Fee suspend fun unbond(amount: BigInteger, collator: AccountId): Result<*> @@ -37,10 +39,10 @@ class RealParachainStakingUnbondInteractor( private val collatorsUseCase: CollatorsUseCase, ) : ParachainStakingUnbondInteractor { - override suspend fun estimateFee(amount: BigInteger, collatorId: AccountId): BigInteger { + override suspend fun estimateFee(amount: BigInteger, collatorId: AccountId): Fee { val chain = selectedAssetSharedState.chain() - return extrinsicService.estimateFee(chain) { + return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { unbond(amount, collatorId) } } @@ -48,9 +50,9 @@ class RealParachainStakingUnbondInteractor( override suspend fun unbond(amount: BigInteger, collator: AccountId): Result<*> = withContext(Dispatchers.IO) { val chain = selectedAssetSharedState.chain() - extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(chain) { + extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { unbond(amount, collator) - } + }.awaitInBlock() } override suspend fun canUnbond(fromCollator: AccountId, delegatorState: DelegatorState): Boolean = withContext(Dispatchers.IO) { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt index 8583bbc2a3..a12ede1cfb 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt @@ -2,11 +2,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.unbo import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.Collator import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal data class ParachainStakingUnbondValidationPayload( val amount: BigDecimal, - val fee: BigDecimal, + val fee: DecimalFee, val collator: Collator, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationSystem.kt index 3ed5fb041e..be3ca659ad 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationSystem.kt @@ -28,6 +28,6 @@ fun ValidationSystem.Companion.parachainStakingUnbond( sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { _, _ -> ParachainStakingUnbondValidationFailure.NotEnoughBalanceToPayFees } + error = { ParachainStakingUnbondValidationFailure.NotEnoughBalanceToPayFees } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/YieldBoostInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/YieldBoostInteractor.kt index bd464bfee4..427fcab723 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/YieldBoostInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/YieldBoostInteractor.kt @@ -2,8 +2,10 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yiel import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.OptimalAutomationRequest import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.TuringAutomationTask import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.TuringAutomationTasksRepository @@ -23,7 +25,6 @@ import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import java.math.BigInteger import kotlin.math.roundToLong import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -41,7 +42,7 @@ interface YieldBoostInteractor { suspend fun calculateFee( configuration: YieldBoostConfiguration, activeTasks: List, - ): BigInteger + ): Fee suspend fun setYieldBoost( configuration: YieldBoostConfiguration, @@ -63,10 +64,10 @@ class RealYieldBoostInteractor( override suspend fun calculateFee( configuration: YieldBoostConfiguration, activeTasks: List - ): BigInteger { + ): Fee { val chain = singleAssetSharedState.chain() - return extrinsicService.estimateFee(chain) { + return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { setYieldBoost(chain, activeTasks, configuration) } } @@ -74,9 +75,9 @@ class RealYieldBoostInteractor( override suspend fun setYieldBoost(configuration: YieldBoostConfiguration, activeTasks: List): Result { val chain = singleAssetSharedState.chain() - return extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(chain) { + return extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { setYieldBoost(chain, activeTasks, configuration) - } + }.awaitInBlock() } override suspend fun optimalYieldBoostParameters(delegatorState: DelegatorState, collatorId: AccountId): YieldBoostParameters { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt index 2c3269a157..13306d0f5f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt @@ -20,7 +20,7 @@ class FirstTaskCanExecute( val token = value.asset.token val balanceBeforeTransaction = value.asset.transferable - val balanceAfterTransaction = balanceBeforeTransaction - value.fee + val balanceAfterTransaction = balanceBeforeTransaction - value.fee.networkFeeDecimalAmount val chainId = value.asset.token.configuration.chainId @@ -32,7 +32,7 @@ class FirstTaskCanExecute( return when { taskExecutionFee > balanceAfterTransaction -> YieldBoostValidationFailure.FirstTaskCannotExecute( minimumBalanceRequired = taskExecutionFee, - networkFee = value.fee, + networkFee = value.fee.networkFeeDecimalAmount, availableBalanceBeforeFees = balanceBeforeTransaction, type = EXECUTION_FEE, chainAsset = token.configuration @@ -40,7 +40,7 @@ class FirstTaskCanExecute( threshold > balanceAfterTransaction -> YieldBoostValidationFailure.FirstTaskCannotExecute( minimumBalanceRequired = threshold, - networkFee = value.fee, + networkFee = value.fee.networkFeeDecimalAmount, availableBalanceBeforeFees = balanceBeforeTransaction, type = THRESHOLD, chainAsset = token.configuration diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/ValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/ValidationSystem.kt index 7dc8d65553..19c494f6de 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/ValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/ValidationSystem.kt @@ -16,11 +16,11 @@ fun ValidationSystem.Companion.yieldBoost( sufficientBalance( fee = { it.fee }, available = { it.asset.transferable }, - error = { payload, availableToPayFees -> + error = { context -> YieldBoostValidationFailure.NotEnoughToPayToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = availableToPayFees, - fee = payload.fee + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt index 9e6a3b6785..6211cabd9c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt @@ -4,12 +4,12 @@ import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.commo import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yieldBoost.YieldBoostConfiguration import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yieldBoost.YieldBoostTask import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class YieldBoostValidationPayload( val collator: Collator, val activeTasks: List, val configuration: YieldBoostConfiguration, val asset: Asset, - val fee: BigDecimal, + val fee: DecimalFee, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/payout/PayoutInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/payout/PayoutInteractor.kt index 0d502a006a..35dc6c4166 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/payout/PayoutInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/payout/PayoutInteractor.kt @@ -1,13 +1,19 @@ package io.novafoundation.nova.feature_staking_impl.domain.payout +import io.novafoundation.nova.common.utils.hasCall +import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult +import io.novafoundation.nova.common.utils.staking +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.model.Payout -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.payoutStakers import io.novafoundation.nova.feature_staking_impl.domain.validations.payout.MakePayoutPayload import io.novafoundation.nova.runtime.ext.accountIdOf +import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus +import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.state.chain -import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId +import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.math.BigInteger @@ -17,26 +23,63 @@ class PayoutInteractor( private val extrinsicService: ExtrinsicService ) { - suspend fun estimatePayoutFee(accountAddress: String, payouts: List): BigInteger { + suspend fun estimatePayoutFee(payouts: List): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stakingSharedState.chain()) { - payouts.forEach { - payoutStakers(it.era, it.validatorAddress.toAccountId()) - } + extrinsicService.estimateMultiFee(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { + payoutMultiple(payouts) } } } - suspend fun makePayouts(payload: MakePayoutPayload): Result { + suspend fun makePayouts(payload: MakePayoutPayload): RetriableMultiResult { return withContext(Dispatchers.IO) { val chain = stakingSharedState.chain() val accountId = chain.accountIdOf(payload.originAddress) + val origin = TransactionOrigin.WalletWithAccount(accountId) - extrinsicService.submitExtrinsicWithAnySuitableWallet(chain, accountId) { - payload.payoutStakersCalls.forEach { - payoutStakers(it.era, it.validatorAddress.toAccountId()) - } + extrinsicService.submitMultiExtrinsicAwaitingInclusion(chain, origin) { + payoutMultiple(payload.payouts) } } } + + private fun CallBuilder.payoutMultiple(payouts: List) { + payouts.forEach { payout -> + makePayout(payout) + } + } + + private fun CallBuilder.makePayout(payout: Payout) { + if (runtime.metadata.staking().hasCall("payout_stakers_by_page")) { + payout.pagesToClaim.onEach { page -> + payoutStakersByPage(payout.era, payout.validatorStash.value, page) + } + } else { + // paged payout is not present so we use regular one + payoutStakers(payout.era, payout.validatorStash.value) + } + } + + private fun CallBuilder.payoutStakers(era: BigInteger, validatorId: AccountId) { + addCall( + "Staking", + "payout_stakers", + mapOf( + "validator_stash" to validatorId, + "era" to era + ) + ) + } + + private fun CallBuilder.payoutStakersByPage(era: BigInteger, validatorId: AccountId, page: Int) { + addCall( + "Staking", + "payout_stakers_by_page", + mapOf( + "validator_stash" to validatorId, + "era" to era, + "page" to page.toBigInteger() + ) + ) + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProvider.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProvider.kt index 97ddf5f5cb..1ae6ae3649 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProvider.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProvider.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow class RecommendationSettingsProvider( - maximumRewardedNominators: Int, private val runtimeSnapshot: RuntimeSnapshot, ) { @@ -23,7 +22,7 @@ class RecommendationSettingsProvider( private val customizableFilters = runtimeSnapshot.availableDependents( NotSlashedFilter, HasIdentityFilter, - NotOverSubscribedFilter(maximumRewardedNominators) + NotOverSubscribedFilter ) val allAvailableFilters = alwaysEnabledFilters + customizableFilters diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProviderFactory.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProviderFactory.kt index 0a634b09f8..6fcd06993b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProviderFactory.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/RecommendationSettingsProviderFactory.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_staking_impl.domain.recommendations.setti import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState -import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConstantsRepository import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.getRuntime import kotlinx.coroutines.CoroutineScope @@ -11,7 +10,6 @@ private const val SETTINGS_PROVIDER_KEY = "SETTINGS_PROVIDER_KEY" class RecommendationSettingsProviderFactory( private val computationalCache: ComputationalCache, - private val stakingConstantsRepository: StakingConstantsRepository, private val chainRegistry: ChainRegistry, private val sharedState: StakingSharedState, ) { @@ -20,10 +18,7 @@ class RecommendationSettingsProviderFactory( return computationalCache.useCache(SETTINGS_PROVIDER_KEY, scope) { val chainId = sharedState.chainId() - RecommendationSettingsProvider( - maximumRewardedNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId), - runtimeSnapshot = chainRegistry.getRuntime(chainId) - ) + RecommendationSettingsProvider(chainRegistry.getRuntime(chainId)) } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/filters/NotOverSubscribedFilter.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/filters/NotOverSubscribedFilter.kt index 123c111edc..0011914783 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/filters/NotOverSubscribedFilter.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/recommendations/settings/filters/NotOverSubscribedFilter.kt @@ -3,18 +3,15 @@ package io.novafoundation.nova.feature_staking_impl.domain.recommendations.setti import io.novafoundation.nova.feature_staking_api.domain.model.Validator import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settings.RecommendationFilter -class NotOverSubscribedFilter( - private val maxSubscribers: Int -) : RecommendationFilter { +object NotOverSubscribedFilter : RecommendationFilter { override fun shouldInclude(model: Validator): Boolean { - val electedInfo = model.electedInfo + val isOversubscribed = model.electedInfo?.isOversubscribed - return if (electedInfo != null) { - electedInfo.nominatorStakes.size < maxSubscribers + return if (isOversubscribed != null) { + !isOversubscribed } else { - // inactive validators are considered as non-oversubscribed - true + true // inactive validators are considered as non-oversubscribed } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/setup/ChangeValidatorsInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/setup/ChangeValidatorsInteractor.kt index 5050fc664e..2f7fe5b501 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/setup/ChangeValidatorsInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/setup/ChangeValidatorsInteractor.kt @@ -1,9 +1,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.setup import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.controllerTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.nominate -import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.ext.multiAddressOf import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.state.chain @@ -11,29 +14,27 @@ import jp.co.soramitsu.fearless_utils.extensions.fromHex import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.math.BigInteger class ChangeValidatorsInteractor( private val extrinsicService: ExtrinsicService, private val stakingSharedState: StakingSharedState, ) { - suspend fun estimateFee(validatorAccountIds: List): BigInteger { + suspend fun estimateFee(validatorAccountIds: List, stakingState: StakingState.Stash): Fee { val chain = stakingSharedState.chain() - return extrinsicService.estimateFee(chain) { + return extrinsicService.estimateFee(chain, stakingState.controllerTransactionOrigin()) { formExtrinsic(chain, validatorAccountIds) } } suspend fun changeValidators( - controllerAddress: String, + stakingState: StakingState.Stash, validatorAccountIds: List - ): Result = withContext(Dispatchers.Default) { + ): Result = withContext(Dispatchers.Default) { val chain = stakingSharedState.chain() - val accountId = chain.accountIdOf(controllerAddress) - extrinsicService.submitExtrinsicWithAnySuitableWallet(chain, accountId) { + extrinsicService.submitExtrinsic(chain, stakingState.controllerTransactionOrigin()) { formExtrinsic(chain, validatorAccountIds) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/bond/BondMoreInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/bond/BondMoreInteractor.kt index 58248e724d..13acfaba3b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/bond/BondMoreInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/bond/BondMoreInteractor.kt @@ -1,6 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.bond +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.stashTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.bondMore import io.novafoundation.nova.runtime.ext.accountIdOf @@ -14,22 +19,22 @@ class BondMoreInteractor( private val stakingSharedState: StakingSharedState, ) { - suspend fun estimateFee(amount: BigInteger): BigInteger { + suspend fun estimateFee(amount: BigInteger, stakingState: StakingState.Stash): Fee { return withContext(Dispatchers.IO) { val chain = stakingSharedState.chain() - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, stakingState.stashTransactionOrigin()) { bondMore(amount) } } } - suspend fun bondMore(accountAddress: String, amount: BigInteger): Result { + suspend fun bondMore(stashAddress: String, amount: BigInteger): Result { return withContext(Dispatchers.IO) { val chain = stakingSharedState.chain() - val accountId = chain.accountIdOf(accountAddress) + val accountId = chain.accountIdOf(stashAddress) - extrinsicService.submitExtrinsicWithAnySuitableWallet(chain, accountId) { + extrinsicService.submitExtrinsic(chain, accountId.intoOrigin()) { bondMore(amount) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/controller/ControllerInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/controller/ControllerInteractor.kt index 969b3f3b81..690a2e0e0c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/controller/ControllerInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/controller/ControllerInteractor.kt @@ -1,6 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.controller +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.stashTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.setController import io.novafoundation.nova.feature_staking_impl.data.repository.ControllersDeprecationStage @@ -10,7 +15,6 @@ import io.novafoundation.nova.runtime.ext.multiAddressOf import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.math.BigInteger class ControllerInteractor( private val extrinsicService: ExtrinsicService, @@ -26,22 +30,22 @@ class ControllerInteractor( } } - suspend fun estimateFee(controllerAccountAddress: String): BigInteger { + suspend fun estimateFee(controllerAccountAddress: String, stakingState: StakingState.Stash): Fee { return withContext(Dispatchers.IO) { val chain = sharedStakingSate.chain() - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, stakingState.stashTransactionOrigin()) { setController(chain.multiAddressOf(controllerAccountAddress)) } } } - suspend fun setController(stashAccountAddress: String, controllerAccountAddress: String): Result { + suspend fun setController(stashAccountAddress: String, controllerAccountAddress: String): Result { return withContext(Dispatchers.IO) { val chain = sharedStakingSate.chain() val accountId = chain.accountIdOf(stashAccountAddress) - extrinsicService.submitExtrinsicWithAnySuitableWallet(chain, accountId) { + extrinsicService.submitExtrinsic(chain, accountId.intoOrigin()) { setController(chain.multiAddressOf(controllerAccountAddress)) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rebond/RebondInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rebond/RebondInteractor.kt index 23d947e201..c5859a02ac 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rebond/RebondInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rebond/RebondInteractor.kt @@ -1,7 +1,10 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.rebond import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.controllerTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.rebond import io.novafoundation.nova.runtime.state.chain @@ -14,19 +17,19 @@ class RebondInteractor( private val sharedStakingSate: StakingSharedState ) { - suspend fun estimateFee(amount: BigInteger): BigInteger { + suspend fun estimateFee(amount: BigInteger, stakingState: StakingState.Stash): Fee { return withContext(Dispatchers.IO) { val chain = sharedStakingSate.chain() - extrinsicService.estimateFee(chain) { + extrinsicService.estimateFee(chain, stakingState.controllerTransactionOrigin()) { rebond(amount) } } } - suspend fun rebond(stashState: StakingState.Stash, amount: BigInteger): Result { + suspend fun rebond(stashState: StakingState.Stash, amount: BigInteger): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithAnySuitableWallet(stashState.chain, stashState.controllerId) { + extrinsicService.submitExtrinsic(stashState.chain, stashState.controllerTransactionOrigin()) { rebond(amount) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/redeem/RedeemInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/redeem/RedeemInteractor.kt index bb5e513c05..d2151b8bb2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/redeem/RedeemInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/redeem/RedeemInteractor.kt @@ -2,9 +2,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.redeem import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_api.domain.model.numberOfSlashingSpans import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.controllerTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.withdrawUnbonded import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import kotlinx.coroutines.Dispatchers @@ -16,9 +18,9 @@ class RedeemInteractor( private val stakingRepository: StakingRepository, ) { - suspend fun estimateFee(stakingState: StakingState.Stash): BigInteger { + suspend fun estimateFee(stakingState: StakingState.Stash): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stakingState.chain) { + extrinsicService.estimateFee(stakingState.chain, stakingState.controllerTransactionOrigin()) { withdrawUnbonded(getSlashingSpansNumber(stakingState)) } } @@ -26,7 +28,7 @@ class RedeemInteractor( suspend fun redeem(stakingState: StakingState.Stash, asset: Asset): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithAnySuitableWallet(stakingState.chain, stakingState.controllerId) { + extrinsicService.submitExtrinsic(stakingState.chain, stakingState.controllerTransactionOrigin()) { withdrawUnbonded(getSlashingSpansNumber(stakingState)) }.map { RedeemConsequences(willKillStash = asset.isRedeemingAll()) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rewardDestination/ChangeRewardDestinationInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rewardDestination/ChangeRewardDestinationInteractor.kt index 47cbe159bb..4e7d08eece 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rewardDestination/ChangeRewardDestinationInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/rewardDestination/ChangeRewardDestinationInteractor.kt @@ -1,12 +1,14 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.rewardDestination import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.controllerTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.setPayee import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.math.BigInteger class ChangeRewardDestinationInteractor( private val extrinsicService: ExtrinsicService @@ -15,8 +17,8 @@ class ChangeRewardDestinationInteractor( suspend fun estimateFee( stashState: StakingState.Stash, rewardDestination: RewardDestination, - ): BigInteger = withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stashState.chain) { + ): Fee = withContext(Dispatchers.IO) { + extrinsicService.estimateFee(stashState.chain, stashState.controllerTransactionOrigin()) { setPayee(rewardDestination) } } @@ -24,8 +26,8 @@ class ChangeRewardDestinationInteractor( suspend fun changeRewardDestination( stashState: StakingState.Stash, rewardDestination: RewardDestination, - ): Result = withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithAnySuitableWallet(stashState.chain, stashState.controllerId) { + ): Result = withContext(Dispatchers.IO) { + extrinsicService.submitExtrinsic(stashState.chain, stashState.controllerTransactionOrigin()) { setPayee(rewardDestination) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt index f12495ba4a..71d4c609ec 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt @@ -1,7 +1,8 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.common +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.extrinsic.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_staking_impl.data.chain @@ -25,7 +26,7 @@ class RealStartMultiStakingInteractor( override suspend fun calculateFee(selection: StartMultiStakingSelection): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFeeV2(selection.stakingOption.chain) { + extrinsicService.estimateFee(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) { startStaking(selection) } } @@ -33,9 +34,9 @@ class RealStartMultiStakingInteractor( override suspend fun startStaking(selection: StartMultiStakingSelection): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithSelectedWalletAndWaitBlockInclusion(selection.stakingOption.chain) { + extrinsicService.submitAndWatchExtrinsic(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) { startStaking(selection) - } + }.awaitInBlock() } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt index 0364d22c13..09ad392904 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.common. import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_staking_impl.data.asset import io.novafoundation.nova.feature_staking_impl.data.chain import io.novafoundation.nova.feature_staking_impl.data.stakingType @@ -18,7 +19,7 @@ class AvailableBalanceGapValidation( override suspend fun validate(value: StartMultiStakingValidationPayload): ValidationStatus { val amount = value.selection.stake val stakingOption = value.selection.stakingOption - val fee = value.fee.fee.amount + val fee = value.fee.networkFee.amountByRequestedAccount val maxToStakeWithMinStakes = candidates.map { val maximumToStake = it.maximumToStake(value.asset, fee) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt index 0ec0861cd7..8f236cbe44 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt @@ -111,14 +111,14 @@ private class DirectStakingProperties( private fun StartMultiStakingValidationSystemBuilder.enoughAvailableToStake() { sufficientBalance( - fee = { it.fee.networkFeeDecimalAmount }, + fee = { it.fee }, available = { it.amountOf(availableBalance(it.asset)) }, amount = { it.amountOf(it.selection.stake) }, - error = { payload, availableToPayFees -> + error = { context -> StartMultiStakingValidationFailure.NotEnoughToPayFees( - chainAsset = payload.asset.token.configuration, - maxUsable = availableToPayFees, - fee = payload.fee.networkFeeDecimalAmount + chainAsset = context.payload.asset.token.configuration, + maxUsable = context.availableToPayFees, + fee = context.fee ) } ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt index a934dc9c8c..e0a5743a11 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt @@ -82,7 +82,7 @@ private class NominationPoolStakingProperties( poolAvailableBalanceValidationFactory.enoughAvailableBalanceToStake( asset = { it.asset }, - fee = { it.fee.fee.amount }, + fee = { it.fee }, amount = { it.selection.stakeAmount() }, error = StartMultiStakingValidationFailure::PoolAvailableBalance ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/ManualMultiStakingSelectionType.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/ManualMultiStakingSelectionType.kt index a5887a5956..f59e5e242c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/ManualMultiStakingSelectionType.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/ManualMultiStakingSelectionType.kt @@ -38,8 +38,8 @@ class ManualMultiStakingSelectionType( sufficientBalance( available = { it.amountOf(availableBalance(it.asset)) }, amount = { it.amountOf(it.selection.stake) }, - error = { _, _ -> StartMultiStakingValidationFailure.NotEnoughAvailableToStake }, - fee = { it.fee.decimalAmount } + error = { StartMultiStakingValidationFailure.NotEnoughAvailableToStake }, + fee = { it.fee } ) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/unbond/UnbondInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/unbond/UnbondInteractor.kt index 1049b7ff15..36682208a1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/unbond/UnbondInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/unbond/UnbondInteractor.kt @@ -1,10 +1,13 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.unbond -import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.sumByBigInteger import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.controllerTransactionOrigin import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.chill import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls.unbond @@ -15,8 +18,7 @@ import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combineTransform -import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import java.math.BigInteger @@ -32,9 +34,9 @@ class UnbondInteractor( stashState: StakingState.Stash, currentBondedBalance: BigInteger, amount: BigInteger - ): BigInteger { + ): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(stashState.chain) { + extrinsicService.estimateFee(stashState.chain, stashState.controllerTransactionOrigin()) { constructUnbondExtrinsic(stashState, currentBondedBalance, amount) } } @@ -44,31 +46,26 @@ class UnbondInteractor( stashState: StakingState.Stash, currentBondedBalance: BigInteger, amount: BigInteger - ): Result { + ): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicWithAnySuitableWallet(stashState.chain, stashState.controllerId) { + extrinsicService.submitExtrinsic(stashState.chain, stashState.controllerTransactionOrigin()) { constructUnbondExtrinsic(stashState, currentBondedBalance, amount) } } } fun unbondingsFlow(stakingState: StakingState.Stash, sharedComputationScope: CoroutineScope): Flow { - return flowOfAll { - combineTransform( - stakingRepository.ledgerFlow(stakingState), - stakingRepository.observeActiveEraIndex(stakingState.chain.id) - ) { ledger, activeEraIndex -> - - val unbondingsFlow = stakingSharedComputation.constructUnbondingList( - eraRedeemables = ledger.unlocking, - activeEra = activeEraIndex, - stakingOption = stakingSharedState.selectedOption(), - sharedComputationScope = sharedComputationScope - ).map { unbondings -> - Unbondings.from(unbondings, rebondPossible = true) - } - - emitAll(unbondingsFlow) + return combineToPair( + stakingRepository.ledgerFlow(stakingState), + stakingRepository.observeActiveEraIndex(stakingState.chain.id) + ).flatMapLatest { (ledger, activeEraIndex) -> + stakingSharedComputation.constructUnbondingList( + eraRedeemables = ledger.unlocking, + activeEra = activeEraIndex, + stakingOption = stakingSharedState.selectedOption(), + sharedComputationScope = sharedComputationScope + ).map { unbondings -> + Unbondings.from(unbondings, rebondPossible = true) } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/AccountRequiredValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/AccountRequiredValidation.kt index 3327c3be35..29104f1dee 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/AccountRequiredValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/AccountRequiredValidation.kt @@ -19,7 +19,7 @@ class AccountRequiredValidation( val accountAddress = accountAddressExtractor(value) val chain = sharedState.chain() - return if (accountRepository.isAccountExists(chain.accountIdOf(accountAddress))) { + return if (accountRepository.isAccountExists(chain.accountIdOf(accountAddress), chain.id)) { ValidationStatus.Valid() } else { ValidationStatus.NotValid(DefaultFailureLevel.ERROR, errorProducer(accountAddress)) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt index 670312999d..3dd0939360 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt @@ -1,11 +1,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.bond import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class BondMoreValidationPayload( val stashAddress: String, - val fee: BigDecimal, + val fee: DecimalFee, val amount: BigDecimal, val stashAsset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/Declarations.kt index 7f18f255b9..d854dafe11 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/Declarations.kt @@ -21,7 +21,7 @@ private fun BondMoreValidationSystemBuilder.enoughToPayFees() { sufficientBalance( fee = { it.fee }, available = { it.stashAsset.transferable }, - error = { _, _ -> BondMoreValidationFailure.NOT_ENOUGH_TO_PAY_FEES } + error = { BondMoreValidationFailure.NOT_ENOUGH_TO_PAY_FEES } ) } @@ -30,7 +30,7 @@ private fun BondMoreValidationSystemBuilder.enoughStakeable() { fee = { it.fee }, available = { it.stashAsset.stakeable }, amount = { it.amount }, - error = { _, _ -> BondMoreValidationFailure.NOT_ENOUGH_STAKEABLE } + error = { BondMoreValidationFailure.NOT_ENOUGH_STAKEABLE } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/controller/SetControllerValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/controller/SetControllerValidationPayload.kt index a2376a7d21..4d35cf660c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/controller/SetControllerValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/controller/SetControllerValidationPayload.kt @@ -1,10 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.controller +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class SetControllerValidationPayload( val stashAddress: String, val controllerAddress: String, - val fee: BigDecimal, + val fee: DecimalFee, val transferable: BigDecimal ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt index c6515988d7..d08cf3ca22 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt @@ -1,15 +1,14 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.payout +import io.novafoundation.nova.feature_staking_impl.data.model.Payout import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal -import java.math.BigInteger class MakePayoutPayload( val originAddress: String, - val fee: BigDecimal, + val fee: DecimalFee, val totalReward: BigDecimal, val asset: Asset, - val payoutStakersCalls: List -) { - data class PayoutStakersPayload(val era: BigInteger, val validatorAddress: String) -} + val payouts: List +) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt index e412658f94..aacb003041 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt @@ -1,10 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.rebond import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class RebondValidationPayload( val controllerAsset: Asset, - val fee: BigDecimal, + val fee: DecimalFee, val rebondAmount: BigDecimal ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt index f3550b71be..f8cf40c06f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.reedeem import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class RedeemValidationPayload( - val fee: BigDecimal, + val fee: DecimalFee, val asset: Asset ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt index d79e06cfaf..afa7a67d6e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt @@ -1,10 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.rewardDestination import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal class RewardDestinationValidationPayload( val availableControllerBalance: BigDecimal, - val fee: BigDecimal, + val fee: DecimalFee, val stashState: StakingState.Stash ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/Declarations.kt index 13f78ef77c..c50546c49e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/Declarations.kt @@ -29,6 +29,6 @@ private fun SetupStakingValidationSystemBuilder.enoughToPayFee() { sufficientBalance( fee = { it.maxFee }, available = { it.controllerAsset.transferable }, - error = { _, _ -> SetupStakingValidationFailure.CannotPayFee } + error = { SetupStakingValidationFailure.CannotPayFee } ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt index 42fb697ded..47837216d2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.setup import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import java.math.BigDecimal +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class SetupStakingPayload( - val maxFee: BigDecimal, + val maxFee: DecimalFee, val controllerAsset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt index 17ecb18f34..a2b4b3eec6 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt @@ -2,11 +2,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.unbond import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal data class UnbondValidationPayload( val stash: StakingState.Stash, - val fee: BigDecimal, + val fee: DecimalFee, val amount: BigDecimal, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt index 2098f8399e..971d742164 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt @@ -64,7 +64,7 @@ class ValidatorProvider( ownStake = it.own, nominatorStakes = it.others, apy = rewardCalculator.getApyFor(accountIdHex), - isOversubscribed = it.others.size > maxNominators + isOversubscribed = maxNominators != null && it.others.size > maxNominators ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/current/CurrentValidatorsInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/current/CurrentValidatorsInteractor.kt index 7ac96bc1db..558a328eac 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/current/CurrentValidatorsInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/current/CurrentValidatorsInteractor.kt @@ -82,7 +82,7 @@ class CurrentValidatorsInteractor( val userNominationRank = userNominationIndex + 1 - val willBeRewarded = userNominationRank < maxRewardedNominators + val willBeRewarded = maxRewardedNominators == null || userNominationRank < maxRewardedNominators Status.Active(nomination = userIndividualExposure.value, willUserBeRewarded = willBeRewarded) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/StakingRouter.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/StakingRouter.kt index 36c001abcb..7682f0bc93 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/StakingRouter.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/StakingRouter.kt @@ -74,7 +74,7 @@ interface StakingRouter { fun openConfirmRewardDestination(payload: ConfirmRewardDestinationPayload) - fun openAccountDetails(metaAccountId: Long) + fun openWalletDetails(metaAccountId: Long) fun openRebag() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt index 889607d1db..9008950530 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt @@ -25,6 +25,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.bagList.rebag.mo import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanksRange import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.state.chain @@ -111,8 +112,12 @@ class RebagViewModel( private fun rebagIfValid() { launch { - val fee = originFeeMixin.awaitFee() - val validationPayload = RebagValidationPayload(fee, stashAsset.first()) + _showNextProgress.value = true + + val validationPayload = RebagValidationPayload( + fee = originFeeMixin.awaitDecimalFee(), + asset = stashAsset.first() + ) validationExecutor.requireValid( validationSystem = validationSystem, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/SetupStakingSharedState.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/SetupStakingSharedState.kt index e0b1dea75c..cf29bb33c5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/SetupStakingSharedState.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/SetupStakingSharedState.kt @@ -11,17 +11,33 @@ sealed class SetupStakingProcess { fun next( activeStake: Balance, - validators: List, - selectionMethod: ReadyToSubmit.SelectionMethod - ): SetupStakingProcess { - return ReadyToSubmit(activeStake, validators, selectionMethod) + currentlyActiveValidators: List, + ): ChoosingValidators { + return ChoosingValidators(currentlySelectedValidators = currentlyActiveValidators, activeStake) } } - class ReadyToSubmit( + class ChoosingValidators( + val currentlySelectedValidators: List, val activeStake: Balance, - val validators: List, - val selectionMethod: SelectionMethod + ) : SetupStakingProcess() { + + fun next( + newValidators: List, + selectionMethod: ReadyToSubmit.SelectionMethod, + ) = ReadyToSubmit( + activeStake = activeStake, + newValidators = newValidators, + selectionMethod = selectionMethod, + currentlySelectedValidators = currentlySelectedValidators + ) + } + + data class ReadyToSubmit( + val activeStake: Balance, + val newValidators: List, + val selectionMethod: SelectionMethod, + val currentlySelectedValidators: List, ) : SetupStakingProcess() { enum class SelectionMethod { @@ -31,13 +47,11 @@ sealed class SetupStakingProcess { fun changeValidators( newValidators: List, selectionMethod: SelectionMethod - ) = ReadyToSubmit(activeStake, newValidators, selectionMethod) + ) = copy(newValidators = newValidators, selectionMethod = selectionMethod) - fun previous(): Initial { - return Initial + fun previous(): ChoosingValidators { + return ChoosingValidators(currentlySelectedValidators, activeStake) } - - fun reset() = Initial } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/currentStakeTargets/CurrentStakeTargetsFragment.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/currentStakeTargets/CurrentStakeTargetsFragment.kt index 27302473b5..478105c208 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/currentStakeTargets/CurrentStakeTargetsFragment.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/common/currentStakeTargets/CurrentStakeTargetsFragment.kt @@ -62,7 +62,7 @@ abstract class CurrentStakeTargetsFragment : B viewModel.warningFlow.observe { if (it != null) { currentValidatorsOversubscribedMessage.makeVisible() - currentValidatorsOversubscribedMessage.setText(it) + currentValidatorsOversubscribedMessage.setMessage(it) } else { currentValidatorsOversubscribedMessage.makeGone() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mappers/Validator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mappers/Validator.kt index 8d6732ddb8..dc8c372ac2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mappers/Validator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mappers/Validator.kt @@ -202,7 +202,7 @@ suspend fun mapValidatorDetailsParcelToValidatorDetailsModel( val nominatorsCount = stake.stakers.size val rewardsWithLabel = displayConfig.rewardSuffix.format(resourceManager, stake.rewards) - val formattedMaxStakers = displayConfig.rewardedStakersPerStakeTarget.format() + val formattedMaxStakers = displayConfig.rewardedStakersPerStakeTarget?.format() ValidatorStakeModel( status = ValidatorStakeModel.Status( @@ -214,7 +214,7 @@ suspend fun mapValidatorDetailsParcelToValidatorDetailsModel( totalStake = totalStakeModel, minimumStake = stake.minimumStake?.let { mapAmountToAmountModel(it, asset) }, nominatorsCount = nominatorsCount.format(), - maxNominations = resourceManager.getString(R.string.staking_nominations_rewarded_format, formattedMaxStakers), + maxNominations = formattedMaxStakers?.let { resourceManager.getString(R.string.staking_nominations_rewarded_format, it) }, apy = rewardsWithLabel ) ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt index 0858c217cf..e87d40133b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt @@ -71,7 +71,7 @@ class NominationPoolsConfirmBondMoreViewModel( .shareInBackground() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt index 8e8c0e1c3a..9c9b329d13 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt @@ -19,6 +19,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimR import io.novafoundation.nova.feature_staking_impl.presentation.NominationPoolsRouter import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel @@ -95,10 +96,12 @@ class NominationPoolsClaimRewardsViewModel( } private fun claimRewardsIfValid() = launch { + _showNextProgress.value = true + val shouldRestake = shouldRestakeInput.first() val payload = NominationPoolsClaimRewardsValidationPayload( - fee = feeLoaderMixin.awaitFee(), + fee = feeLoaderMixin.awaitDecimalFee(), pendingRewardsPlanks = pendingRewards.first(), asset = assetFlow.first(), chain = stakingSharedState.chain() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt index 544de66a8a..94ee117ac1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt @@ -20,6 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel @@ -102,11 +103,11 @@ class NominationPoolsRedeemViewModel( } private fun redeemIfValid() = launch { - val asset = assetFlow.first() + _showNextProgress.value = true val payload = NominationPoolsRedeemValidationPayload( - fee = feeLoaderMixin.awaitFee(), - asset = asset, + fee = feeLoaderMixin.awaitDecimalFee(), + asset = assetFlow.first(), chain = stakingSharedState.chain() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondPayload.kt index e039902a87..6907a65680 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondPayload.kt @@ -1,11 +1,12 @@ package io.novafoundation.nova.feature_staking_impl.presentation.nominationPools.unbond.confirm import android.os.Parcelable +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @Parcelize class NominationPoolsConfirmUnbondPayload( val amount: BigDecimal, - val fee: BigDecimal, + val fee: FeeParcelModel, ) : Parcelable diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt index cd425281ec..d54df9df77 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt @@ -24,6 +24,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.Flow @@ -49,6 +50,8 @@ class NominationPoolsConfirmUnbondViewModel( ExternalActions by externalActions, Validatable by validationExecutor { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val _showNextProgress = MutableStateFlow(false) val showNextProgress: Flow = _showNextProgress @@ -66,7 +69,7 @@ class NominationPoolsConfirmUnbondViewModel( .shareInBackground() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(payload.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } @@ -102,7 +105,7 @@ class NominationPoolsConfirmUnbondViewModel( val stakedBalance = asset.token.amountFromPlanks(stakedBalance.first()) val payload = NominationPoolsUnbondValidationPayload( - fee = payload.fee, + fee = decimalFee, amount = payload.amount, poolMember = poolMemberFlow.first(), asset = asset, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt index f0269876fe..9eafdda2f2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt @@ -21,8 +21,10 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.MutableStateFlow @@ -110,7 +112,7 @@ class NominationPoolsSetupUnbondViewModel( val stakedBalance = asset.token.amountFromPlanks(stakedBalance.first()) val payload = NominationPoolsUnbondValidationPayload( - fee = originFeeMixin.awaitFee(), + fee = originFeeMixin.awaitDecimalFee(), poolMember = poolMemberFlow.first(), stakedBalance = stakedBalance, asset = asset, @@ -134,7 +136,7 @@ class NominationPoolsSetupUnbondViewModel( private fun openConfirm(validationPayload: NominationPoolsUnbondValidationPayload) = launch { val confirmPayload = NominationPoolsConfirmUnbondPayload( amount = validationPayload.amount, - fee = validationPayload.fee + fee = mapFeeToParcel(validationPayload.fee) ) router.openConfirmUnbond(confirmPayload) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt index 44fc12b2e8..d11875b3bf 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt @@ -25,6 +25,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.StakeTargetDetailsPayload import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -36,7 +37,6 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal class ParachainStakingRebondViewModel( private val router: ParachainStakingRouter, @@ -125,21 +125,21 @@ class ParachainStakingRebondViewModel( router.openCollatorDetails(StakeTargetDetailsPayload.parachain(parcel, collatorsUseCase)) } - private fun sendTransactionIfValid() = requireFee { fee -> - launch { - val payload = ParachainStakingRebondValidationPayload( - fee = fee, - asset = assetFlow.first() - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { parachainStakingRebondValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - sendTransaction() - } + private fun sendTransactionIfValid() = launch { + _showNextProgress.value = true + + val payload = ParachainStakingRebondValidationPayload( + fee = feeLoaderMixin.awaitDecimalFee(), + asset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { parachainStakingRebondValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction() } } @@ -154,9 +154,4 @@ class ParachainStakingRebondViewModel( _showNextProgress.value = false } - - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt index 717a18de79..c816c8c289 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt @@ -20,6 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.redee import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState @@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.math.BigDecimal class ParachainStakingRedeemViewModel( private val router: StakingRouter, @@ -105,21 +105,21 @@ class ParachainStakingRedeemViewModel( externalActions.showExternalActions(ExternalActions.Type.Address(address), selectedAssetState.chain()) } - private fun sendTransactionIfValid() = requireFee { fee -> - launch { - val payload = ParachainStakingRedeemValidationPayload( - fee = fee, - asset = assetFlow.first() - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { parachainStakingRedeemValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - sendTransaction() - } + private fun sendTransactionIfValid() = launch { + _showNextProgress.value = true + + val payload = ParachainStakingRedeemValidationPayload( + fee = feeLoaderMixin.awaitDecimalFee(), + asset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { parachainStakingRedeemValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction() } } @@ -134,9 +134,4 @@ class ParachainStakingRedeemViewModel( _showNextProgress.value = false } - - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt index 9a845b9243..ae57d4b17c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt @@ -39,6 +39,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.validators.detai import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -50,7 +51,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal class ConfirmStartParachainStakingViewModel( private val parachainStakingRouter: ParachainStakingRouter, @@ -77,6 +77,8 @@ class ConfirmStartParachainStakingViewModel( FeeLoaderMixin by feeLoaderMixin, ExternalActions by externalActions { + private val decimalFee = mapFeeFromParcel(payload.fee) + // Take state only once since subscribing to it might cause switch to Delegator state while waiting for tx confirmation private val delegatorStateFlow = flowOf { delegatorStateUseCase.currentDelegatorState() } .shareInBackground() @@ -149,27 +151,25 @@ class ConfirmStartParachainStakingViewModel( } private fun setInitialFee() = launch { - feeLoaderMixin.setFee(payload.fee) + feeLoaderMixin.setFee(decimalFee.genericFee) } - private fun sendTransactionIfValid() = requireFee { _ -> - launch { - val payload = StartParachainStakingValidationPayload( - amount = payload.amount, - fee = payload.fee, - collator = collator(), - asset = assetFlow.first(), - delegatorState = delegatorStateFlow.first(), - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { startParachainStakingValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - sendTransaction() - } + private fun sendTransactionIfValid() = launch { + val payload = StartParachainStakingValidationPayload( + amount = payload.amount, + fee = decimalFee, + collator = collator(), + asset = assetFlow.first(), + delegatorState = delegatorStateFlow.first(), + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { startParachainStakingValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction() } } @@ -203,9 +203,4 @@ class ConfirmStartParachainStakingViewModel( StartParachainStakingMode.BOND_MORE -> parachainStakingRouter.returnToStakingMain() } } - - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/model/ConfirmStartParachainStakingPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/model/ConfirmStartParachainStakingPayload.kt index 387645ca83..b2a44d5ba5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/model/ConfirmStartParachainStakingPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/model/ConfirmStartParachainStakingPayload.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.presentation.parachainStakin import android.os.Parcelable import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking.collator.select.model.CollatorParcelModel import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking.start.common.StartParachainStakingMode +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @@ -10,6 +11,6 @@ import java.math.BigDecimal class ConfirmStartParachainStakingPayload( val collator: CollatorParcelModel, val amount: BigDecimal, - val fee: BigDecimal, + val fee: FeeParcelModel, val flowMode: StartParachainStakingMode ) : Parcelable diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt index 324aadde55..f7fc9dc156 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt @@ -43,7 +43,10 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import jp.co.soramitsu.fearless_utils.extensions.fromHex import kotlinx.coroutines.Dispatchers @@ -263,34 +266,34 @@ class StartParachainStakingViewModel( .launchIn(this) } - private fun maybeGoToNext() = requireFee { fee -> - launch { - val collator = selectedCollatorFlow.first() ?: return@launch - val amount = amountChooserMixin.amount.first() + private fun maybeGoToNext() = launch { + validationInProgress.value = true - val payload = StartParachainStakingValidationPayload( - amount = amount, - fee = fee, - asset = assetFlow.first(), - collator = collator, - delegatorState = currentDelegatorStateFlow.first(), - ) + val collator = selectedCollatorFlow.first() ?: return@launch + val amount = amountChooserMixin.amount.first() - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { startParachainStakingValidationFailure(it, resourceManager) }, - progressConsumer = validationInProgress.progressConsumer() - ) { - validationInProgress.value = false + val payload = StartParachainStakingValidationPayload( + amount = amount, + fee = feeLoaderMixin.awaitDecimalFee(), + asset = assetFlow.first(), + collator = collator, + delegatorState = currentDelegatorStateFlow.first(), + ) - goToNextStep(fee = fee, amount = amount, collator = collator) - } + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { startParachainStakingValidationFailure(it, resourceManager) }, + progressConsumer = validationInProgress.progressConsumer() + ) { + validationInProgress.value = false + + goToNextStep(fee = it.fee, amount = amount, collator = collator) } } private fun goToNextStep( - fee: BigDecimal, + fee: DecimalFee, amount: BigDecimal, collator: Collator, ) = launch { @@ -298,16 +301,11 @@ class StartParachainStakingViewModel( ConfirmStartParachainStakingPayload( collator = mapCollatorToCollatorParcelModel(collator), amount = amount, - fee = fee, + fee = mapFeeToParcel(fee), flowMode = payload.flowMode ) } router.openConfirmStartStaking(payload) } - - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt index 59441612d8..16c2b0d4b1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt @@ -32,6 +32,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.validators.detai import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -66,6 +67,8 @@ class ParachainStakingUnbondConfirmViewModel( FeeLoaderMixin by feeLoaderMixin, ExternalActions by externalActions { + private val decimalFee = mapFeeFromParcel(payload.fee) + val hintsMixin = hintsMixinFactory.create(coroutineScope = this) private val assetFlow = assetUseCase.currentAssetFlow() @@ -125,13 +128,13 @@ class ParachainStakingUnbondConfirmViewModel( } private fun setInitialFee() = launch { - feeLoaderMixin.setFee(payload.fee) + feeLoaderMixin.setFee(decimalFee.genericFee) } private fun sendTransactionIfValid() = launch { val payload = ParachainStakingUnbondValidationPayload( amount = payload.amount, - fee = payload.fee, + fee = decimalFee, collator = collator(), asset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/model/ParachainStakingUnbondConfirmPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/model/ParachainStakingUnbondConfirmPayload.kt index 85092bcb65..e2a0805eee 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/model/ParachainStakingUnbondConfirmPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/model/ParachainStakingUnbondConfirmPayload.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.presentation.parachainStakin import android.os.Parcelable import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking.collator.select.model.CollatorParcelModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @@ -9,5 +10,5 @@ import java.math.BigDecimal class ParachainStakingUnbondConfirmPayload( val collator: CollatorParcelModel, val amount: BigDecimal, - val fee: BigDecimal + val fee: FeeParcelModel ) : Parcelable diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt index 3ad56531d3..82e233197c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt @@ -36,7 +36,10 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.transferableAmountModel import jp.co.soramitsu.fearless_utils.extensions.fromHex @@ -206,31 +209,31 @@ class ParachainStakingUnbondViewModel( } } - private fun maybeGoToNext() = requireFee { fee -> - launch { - val payload = ParachainStakingUnbondValidationPayload( - amount = amountChooserMixin.amount.first(), - fee = fee, - asset = assetFlow.first(), - collator = selectedCollatorFlow.first() - ) + private fun maybeGoToNext() = launch { + validationInProgress.value = true - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { parachainStakingUnbondValidationFailure(it, resourceManager) }, - autoFixPayload = ::parachainStakingUnbondPayloadAutoFix, - progressConsumer = validationInProgress.progressConsumer() - ) { fixedPayload -> - validationInProgress.value = false + val payload = ParachainStakingUnbondValidationPayload( + amount = amountChooserMixin.amount.first(), + fee = feeLoaderMixin.awaitDecimalFee(), + asset = assetFlow.first(), + collator = selectedCollatorFlow.first() + ) - goToNextStep(fee = fee, amount = fixedPayload.amount, collator = fixedPayload.collator) - } + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { parachainStakingUnbondValidationFailure(it, resourceManager) }, + autoFixPayload = ::parachainStakingUnbondPayloadAutoFix, + progressConsumer = validationInProgress.progressConsumer() + ) { fixedPayload -> + validationInProgress.value = false + + goToNextStep(fee = fixedPayload.fee, amount = fixedPayload.amount, collator = fixedPayload.collator) } } private fun goToNextStep( - fee: BigDecimal, + fee: DecimalFee, amount: BigDecimal, collator: Collator, ) = launch { @@ -238,15 +241,10 @@ class ParachainStakingUnbondViewModel( ParachainStakingUnbondConfirmPayload( collator = mapCollatorToCollatorParcelModel(collator), amount = amount, - fee = fee + fee = mapFeeToParcel(fee) ) } router.openConfirmUnbond(payload) } - - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt index 19e7830121..f274da32b0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt @@ -36,6 +36,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking.yieldBoost.confirm.model.YieldBoostConfirmPayload import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -69,6 +70,8 @@ class YieldBoostConfirmViewModel( FeeLoaderMixin by feeLoaderMixin, ExternalActions by externalActions { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val assetFlow = assetUseCase.currentAssetFlow() .shareInBackground() @@ -138,14 +141,14 @@ class YieldBoostConfirmViewModel( } private fun setInitialFee() = launch { - feeLoaderMixin.setFee(payload.fee) + feeLoaderMixin.setFee(decimalFee.genericFee) } private fun sendTransactionIfValid() = launch { val payload = YieldBoostValidationPayload( collator = collator(), configuration = yieldBoostConfiguration(), - fee = payload.fee, + fee = decimalFee, activeTasks = activeTasksFlow.first(), asset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/model/YieldBoostConfirmPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/model/YieldBoostConfirmPayload.kt index 825efcc681..98b776a517 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/model/YieldBoostConfirmPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/model/YieldBoostConfirmPayload.kt @@ -3,15 +3,15 @@ package io.novafoundation.nova.feature_staking_impl.presentation.parachainStakin import android.os.Parcelable import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yieldBoost.YieldBoostConfiguration import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking.collator.select.model.CollatorParcelModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize -import java.math.BigDecimal import java.math.BigInteger @Parcelize class YieldBoostConfirmPayload( val collator: CollatorParcelModel, val configurationParcel: YieldBoostConfigurationParcel, - val fee: BigDecimal, + val fee: FeeParcelModel, ) : Parcelable sealed class YieldBoostConfigurationParcel(open val collatorIdHex: String) : Parcelable { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt index 00b82be1f4..ace1a5bf26 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt @@ -53,7 +53,10 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmountInput import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import jp.co.soramitsu.fearless_utils.extensions.toHexString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -330,37 +333,37 @@ class SetupYieldBoostViewModel( selectedCollatorFlow.emit(mostRelevantCollator.collator) } - private fun maybeGoToNext() = requireFee { fee -> - launch { - val payload = YieldBoostValidationPayload( - collator = selectedCollatorFlow.first(), - configuration = modifiedYieldBoostConfiguration.first(), - fee = fee, - activeTasks = activeTasksFlow.first(), - asset = assetFlow.first() - ) + private fun maybeGoToNext() = launch { + validationInProgressFlow.value = true - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { yieldBoostValidationFailure(it, resourceManager) }, - progressConsumer = validationInProgressFlow.progressConsumer() - ) { - validationInProgressFlow.value = false + val payload = YieldBoostValidationPayload( + collator = selectedCollatorFlow.first(), + configuration = modifiedYieldBoostConfiguration.first(), + fee = feeLoaderMixin.awaitDecimalFee(), + activeTasks = activeTasksFlow.first(), + asset = assetFlow.first() + ) - goToNextStep(fee = it.fee, collator = it.collator, configuration = it.configuration) - } + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { yieldBoostValidationFailure(it, resourceManager) }, + progressConsumer = validationInProgressFlow.progressConsumer() + ) { + validationInProgressFlow.value = false + + goToNextStep(fee = it.fee, collator = it.collator, configuration = it.configuration) } } private fun goToNextStep( - fee: BigDecimal, + fee: DecimalFee, configuration: YieldBoostConfiguration, collator: Collator, ) = launch { val payload = withContext(Dispatchers.Default) { YieldBoostConfirmPayload( - fee = fee, + fee = mapFeeToParcel(fee), configurationParcel = YieldBoostConfigurationParcel(configuration), collator = mapCollatorToCollatorParcelModel(collator) ) @@ -369,11 +372,6 @@ class SetupYieldBoostViewModel( router.openConfirmYieldBoost(payload) } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) - private fun constructActiveConfiguration(tasks: List, collator: Collator): YieldBoostConfiguration { val collatorId = collator.accountId() val collatorTask = tasks.find { it.collator.contentEquals(collatorId) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutFragment.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutFragment.kt index 3196e9b0cb..8a6053ddc9 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutFragment.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutFragment.kt @@ -66,6 +66,7 @@ class ConfirmPayoutFragment : BaseFragment() { } override fun subscribe(viewModel: ConfirmPayoutViewModel) { + observeRetries(viewModel.partialRetriableMixin) setupExternalActions(viewModel) observeValidations(viewModel) observeRetries(viewModel) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt index 13feba29d0..c5ee482861 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.requireException +import io.novafoundation.nova.common.utils.multiResult.PartialRetriableMixin import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.progressConsumer @@ -17,16 +17,16 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.W import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState import io.novafoundation.nova.feature_staking_impl.R -import io.novafoundation.nova.feature_staking_impl.data.model.Payout import io.novafoundation.nova.feature_staking_impl.domain.StakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.payout.PayoutInteractor import io.novafoundation.nova.feature_staking_impl.domain.validations.payout.MakePayoutPayload import io.novafoundation.nova.feature_staking_impl.domain.validations.payout.PayoutValidationFailure import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.payouts.confirm.model.ConfirmPayoutPayload +import io.novafoundation.nova.feature_staking_impl.presentation.payouts.model.mapPendingPayoutParcelToPayout import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -48,6 +48,7 @@ class ConfirmPayoutViewModel( private val resourceManager: ResourceManager, private val selectedAssetState: AnySelectedAssetOptionSharedState, walletUiUseCase: WalletUiUseCase, + partialRetriableMixinFactory: PartialRetriableMixin.Factory, ) : BaseViewModel(), ExternalActions.Presentation by externalActions, FeeLoaderMixin by feeLoaderMixin, @@ -60,7 +61,7 @@ class ConfirmPayoutViewModel( .filterIsInstance() .share() - private val payouts = payload.payouts.map { Payout(it.validatorInfo.address, it.era, it.amountInPlanks) } + private val payouts = payload.payouts.map(::mapPendingPayoutParcelToPayout) private val _showNextProgress = MutableLiveData(false) val showNextProgress: LiveData = _showNextProgress @@ -78,6 +79,8 @@ class ConfirmPayoutViewModel( } .shareInBackground() + val partialRetriableMixin = partialRetriableMixinFactory.create(scope = this) + init { loadFee() } @@ -98,48 +101,48 @@ class ConfirmPayoutViewModel( router.back() } - private fun sendTransactionIfValid() = feeLoaderMixin.requireFee(this) { fee -> - launch { - val asset = assetFlow.first() - val accountAddress = stakingStateFlow.first().accountAddress - val amount = asset.token.configuration.amountFromPlanks(payload.totalRewardInPlanks) - - val payoutStakersPayloads = payouts.map { MakePayoutPayload.PayoutStakersPayload(it.era, it.validatorAddress) } - - val makePayoutPayload = MakePayoutPayload(accountAddress, fee, amount, asset, payoutStakersPayloads) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = makePayoutPayload, - validationFailureTransformer = ::payloadValidationFailure, - progressConsumer = _showNextProgress.progressConsumer() - ) { - sendTransaction(makePayoutPayload) - } + private fun sendTransactionIfValid() = launch { + val asset = assetFlow.first() + val accountAddress = stakingStateFlow.first().accountAddress + val amount = asset.token.configuration.amountFromPlanks(payload.totalRewardInPlanks) + + val makePayoutPayload = MakePayoutPayload( + originAddress = accountAddress, + fee = feeLoaderMixin.awaitDecimalFee(), + totalReward = amount, + asset = asset, + payouts = payouts + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = makePayoutPayload, + validationFailureTransformer = ::payloadValidationFailure, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction(makePayoutPayload) } } private fun sendTransaction(payload: MakePayoutPayload) = launch { val result = payoutInteractor.makePayouts(payload) - _showNextProgress.value = false - - if (result.isSuccess) { - showMessage(resourceManager.getString(R.string.make_payout_transaction_sent)) - - router.returnToStakingMain() - } else { - showError(result.requireException()) - } + partialRetriableMixin.handleMultiResult( + multiResult = result, + onSuccess = { + showMessage(resourceManager.getString(R.string.make_payout_transaction_sent)) + router.returnToStakingMain() + }, + progressConsumer = _showNextProgress.progressConsumer(), + onRetryCancelled = { router.back() } + ) } private fun loadFee() { feeLoaderMixin.loadFee( - viewModelScope, + coroutineScope = viewModelScope, feeConstructor = { - val address = stakingStateFlow.first().accountAddress - - payoutInteractor.estimatePayoutFee(address, payouts) + payoutInteractor.estimatePayoutFee(payouts) }, onRetryCancelled = ::backClicked ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/di/ConfirmPayoutModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/di/ConfirmPayoutModule.kt index 88ce3d0327..d3fd60bd45 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/di/ConfirmPayoutModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/di/ConfirmPayoutModule.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.address.AddressIconGenerator 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.utils.multiResult.PartialRetriableMixin import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase @@ -43,6 +44,7 @@ class ConfirmPayoutModule { resourceManager: ResourceManager, singleAssetSharedState: StakingSharedState, walletUiUseCase: WalletUiUseCase, + partialRetriableMixinFactory: PartialRetriableMixin.Factory, ): ViewModel { return ConfirmPayoutViewModel( interactor = interactor, @@ -56,7 +58,8 @@ class ConfirmPayoutModule { validationExecutor = validationExecutor, resourceManager = resourceManager, selectedAssetState = singleAssetSharedState, - walletUiUseCase = walletUiUseCase + walletUiUseCase = walletUiUseCase, + partialRetriableMixinFactory = partialRetriableMixinFactory ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/list/PayoutsListViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/list/PayoutsListViewModel.kt index e9134a2097..00785a2458 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/list/PayoutsListViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/list/PayoutsListViewModel.kt @@ -8,8 +8,6 @@ import io.novafoundation.nova.common.mixin.api.RetryPayload import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.inBackground -import io.novafoundation.nova.common.utils.requireException -import io.novafoundation.nova.common.utils.requireValue import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.withLoading import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency @@ -81,10 +79,10 @@ class PayoutsListViewModel( launch { val result = interactor.calculatePendingPayouts(viewModelScope) - if (result.isSuccess) { - payoutsStatisticsFlow.emit(result.requireValue()) - } else { - val errorMessage = result.requireException().message ?: resourceManager.getString(R.string.common_undefined_error_message) + result.onSuccess { value -> + payoutsStatisticsFlow.emit(value) + }.onFailure { exception -> + val errorMessage = exception.message ?: resourceManager.getString(R.string.common_undefined_error_message) retryEvent.value = Event( RetryPayload( @@ -139,7 +137,8 @@ class PayoutsListViewModel( amountInPlanks = amountInPlanks, timeLeftCalculatedAt = timeLeftCalculatedAt, timeLeft = timeLeft, - closeToExpire = closeToExpire + closeToExpire = closeToExpire, + pagesToClaim = pagesToClaim ) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/model/PendingPayoutParcelable.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/model/PendingPayoutParcelable.kt index 4e6e2cebac..227ee13d36 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/model/PendingPayoutParcelable.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/model/PendingPayoutParcelable.kt @@ -1,6 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.presentation.payouts.model import android.os.Parcelable +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.feature_staking_impl.data.model.Payout +import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId import kotlinx.android.parcel.Parcelize import java.math.BigInteger @@ -12,6 +15,7 @@ class PendingPayoutParcelable( val timeLeftCalculatedAt: Long, val timeLeft: Long, val closeToExpire: Boolean, + val pagesToClaim: List ) : Parcelable { @Parcelize class ValidatorInfoParcelable( @@ -19,3 +23,14 @@ class PendingPayoutParcelable( val identityName: String?, ) : Parcelable } + +fun mapPendingPayoutParcelToPayout( + parcelPayoutParcelable: PendingPayoutParcelable +): Payout { + return Payout( + validatorStash = parcelPayoutParcelable.validatorInfo.address.toAccountId().intoKey(), + era = parcelPayoutParcelable.era, + amount = parcelPayoutParcelable.amountInPlanks, + pagesToClaim = parcelPayoutParcelable.pagesToClaim + ) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMorePayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMorePayload.kt index 458067efa1..82f39241f3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMorePayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMorePayload.kt @@ -1,12 +1,13 @@ package io.novafoundation.nova.feature_staking_impl.presentation.staking.bond.confirm import android.os.Parcelable +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @Parcelize class ConfirmBondMorePayload( val amount: BigDecimal, - val fee: BigDecimal, + val fee: FeeParcelModel, val stashAddress: String, ) : Parcelable diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt index ba1ada79f5..10fa478f0d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt @@ -24,6 +24,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.bond.bon import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -48,6 +49,8 @@ class ConfirmBondMoreViewModel( ExternalActions by externalActions, Validatable by validationExecutor { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val _showNextProgress = MutableLiveData(false) val showNextProgress: LiveData = _showNextProgress @@ -68,7 +71,7 @@ class ConfirmBondMoreViewModel( .shareInBackground() val feeStatusFlow = stashAssetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(payload.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } @@ -94,7 +97,7 @@ class ConfirmBondMoreViewModel( private fun maybeGoToNext() = launch { val payload = BondMoreValidationPayload( stashAddress = payload.stashAddress, - fee = payload.fee, + fee = decimalFee, amount = payload.amount, stashAsset = stashAssetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt index 8b5f340b2d..9d29e40aec 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt @@ -24,6 +24,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest @@ -96,43 +98,38 @@ class SelectBondMoreViewModel( feeConstructor = { token -> val amountInPlanks = token.planksFromAmount(amount) - bondMoreInteractor.estimateFee(amountInPlanks) + bondMoreInteractor.estimateFee(amountInPlanks, accountStakingFlow.first()) }, onRetryCancelled = ::backClicked ) } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) + private fun maybeGoToNext() = launch { + _showNextProgress.value = true + + val payload = BondMoreValidationPayload( + stashAddress = stashAddress(), + fee = feeLoaderMixin.awaitDecimalFee(), + amount = amountChooserMixin.amount.first(), + stashAsset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { bondMoreValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + _showNextProgress.value = false - private fun maybeGoToNext() = requireFee { fee -> - launch { - val payload = BondMoreValidationPayload( - stashAddress = stashAddress(), - fee = fee, - amount = amountChooserMixin.amount.first(), - stashAsset = assetFlow.first() - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { bondMoreValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - _showNextProgress.value = false - - openConfirm(payload) - } + openConfirm(payload) } } private fun openConfirm(validationPayload: BondMoreValidationPayload) { val confirmPayload = ConfirmBondMorePayload( amount = validationPayload.amount, - fee = validationPayload.fee, + fee = mapFeeToParcel(validationPayload.fee), stashAddress = validationPayload.stashAddress, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerPayload.kt index 834b0b94ad..385b742f66 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerPayload.kt @@ -1,12 +1,13 @@ package io.novafoundation.nova.feature_staking_impl.presentation.staking.controller.confirm import android.os.Parcelable +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @Parcelize class ConfirmSetControllerPayload( - val fee: BigDecimal, + val fee: FeeParcelModel, val stashAddress: String, val controllerAddress: String, val transferable: BigDecimal diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt index 868208b0a5..655ac315ba 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt @@ -20,6 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.controller.set.bondSetControllerValidationFailure import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.MutableStateFlow @@ -42,12 +43,14 @@ class ConfirmSetControllerViewModel( Validatable by validationExecutor, ExternalActions by externalActions { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val assetFlow = interactor.assetFlow(payload.stashAddress) .inBackground() .share() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(payload.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } @@ -90,7 +93,7 @@ class ConfirmSetControllerViewModel( val payload = SetControllerValidationPayload( stashAddress = payload.stashAddress, controllerAddress = payload.controllerAddress, - fee = payload.fee, + fee = decimalFee, transferable = payload.transferable ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/set/SetControllerViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/set/SetControllerViewModel.kt index 4e37f11b0b..2e3d7bc762 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/set/SetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/controller/set/SetControllerViewModel.kt @@ -35,7 +35,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.controller import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.controller.confirm.ConfirmSetControllerPayload import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.Flow @@ -177,7 +178,7 @@ class SetControllerViewModel( private fun loadFee() { feeLoaderMixin.loadFee( coroutineScope = viewModelScope, - feeConstructor = { interactor.estimateFee(controllerAddress()) }, + feeConstructor = { interactor.estimateFee(controllerAddress(), accountStakingFlow.first()) }, onRetryCancelled = ::backClicked ) } @@ -199,34 +200,34 @@ class SetControllerViewModel( private suspend fun controllerAddress() = accountStakingFlow.first().controllerAddress - private fun maybeGoToConfirm() = feeLoaderMixin.requireFee(this) { fee -> - launch { - val controllerAddress = getNewControllerAddress() + private fun maybeGoToConfirm() = launch { + validationInProgress.value = true - val payload = SetControllerValidationPayload( - stashAddress = stashAddress(), - controllerAddress = controllerAddress, - fee = fee, - transferable = assetFlow.first().transferable - ) + val controllerAddress = getNewControllerAddress() + + val payload = SetControllerValidationPayload( + stashAddress = stashAddress(), + controllerAddress = controllerAddress, + fee = feeLoaderMixin.awaitDecimalFee(), + transferable = assetFlow.first().transferable + ) - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - progressConsumer = validationInProgress.progressConsumer(), - validationFailureTransformer = { bondSetControllerValidationFailure(it, resourceManager) } - ) { - validationInProgress.value = false - - openConfirm( - ConfirmSetControllerPayload( - fee = fee, - stashAddress = payload.stashAddress, - controllerAddress = payload.controllerAddress, - transferable = payload.transferable - ) + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + progressConsumer = validationInProgress.progressConsumer(), + validationFailureTransformer = { bondSetControllerValidationFailure(it, resourceManager) } + ) { + validationInProgress.value = false + + openConfirm( + ConfirmSetControllerPayload( + fee = mapFeeToParcel(it.fee), + stashAddress = payload.stashAddress, + controllerAddress = payload.controllerAddress, + transferable = payload.transferable ) - } + ) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt index 9da994f3ab..cc5a82b565 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt @@ -24,7 +24,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.rebond.rebondValidationFailure import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -110,28 +110,28 @@ class ConfirmRebondViewModel( feeConstructor = { token -> val amountInPlanks = token.planksFromAmount(payload.amount) - rebondInteractor.estimateFee(amountInPlanks) + rebondInteractor.estimateFee(amountInPlanks, accountStakingFlow.first()) }, onRetryCancelled = ::backClicked ) } - private fun maybeGoToNext() = feeLoaderMixin.requireFee(this) { fee -> - launch { - val payload = RebondValidationPayload( - fee = fee, - rebondAmount = payload.amount, - controllerAsset = assetFlow.first() - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { rebondValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer(), - block = ::sendTransaction - ) - } + private fun maybeGoToNext() = launch { + _showNextProgress.value = true + + val payload = RebondValidationPayload( + fee = feeLoaderMixin.awaitDecimalFee(), + rebondAmount = payload.amount, + controllerAsset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { rebondValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer(), + block = ::sendTransaction + ) } private fun sendTransaction(validPayload: RebondValidationPayload) = launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt index d5d78a2467..8c980c1d70 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt @@ -22,7 +22,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.transferableAmountModelOf import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.math.BigDecimal -import kotlin.time.ExperimentalTime class CustomRebondViewModel( private val router: StakingRouter, @@ -87,7 +86,6 @@ class CustomRebondViewModel( router.back() } - @OptIn(ExperimentalTime::class) private fun listenFee() { amountChooserMixin.backPressuredAmount .onEach { loadFee(it) } @@ -100,28 +98,26 @@ class CustomRebondViewModel( feeConstructor = { token -> val amountInPlanks = token.planksFromAmount(amount) - rebondInteractor.estimateFee(amountInPlanks) + rebondInteractor.estimateFee(amountInPlanks, accountStakingFlow.first()) }, onRetryCancelled = ::backClicked ) } - private fun maybeGoToNext() = feeLoaderMixin.requireFee(this) { fee -> - launch { - val payload = RebondValidationPayload( - fee = fee, - rebondAmount = amountChooserMixin.amount.first(), - controllerAsset = assetFlow.first() - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { rebondValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer(), - block = ::openConfirm - ) - } + private fun maybeGoToNext() = launch { + val payload = RebondValidationPayload( + fee = feeLoaderMixin.awaitDecimalFee(), + rebondAmount = amountChooserMixin.amount.first(), + controllerAsset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { rebondValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer(), + block = ::openConfirm + ) } private fun openConfirm(validPayload: RebondValidationPayload) { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt index a0db183f44..76ebc73767 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt @@ -20,6 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.reedeem.Re import io.novafoundation.nova.feature_staking_impl.domain.validations.reedeem.RedeemValidationSystem import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -28,7 +29,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.math.BigDecimal class RedeemViewModel( private val router: StakingRouter, @@ -98,28 +98,23 @@ class RedeemViewModel( ) } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) + private fun maybeGoToNext() = launch { + _showNextProgress.value = true - private fun maybeGoToNext() = requireFee { fee -> - launch { - val asset = assetFlow.first() - - val validationPayload = RedeemValidationPayload( - fee = fee, - asset = asset - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = validationPayload, - validationFailureTransformer = { redeemValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - sendTransaction(it) - } + val asset = assetFlow.first() + + val validationPayload = RedeemValidationPayload( + fee = feeLoaderMixin.awaitDecimalFee(), + asset = asset + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = validationPayload, + validationFailureTransformer = { redeemValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction(it) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt index ae2dd95fc2..23471c377f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt @@ -29,6 +29,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDe import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.select.rewardDestinationValidationFailure import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.filterIsInstance @@ -53,6 +54,8 @@ class ConfirmRewardDestinationViewModel( Validatable by validationExecutor, ExternalActions by externalActions { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val _showNextProgress = MutableLiveData(false) val showNextProgress: LiveData = _showNextProgress @@ -78,7 +81,7 @@ class ConfirmRewardDestinationViewModel( .shareInBackground() val feeStatusFlow = controllerAssetFlow.map { - FeeStatus.Loaded(mapFeeToFeeModel(payload.fee, it.token)) + FeeStatus.Loaded(mapFeeToFeeModel(decimalFee.genericFee, it.token)) } .shareInBackground() @@ -128,7 +131,7 @@ class ConfirmRewardDestinationViewModel( val payload = RewardDestinationValidationPayload( availableControllerBalance = controllerAsset.transferable, - fee = payload.fee, + fee = decimalFee, stashState = stashState ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/parcel/ConfirmRewardDestinationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/parcel/ConfirmRewardDestinationPayload.kt index ced4b0a2aa..e7dec66bcf 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/parcel/ConfirmRewardDestinationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/parcel/ConfirmRewardDestinationPayload.kt @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.confirm.parcel import android.os.Parcelable +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize -import java.math.BigDecimal @Parcelize class ConfirmRewardDestinationPayload( - val fee: BigDecimal, + val fee: FeeParcelModel, val rewardDestination: RewardDestinationParcelModel, ) : Parcelable diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt index e8c0bd8e5b..dd1cecdc08 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt @@ -25,6 +25,9 @@ import io.novafoundation.nova.feature_staking_impl.presentation.common.rewardDes import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.confirm.parcel.ConfirmRewardDestinationPayload import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.confirm.parcel.RewardDestinationParcelModel import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.state.selectedOption import kotlinx.coroutines.async import kotlinx.coroutines.flow.combine @@ -35,7 +38,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import java.math.BigDecimal class SelectRewardDestinationViewModel( private val router: StakingRouter, @@ -111,35 +113,32 @@ class SelectRewardDestinationViewModel( ) } - private fun maybeGoToNext() = requireFee { fee -> - launch { - val payload = RewardDestinationValidationPayload( - availableControllerBalance = controllerAssetFlow.first().transferable, - fee = fee, - stashState = stashStateFlow.first() - ) - - val rewardDestination = rewardDestinationModelFlow.first() - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { rewardDestinationValidationFailure(resourceManager, it) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - _showNextProgress.value = false - - goToNextStep(rewardDestination, it.fee) - } + private fun maybeGoToNext() = launch { + _showNextProgress.value = true + + val payload = RewardDestinationValidationPayload( + availableControllerBalance = controllerAssetFlow.first().transferable, + fee = feeLoaderMixin.awaitDecimalFee(), + stashState = stashStateFlow.first() + ) + + val rewardDestination = rewardDestinationModelFlow.first() + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { rewardDestinationValidationFailure(resourceManager, it) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + _showNextProgress.value = false + + goToNextStep(rewardDestination, it.fee) } } - private fun goToNextStep( - rewardDestination: RewardDestinationModel, - fee: BigDecimal - ) { + private fun goToNextStep(rewardDestination: RewardDestinationModel, fee: DecimalFee) { val payload = ConfirmRewardDestinationPayload( - fee = fee, + fee = mapFeeToParcel(fee), rewardDestination = mapRewardDestinationModelToRewardDestinationParcelModel(rewardDestination) ) @@ -153,10 +152,5 @@ class SelectRewardDestinationViewModel( } } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) - private suspend fun rewardCalculator() = rewardCalculator.await() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt index 2eb5f070ec..c19ff42e87 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt @@ -104,7 +104,7 @@ class ConfirmMultiStakingViewModel( .shareInBackground() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/types/DirectConfirmMultiStakingType.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/types/DirectConfirmMultiStakingType.kt index a2c833da62..fb94ce9e7d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/types/DirectConfirmMultiStakingType.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/types/DirectConfirmMultiStakingType.kt @@ -33,9 +33,10 @@ class DirectConfirmMultiStakingType( override suspend fun onStakingTypeDetailsClicked() { // act as an adapter between new flow and legacy logic val reviewValidatorsState = ReadyToSubmit( - validators = selection.validators, + newValidators = selection.validators, selectionMethod = ReadyToSubmit.SelectionMethod.RECOMMENDED, activeStake = selection.stake, + currentlySelectedValidators = emptyList() ) setupStakingSharedState.set(reviewValidatorsState) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt index fdaedd4bb7..b9b4c85041 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt @@ -29,7 +29,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWithV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.Dispatchers @@ -190,7 +190,7 @@ class SetupAmountMultiStakingViewModel( } private fun runFeeUpdates() { - feeLoaderMixin.connectWithV2( + feeLoaderMixin.connectWith( inputSource = currentSelectionFlow .filterNotNull() .debounce(DEBOUNCE_RATE_MILLIS.milliseconds), diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupStakingType/SetupStakingTypeFlowExecutor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupStakingType/SetupStakingTypeFlowExecutor.kt index 46e88457fc..b6e4885415 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupStakingType/SetupStakingTypeFlowExecutor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupStakingType/SetupStakingTypeFlowExecutor.kt @@ -55,8 +55,9 @@ class SetupDirectStakingFlowExecutor( setupStakingSharedState.set( SetupStakingProcess.ReadyToSubmit( activeStake = selectionStore.getCurrentSelection()?.selection?.stake.orZero(), - validators = selectionStore.getValidatorsOrEmpty(), - selectionMethod = SetupStakingProcess.ReadyToSubmit.SelectionMethod.CUSTOM + newValidators = selectionStore.getValidatorsOrEmpty(), + selectionMethod = SetupStakingProcess.ReadyToSubmit.SelectionMethod.CUSTOM, + currentlySelectedValidators = emptyList() ) ) router.openSelectCustomValidators() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondPayload.kt index 61c7a4d3a0..846e3ed74a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondPayload.kt @@ -1,11 +1,12 @@ package io.novafoundation.nova.feature_staking_impl.presentation.staking.unbond.confirm import android.os.Parcelable +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @Parcelize class ConfirmUnbondPayload( val amount: BigDecimal, - val fee: BigDecimal + val fee: FeeParcelModel ) : Parcelable diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt index 71a1bbbb1b..0073a07f6e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt @@ -26,6 +26,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.unbond.u import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -52,6 +53,8 @@ class ConfirmUnbondViewModel( ExternalActions by externalActions, Validatable by validationExecutor { + private val decimalFee = mapFeeFromParcel(payload.fee) + private val _showNextProgress = MutableLiveData(false) val showNextProgress: LiveData = _showNextProgress @@ -77,7 +80,7 @@ class ConfirmUnbondViewModel( .shareInBackground() val feeStatusLiveData = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(payload.fee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) FeeStatus.Loaded(feeModel) } @@ -111,7 +114,7 @@ class ConfirmUnbondViewModel( val payload = UnbondValidationPayload( asset = asset, stash = accountStakingFlow.first(), - fee = payload.fee, + fee = decimalFee, amount = payload.amount, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt index b1bd3d9e33..09a2044f1a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt @@ -22,6 +22,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.transferableAmountModelOf import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -31,7 +33,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.math.BigDecimal -import kotlin.time.ExperimentalTime class SelectUnbondViewModel( private val router: StakingRouter, @@ -82,7 +83,6 @@ class SelectUnbondViewModel( router.back() } - @OptIn(ExperimentalTime::class) private fun listenFee() { amountMixin.backPressuredAmount .onEach { loadFee(it) } @@ -102,39 +102,34 @@ class SelectUnbondViewModel( ) } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) + private fun maybeGoToNext() = launch { + _showNextProgress.value = true + + val asset = assetFlow.first() + + val payload = UnbondValidationPayload( + stash = accountStakingFlow.first(), + asset = asset, + fee = feeLoaderMixin.awaitDecimalFee(), + amount = amountMixin.amount.first(), + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformerCustom = { status, flowActions -> unbondValidationFailure(status, flowActions, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { correctPayload -> + _showNextProgress.value = false - private fun maybeGoToNext() = requireFee { fee -> - launch { - val asset = assetFlow.first() - - val payload = UnbondValidationPayload( - stash = accountStakingFlow.first(), - asset = asset, - fee = fee, - amount = amountMixin.amount.first(), - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformerCustom = { status, flowActions -> unbondValidationFailure(status, flowActions, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { correctPayload -> - _showNextProgress.value = false - - openConfirm(correctPayload) - } + openConfirm(correctPayload) } } private fun openConfirm(validationPayload: UnbondValidationPayload) { val confirmUnbondPayload = ConfirmUnbondPayload( amount = validationPayload.amount, - fee = validationPayload.fee + fee = mapFeeToParcel(validationPayload.fee) ) router.openConfirmUnbond(confirmUnbondPayload) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/ChangeStakingValidationFailureUI.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/ChangeStakingValidationFailureUI.kt index d7bca03440..55afbd8826 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/ChangeStakingValidationFailureUI.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/ChangeStakingValidationFailureUI.kt @@ -11,8 +11,8 @@ fun mapAddEvmTokensValidationFailureToUI( ): TitleAndMessage { return when (failure) { ChangeStackingValidationFailure.NO_ACCESS_TO_CONTROLLER_ACCOUNT -> { - resourceManager.getString(R.string.stacking_no_access_to_controller_account_title) to - resourceManager.getString(R.string.stacking_no_access_to_controller_account_message) + resourceManager.getString(R.string.staking_no_access_to_controller_account_title) to + resourceManager.getString(R.string.staking_no_access_to_controller_account_message) } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/StateTransitions.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/StateTransitions.kt index 8b6ae434b2..67d008106b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/StateTransitions.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/StateTransitions.kt @@ -7,9 +7,14 @@ import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStak import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance fun SetupStakingSharedState.reset() = mutate { - when (it) { - is SetupStakingProcess.ReadyToSubmit -> it.reset() - else -> throw IllegalArgumentException("Cannot retract validators from $it state") + SetupStakingProcess.Initial +} + +fun SetupStakingSharedState.retractRecommended() = mutate { + if (it is SetupStakingProcess.ReadyToSubmit && it.selectionMethod == SelectionMethod.RECOMMENDED) { + it.previous() + } else { + it } } @@ -24,23 +29,48 @@ fun SetupStakingSharedState.setRecommendedValidators( fun SetupStakingSharedState.activeStake(): Balance { return when (val state = setupStakingProcess.value) { is SetupStakingProcess.ReadyToSubmit -> state.activeStake - else -> throw IllegalArgumentException("Cannot get active stake from $state state") + is SetupStakingProcess.ChoosingValidators -> state.activeStake + SetupStakingProcess.Initial -> throw IllegalArgumentException("Cannot get active stake from $state state") } } -fun SetupStakingSharedState.getSelectedValidators(): List { +/** + * Validators that has been selected by user during current flow + * Does not count currently selected validators + */ +fun SetupStakingSharedState.getNewValidators(): List { return when (val process = setupStakingProcess.value) { - is SetupStakingProcess.ReadyToSubmit -> process.validators - else -> throw IllegalArgumentException("Cannot get validators from $process state") + is SetupStakingProcess.ReadyToSubmit -> process.newValidators + SetupStakingProcess.Initial, is SetupStakingProcess.ChoosingValidators -> { + throw IllegalArgumentException("Cannot get validators from $process state") + } + } +} + +/** + * Validators that should be shown for the user as selected + * It is either its already updated selection during current flow + * or its currently selected validators (based on on-chain nominations) + */ +fun SetupStakingProcess.getSelectedValidatorsOrNull(): List? { + return when (this) { + is SetupStakingProcess.ReadyToSubmit -> newValidators + is SetupStakingProcess.ChoosingValidators -> currentlySelectedValidators + SetupStakingProcess.Initial -> null } } +fun SetupStakingProcess.getSelectedValidatorsOrEmpty(): List { + return getSelectedValidatorsOrNull().orEmpty() +} + private fun SetupStakingSharedState.setValidators( validators: List, selectionMethod: SelectionMethod ) = mutate { when (it) { is SetupStakingProcess.ReadyToSubmit -> it.changeValidators(validators, selectionMethod) - else -> throw IllegalArgumentException("Cannot set validators from $it state") + is SetupStakingProcess.ChoosingValidators -> it.next(validators, selectionMethod) + SetupStakingProcess.Initial -> throw IllegalArgumentException("Cannot set validators from $it state") } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt index f5bfabcad3..6dbdb7054e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt @@ -33,7 +33,9 @@ import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStak import io.novafoundation.nova.feature_staking_impl.presentation.common.validation.stakingValidationFailure import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.activeStake import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.confirm.hints.ConfirmStakeHintsMixinFactory +import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.reset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.filterIsInstance @@ -41,7 +43,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.math.BigDecimal class ConfirmChangeValidatorsViewModel( private val router: StakingRouter, @@ -94,7 +95,7 @@ class ConfirmChangeValidatorsViewModel( .share() val nominationsFlow = flowOf { - val selectedCount = currentProcessState.validators.size + val selectedCount = currentProcessState.newValidators.size val maxValidatorsPerNominator = maxValidatorsPerNominator() resourceManager.getString(R.string.staking_confirm_nominations, selectedCount, maxValidatorsPerNominator) @@ -128,34 +129,34 @@ class ConfirmChangeValidatorsViewModel( private fun loadFee() { feeLoaderMixin.loadFee( coroutineScope = viewModelScope, - feeConstructor = { changeValidatorsInteractor.estimateFee(prepareNominations()) }, + feeConstructor = { changeValidatorsInteractor.estimateFee(prepareNominations(), stashFlow.first()) }, onRetryCancelled = ::backClicked ) } - private fun prepareNominations() = currentProcessState.validators.map(Validator::accountIdHex) - - private fun sendTransactionIfValid() = requireFee { fee -> - launch { - val payload = SetupStakingPayload( - maxFee = fee, - controllerAsset = controllerAssetFlow.first() - ) - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - validationFailureTransformer = { stakingValidationFailure(it, resourceManager) }, - progressConsumer = _showNextProgress.progressConsumer() - ) { - sendTransaction() - } + private fun prepareNominations() = currentProcessState.newValidators.map(Validator::accountIdHex) + + private fun sendTransactionIfValid() = launch { + _showNextProgress.value = true + + val payload = SetupStakingPayload( + maxFee = feeLoaderMixin.awaitDecimalFee(), + controllerAsset = controllerAssetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = { stakingValidationFailure(it, resourceManager) }, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction() } } private fun sendTransaction() = launch { val setupResult = changeValidatorsInteractor.changeValidators( - controllerAddress = controllerAddressFlow.first(), + stakingState = stashFlow.first(), validatorAccountIds = prepareNominations(), ) @@ -164,7 +165,7 @@ class ConfirmChangeValidatorsViewModel( if (setupResult.isSuccess) { showMessage(resourceManager.getString(R.string.common_transaction_submitted)) - setupStakingSharedState.set(currentProcessState.reset()) + setupStakingSharedState.reset() router.returnToCurrentValidators() } else { @@ -172,11 +173,6 @@ class ConfirmChangeValidatorsViewModel( } } - private fun requireFee(block: (BigDecimal) -> Unit) = feeLoaderMixin.requireFee( - block, - onError = { title, message -> showError(title, message) } - ) - private suspend fun generateDestinationModel(address: String, name: String?): AddressModel { return addressIconGenerator.createAddressModel( accountAddress = address, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/nominations/ConfirmNominationsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/nominations/ConfirmNominationsViewModel.kt index 61c666a69f..f8d3a3589f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/nominations/ConfirmNominationsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/nominations/ConfirmNominationsViewModel.kt @@ -36,7 +36,7 @@ class ConfirmNominationsViewModel( private val currentSetupStakingProcess = sharedStateSetup.get() - private val validators = currentSetupStakingProcess.validators + private val validators = currentSetupStakingProcess.newValidators val selectedValidatorsLiveData = liveData(Dispatchers.Default) { emit(convertToModels(validators, tokenUseCase.currentToken())) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/ReviewCustomValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/ReviewCustomValidatorsViewModel.kt index 62b56801b3..36c57e3ab6 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/ReviewCustomValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/ReviewCustomValidatorsViewModel.kt @@ -45,7 +45,7 @@ class ReviewCustomValidatorsViewModel( .share() private val selectedValidators = confirmSetupState - .map { it.validators } + .map { it.newValidators } .share() private val currentTokenFlow = tokenUseCase.currentTokenFlow() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/flowAction/SetupStakingReviewValidatorsFlowAction.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/flowAction/SetupStakingReviewValidatorsFlowAction.kt index 6fdfa4c309..c4a3d5e1c5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/flowAction/SetupStakingReviewValidatorsFlowAction.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/review/flowAction/SetupStakingReviewValidatorsFlowAction.kt @@ -4,7 +4,8 @@ import io.novafoundation.nova.feature_staking_impl.data.StakingOption import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupStakingType.SetupStakingTypeSelectionMixinFactory import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingSharedState -import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.getSelectedValidators +import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.getNewValidators +import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.reset import kotlinx.coroutines.CoroutineScope class SetupStakingReviewValidatorsFlowAction( @@ -15,8 +16,9 @@ class SetupStakingReviewValidatorsFlowAction( override suspend fun execute(coroutineScope: CoroutineScope, stakingOption: StakingOption) { val setupStakingTypeSelectionMixin = setupStakingTypeSelectionMixinFactory.create(coroutineScope) - val selectedValidators = sharedStateSetup.getSelectedValidators() + val selectedValidators = sharedStateSetup.getNewValidators() setupStakingTypeSelectionMixin.selectValidatorsAndApply(selectedValidators, stakingOption) + sharedStateSetup.reset() stakingRouter.finishSetupValidatorsFlow() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/search/SearchCustomValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/search/SearchCustomValidatorsViewModel.kt index cce38da51e..d2af5707be 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/search/SearchCustomValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/search/SearchCustomValidatorsViewModel.kt @@ -49,7 +49,7 @@ class SearchCustomValidatorsViewModel( .share() private val selectedValidators = confirmSetupState - .map { it.validators.toSet() } + .map { it.newValidators.toSet() } .inBackground() .share() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/select/SelectCustomValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/select/SelectCustomValidatorsViewModel.kt index 3c6eac6312..676068addb 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/select/SelectCustomValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/custom/select/SelectCustomValidatorsViewModel.kt @@ -21,7 +21,6 @@ import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settin import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settings.sortings.TotalStakeSorting import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settings.sortings.ValidatorOwnStakeSorting import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter -import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingProcess import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingSharedState import io.novafoundation.nova.feature_staking_impl.presentation.mappers.mapValidatorToValidatorDetailsParcelModel import io.novafoundation.nova.feature_staking_impl.presentation.mappers.mapValidatorToValidatorModel @@ -29,6 +28,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.validators.chang import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.activeStake import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.custom.common.CustomValidatorsPayload import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.custom.select.model.ContinueButtonState +import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.getSelectedValidatorsOrNull import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.setCustomValidators import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.StakeTargetDetailsPayload import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.relaychain @@ -40,11 +40,11 @@ import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @@ -221,8 +221,8 @@ class SelectCustomValidatorsViewModel( private fun observeExternalSelectionChanges() { setupStakingSharedState.setupStakingProcess - .filterIsInstance() - .onEach { selectedValidators.value = it.validators.asSetItems() } + .mapNotNull { it.getSelectedValidatorsOrNull() } + .onEach { validators -> selectedValidators.value = validators.asSetItems() } .launchIn(viewModelScope) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/recommended/RecommendedValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/recommended/RecommendedValidatorsViewModel.kt index 00f48fa65f..2fa2e71d1b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/recommended/RecommendedValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/recommended/RecommendedValidatorsViewModel.kt @@ -13,13 +13,12 @@ import io.novafoundation.nova.feature_staking_impl.domain.StakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.recommendations.ValidatorRecommenderFactory import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settings.RecommendationSettingsProviderFactory import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter -import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingProcess.ReadyToSubmit -import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingProcess.ReadyToSubmit.SelectionMethod import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingSharedState import io.novafoundation.nova.feature_staking_impl.presentation.mappers.mapValidatorToValidatorDetailsParcelModel import io.novafoundation.nova.feature_staking_impl.presentation.mappers.mapValidatorToValidatorModel import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.ValidatorStakeTargetModel import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.activeStake +import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.retractRecommended import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.setRecommendedValidators import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.StakeTargetDetailsPayload import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.relaychain @@ -70,7 +69,7 @@ class RecommendedValidatorsViewModel( }.inBackground().share() fun backClicked() { - retractRecommended() + sharedStateSetup.retractRecommended() router.back() } @@ -99,12 +98,4 @@ class RecommendedValidatorsViewModel( mapValidatorToValidatorModel(chain, it, addressIconGenerator, token) } } - - private fun retractRecommended() = sharedStateSetup.mutate { - if (it is ReadyToSubmit && it.selectionMethod == SelectionMethod.RECOMMENDED) { - it.previous() - } else { - it - } - } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/start/StartChangeValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/start/StartChangeValidatorsViewModel.kt index 04fccfb448..f27614c088 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/start/StartChangeValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/start/StartChangeValidatorsViewModel.kt @@ -13,13 +13,13 @@ import io.novafoundation.nova.feature_staking_impl.R import io.novafoundation.nova.feature_staking_impl.domain.StakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.recommendations.ValidatorRecommenderFactory import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter -import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingProcess import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingSharedState import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.activeStake +import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.getSelectedValidatorsOrEmpty import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.reset import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class Texts( @@ -41,32 +41,31 @@ class StartChangeValidatorsViewModel( private val maxValidatorsPerNominator = flowOf { interactor.maxValidatorsPerNominator(setupStakingSharedState.activeStake()) - }.share() + }.shareInBackground() val validatorsLoading = MutableStateFlow(true) - val customValidatorsTexts = setupStakingSharedState.setupStakingProcess.transform { - when { - it is SetupStakingProcess.ReadyToSubmit && it.validators.isNotEmpty() -> emit( - Texts( - toolbarTitle = resourceManager.getString(R.string.staking_change_validators), - selectManuallyTitle = resourceManager.getString(R.string.staking_select_custom), - selectManuallyBadge = resourceManager.getString( - R.string.staking_max_format, - it.validators.size, - maxValidatorsPerNominator.first() - ) + val customValidatorsTexts = setupStakingSharedState.setupStakingProcess.map { + val selectedValidators = it.getSelectedValidatorsOrEmpty() + + if (selectedValidators.isNotEmpty()) { + Texts( + toolbarTitle = resourceManager.getString(R.string.staking_change_validators), + selectManuallyTitle = resourceManager.getString(R.string.staking_select_custom), + selectManuallyBadge = resourceManager.getString( + R.string.staking_max_format, + selectedValidators.size, + maxValidatorsPerNominator.first() ) ) - else -> emit( - Texts( - toolbarTitle = resourceManager.getString(R.string.staking_set_validators), - selectManuallyTitle = resourceManager.getString(R.string.staking_select_custom), - selectManuallyBadge = null - ) + } else { + Texts( + toolbarTitle = resourceManager.getString(R.string.staking_set_validators), + selectManuallyTitle = resourceManager.getString(R.string.staking_select_custom), + selectManuallyBadge = null ) } - } + }.shareInBackground() init { launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/current/CurrentValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/current/CurrentValidatorsViewModel.kt index 89b55eeb24..8ec5486d08 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/current/CurrentValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/current/CurrentValidatorsViewModel.kt @@ -8,7 +8,6 @@ import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.flowOf -import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.toHexAccountId import io.novafoundation.nova.common.utils.withLoading import io.novafoundation.nova.common.validation.ValidationExecutor @@ -21,7 +20,6 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.controller import io.novafoundation.nova.feature_staking_impl.domain.validators.current.CurrentValidatorsInteractor import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingProcess -import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingProcess.ReadyToSubmit.SelectionMethod import io.novafoundation.nova.feature_staking_impl.presentation.common.SetupStakingSharedState import io.novafoundation.nova.feature_staking_impl.presentation.common.currentStakeTargets.CurrentStakeTargetsViewModel import io.novafoundation.nova.feature_staking_impl.presentation.common.currentStakeTargets.model.SelectedStakeTargetModel @@ -64,8 +62,7 @@ class CurrentValidatorsViewModel( private val stashFlow = stakingInteractor.selectedAccountStakingStateFlow(viewModelScope) .filterIsInstance() - .inBackground() - .share() + .shareInBackground() private val assetFlow = assetUseCase.currentAssetFlow() .shareInBackground() @@ -76,13 +73,11 @@ class CurrentValidatorsViewModel( private val groupedCurrentValidatorsFlow = combineToPair(stashFlow, activeStakeFlow) .flatMapLatest { (stash, activeStake) -> currentValidatorsInteractor.nominatedValidatorsFlow(stash, activeStake, viewModelScope) } - .inBackground() - .share() + .shareInBackground() private val flattenCurrentValidators = groupedCurrentValidatorsFlow .map { it.toValueList() } - .inBackground() - .share() + .shareInBackground() private val tokenFlow = assetFlow .map { it.token } @@ -150,10 +145,9 @@ class CurrentValidatorsViewModel( private fun openStartChangeValidators() { launch { - val currentState = setupStakingSharedState.get() val currentValidators = flattenCurrentValidators.first().map(NominatedValidator::validator) val activeStake = activeStakeFlow.first() - val newState = currentState.next(activeStake, currentValidators, SelectionMethod.CUSTOM) + val newState = SetupStakingProcess.Initial.next(activeStake, currentValidators) setupStakingSharedState.set(newState) router.openStartChangeValidators() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/StakeTargetDetailsPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/StakeTargetDetailsPayload.kt index 320380b6a2..abee71d5f0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/StakeTargetDetailsPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/StakeTargetDetailsPayload.kt @@ -19,7 +19,7 @@ class StakeTargetDetailsPayload( @Parcelize class DisplayConfig( val rewardSuffix: RewardSuffix, - val rewardedStakersPerStakeTarget: Int, + val rewardedStakersPerStakeTarget: Int?, @StringRes val titleRes: Int, @StringRes val stakersLabelRes: Int, @StringRes val oversubscribedWarningText: Int, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/ValidatorDetailsFragment.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/ValidatorDetailsFragment.kt index e599320b7e..36cbc37777 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/ValidatorDetailsFragment.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/details/ValidatorDetailsFragment.kt @@ -131,7 +131,7 @@ class ValidatorDetailsFragment : BaseFragment() { return AlertView(requireContext()).also { alertView -> alertView.setStylePreset(style) - alertView.setText(alert.descriptionRes) + alertView.setMessage(alert.descriptionRes) alertView.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { params -> params.updateMarginsRelative(start = 16.dp, end = 16.dp, top = 12.dp) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 21f714bc52..1e827dd495 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks @@ -66,7 +67,7 @@ class SwapFee( ) : GenericFee val SwapFee.totalDeductedPlanks: Balance - get() = networkFee.amount + minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn + get() = networkFee.amountByRequestedAccount + minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn sealed class MinimumBalanceBuyIn { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 25461c42af..003eb1e32d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -7,10 +7,11 @@ import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.assetConversion import io.novafoundation.nova.common.utils.mutableMultiMapOf import io.novafoundation.nova.common.utils.put +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.InlineFee +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection @@ -112,16 +113,17 @@ private class AssetConversionExchange( } override suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee { - val nativeAssetFee = extrinsicService.estimateFeeV2(chain) { - executeSwap(args, origin = chain.emptyAccountId()) + val nativeAssetFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { + executeSwap(args, sendTo = chain.emptyAccountId()) } return convertNativeFeeToPayingTokenFee(nativeAssetFee, args) } override suspend fun swap(args: SwapExecuteArgs): Result { - return extrinsicService.submitExtrinsicWithSelectedWalletV2(chain) { origin -> - executeSwap(args, origin) + return extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> + // Send swapped funds to the requested origin since it the account doing the swap + executeSwap(args, sendTo = submissionOrigin.requestedOrigin) } } @@ -163,6 +165,9 @@ private class AssetConversionExchange( } } + // TODO we purposefully do not use `nativeTokenFee.amountByRequestedAccount` + // since we have disabled fee payment in custom tokens for accounts where the difference matters (e.g. proxy) + // We should adapt it if we decide to remove the restriction private suspend fun calculateCustomTokenFee( nativeTokenFee: Fee, nativeAsset: Asset, @@ -195,7 +200,7 @@ private class AssetConversionExchange( } return AssetExchangeFee( - networkFee = InlineFee(toBuyNativeFee), + networkFee = SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin), minimumBalanceBuyIn = minimumBalanceBuyIn ) } @@ -211,7 +216,7 @@ private class AssetConversionExchange( return requireNotNull(quotedAmount) } - private suspend fun ExtrinsicBuilder.executeSwap(swapExecuteArgs: SwapExecuteArgs, origin: AccountId) { + private suspend fun ExtrinsicBuilder.executeSwap(swapExecuteArgs: SwapExecuteArgs, sendTo: AccountId) { val path = listOf(swapExecuteArgs.assetIn, swapExecuteArgs.assetOut) .map { asset -> multiLocationConverter.encodableMultiLocationOf(asset) } @@ -225,7 +230,7 @@ private class AssetConversionExchange( "path" to path, "amount_in" to swapLimit.expectedAmountIn, "amount_out_min" to swapLimit.amountOutMin, - "send_to" to origin, + "send_to" to sendTo, "keep_alive" to keepAlive ) ) @@ -237,7 +242,7 @@ private class AssetConversionExchange( "path" to path, "amount_out" to swapLimit.expectedAmountOut, "amount_in_max" to swapLimit.amountInMax, - "send_to" to origin, + "send_to" to sendTo, "keep_alive" to keepAlive ) ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt index dd6e420db6..ffac578b81 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt @@ -40,8 +40,9 @@ class RealSwapTransactionHistoryRepository( val localOperation = with(swapArgs) { OperationLocal.manualSwap( hash = txSubmission.hash, - originAddress = chain.addressOf(txSubmission.origin), + originAddress = chain.addressOf(txSubmission.submissionOrigin.requestedOrigin), assetId = chainAsset.localId, + // Insert fee regardless of who actually paid it fee = feeAsset.withAmountLocal(fee.networkFee.amount), amountIn = assetIn.withAmountLocal(swapLimit.expectedAmountIn), amountOut = assetOut.withAmountLocal(swapLimit.expectedAmountOut), diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 5c5e510610..37d1b29f83 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -13,13 +13,13 @@ import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter +import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory import io.novafoundation.nova.feature_swap_impl.data.repository.RealSwapTransactionHistoryRepository import io.novafoundation.nova.feature_swap_impl.data.repository.SwapTransactionHistoryRepository -import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor -import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_impl.domain.interactor.RealSwapAvailabilityInteractor +import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.domain.swap.RealSwapService import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.RealPriceImpactFormatter @@ -69,9 +69,10 @@ class SwapFeatureModule { fun provideSwapService( assetConversionExchangeFactory: AssetConversionExchangeFactory, computationalCache: ComputationalCache, - chainRegistry: ChainRegistry + chainRegistry: ChainRegistry, + accountRepository: AccountRepository ): SwapService { - return RealSwapService(assetConversionExchangeFactory, computationalCache, chainRegistry) + return RealSwapService(assetConversionExchangeFactory, computationalCache, chainRegistry, accountRepository) } @Provides diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 28149e90a5..07bde098af 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -40,6 +40,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.A import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.incomingCrossChainDirectionsAvailable +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -88,20 +89,23 @@ class SwapInteractor( return swapService.quote(quoteArgs) } - suspend fun executeSwap(swapExecuteArgs: SwapExecuteArgs, swapFee: SwapFee): Result = withContext(Dispatchers.IO) { + suspend fun executeSwap( + swapExecuteArgs: SwapExecuteArgs, + decimalFee: GenericDecimalFee + ): Result = withContext(Dispatchers.IO) { swapService.swap(swapExecuteArgs) .onSuccess { submission -> swapTransactionHistoryRepository.insertPendingSwap( chainAsset = swapExecuteArgs.assetIn, swapArgs = swapExecuteArgs, - fee = swapFee, + fee = decimalFee.genericFee, txSubmission = submission ) swapTransactionHistoryRepository.insertPendingSwap( chainAsset = swapExecuteArgs.assetOut, swapArgs = swapExecuteArgs, - fee = swapFee, + fee = decimalFee.genericFee, txSubmission = submission ) } @@ -174,7 +178,7 @@ class SwapInteractor( feeAsset: Chain.Asset, quoteArgs: SwapQuoteArgs, swapQuote: SwapQuote, - swapFee: SwapFee + swapFee: GenericDecimalFee ): SwapValidationPayload? { val metaAccount = accountRepository.getSelectedMetaAccount() val chainIn = chainRegistry.getChain(swapQuote.assetIn.chainId) @@ -199,7 +203,7 @@ class SwapInteractor( ), slippage = quoteArgs.slippage, feeAsset = walletRepository.getAsset(metaAccount.id, feeAsset) ?: return null, - swapFee = swapFee, + decimalFee = swapFee, swapQuote = swapQuote, swapQuoteArgs = quoteArgs, swapExecuteArgs = executeArgs diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 67af34e1a6..36fb88693e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -13,6 +13,8 @@ import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.model.requestedAccountPaysFees import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs @@ -48,7 +50,8 @@ private const val EXCHANGES_CACHE = "RealSwapService.EXCHANGES" internal class RealSwapService( private val assetConversionFactory: AssetConversionExchangeFactory, private val computationalCache: ComputationalCache, - private val chainRegistry: ChainRegistry + private val chainRegistry: ChainRegistry, + private val accountRepository: AccountRepository, ) : SwapService { override suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean = withContext(Dispatchers.Default) { @@ -56,8 +59,11 @@ internal class RealSwapService( val exchange = exchanges(computationScope).getValue(asset.chainId) val isCustomFeeToken = !asset.isCommissionAsset + val currentMetaAccount = accountRepository.getSelectedMetaAccount() - isCustomFeeToken && exchange.canPayFeeInNonUtilityToken(asset) + // TODO we disable custom fee tokens payment for account types where current account is not the one who pays fees (e.g. it is proxied). + // This restriction can be removed once we consider all corner-cases + isCustomFeeToken && exchange.canPayFeeInNonUtilityToken(asset) && currentMetaAccount.type.requestedAccountPaysFees() } override suspend fun assetsAvailableForSwap( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt index 66dd6bee59..38e7960e03 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validOrError +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSelfSufficientAsset @@ -21,7 +22,7 @@ class SufficientBalanceConsideringNonSufficientAssetsValidation( if (!isSelfSufficientAssetOut && assetIn.token.configuration.isCommissionAsset) { val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(value.detailedAssetIn.chain, assetIn.token.configuration) - val fee = value.swapFee.networkFee.amount + val fee = value.decimalFee.networkFee.amountByRequestedAccount return validOrError(assetIn.balanceCountedTowardsEDInPlanks - existentialDeposit >= amount + fee) { SwapValidationFailure.InsufficientBalance.BalanceNotConsiderInsufficientReceiveAsset( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt index 11a34c61e3..b585901d6e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.commissionAssetToSpe import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigInteger @@ -18,7 +19,7 @@ data class SwapValidationPayload( val detailedAssetOut: SwapAssetData, val slippage: Percent, val feeAsset: Asset, - val swapFee: SwapFee, + val decimalFee: GenericDecimalFee, val swapQuote: SwapQuote, val swapQuoteArgs: SwapQuoteArgs, val swapExecuteArgs: SwapExecuteArgs @@ -43,14 +44,14 @@ val SwapValidationPayload.swapAmountInFeeToken: Balance val SwapValidationPayload.toBuyAmountToKeepMainEDInFeeAsset: Balance get() = if (isFeePayingByAssetIn) { - swapFee.minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn + decimalFee.genericFee.minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn } else { BigInteger.ZERO } val SwapValidationPayload.totalDeductedAmountInFeeToken: Balance get() = if (isFeePayingByAssetIn) { - swapFee.totalDeductedPlanks + decimalFee.genericFee.totalDeductedPlanks } else { BigInteger.ZERO } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt index 0a74e1da41..5f4e7b206b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeCh import io.novafoundation.nova.feature_wallet_api.domain.validation.positiveAmount import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalanceConsideringConsumersValidation +import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalanceGeneric import java.math.BigDecimal typealias SwapValidationSystem = ValidationSystem @@ -51,7 +52,7 @@ fun SwapValidationSystemBuilder.sufficientBalanceConsideringConsumersValidation( SwapValidationFailure.InsufficientBalance.BalanceNotConsiderConsumers( nativeAsset = payload.detailedAssetIn.asset.token.configuration, feeAsset = payload.feeAsset.token.configuration, - swapFee = payload.swapFee, + swapFee = payload.decimalFee.genericFee, existentialDeposit = existentialDeposit ) } @@ -65,20 +66,18 @@ fun SwapValidationSystemBuilder.enoughLiquidity(sharedQuoteValidationRetriever: SwapEnoughLiquidityValidation { sharedQuoteValidationRetriever.retrieveQuote(it) } ) -fun SwapValidationSystemBuilder.sufficientBalanceInFeeAsset() = sufficientBalance( +fun SwapValidationSystemBuilder.sufficientBalanceInFeeAsset() = sufficientBalanceGeneric( available = { it.feeAsset.transferable }, amount = { BigDecimal.ZERO }, - fee = { it.feeAsset.token.amountFromPlanks(it.swapFee.networkFee.amount) }, - error = { _, _ -> SwapValidationFailure.NotEnoughFunds.ToPayFee } + fee = { it.decimalFee }, + error = { SwapValidationFailure.NotEnoughFunds.ToPayFee } ) fun SwapValidationSystemBuilder.sufficientBalanceInUsedAsset() = sufficientBalance( available = { it.detailedAssetIn.asset.transferable }, amount = { it.detailedAssetIn.asset.token.amountFromPlanks(it.detailedAssetIn.amountInPlanks) }, - fee = { BigDecimal.ZERO }, - error = { _, _ -> - SwapValidationFailure.NotEnoughFunds.InUsedAsset - } + fee = { null }, + error = { SwapValidationFailure.NotEnoughFunds.InUsedAsset } ) fun SwapValidationSystemBuilder.sufficientAssetOutBalanceToStayAboveED( @@ -89,7 +88,7 @@ fun SwapValidationSystemBuilder.checkForFeeChanges( swapService: SwapService ) = checkForFeeChanges( calculateFee = { swapService.estimateFee(it.swapExecuteArgs) }, - currentFee = { it.feeAsset.token.amountFromPlanks(it.swapFee.networkFee.amount) }, + currentFee = { it.decimalFee }, chainAsset = { it.feeAsset.token.configuration }, error = SwapValidationFailure::FeeChangeDetected ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt index 2422a9bded..2dd5ad8e99 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.validations import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validOrError +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidation import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NotEnoughFunds @@ -24,7 +25,7 @@ class EnoughNativeAssetBalanceToPayFeeConsideringEDValidation( if (feeChainAsset.isCommissionAsset) { val chain = chainRegistry.getChain(feeChainAsset.chainId) val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, feeChainAsset) - return validOrError(value.feeAsset.balanceCountedTowardsEDInPlanks - value.swapFee.networkFee.amount >= existentialDeposit) { + return validOrError(value.feeAsset.balanceCountedTowardsEDInPlanks - value.decimalFee.networkFee.amountByRequestedAccount >= existentialDeposit) { NotEnoughFunds.ToPayFeeAndStayAboveED(value.feeAsset.token.configuration) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt index d8141e3f74..bd694f1636 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt @@ -28,16 +28,16 @@ class SwapFeeSufficientBalanceValidation : SwapValidation { val toBuyAmountToKeepEDInFeeAsset = value.toBuyAmountToKeepMainEDInFeeAsset return if (toBuyAmountToKeepEDInFeeAsset.isZero) { - InsufficientBalance.NoNeedsToBuyMainAssetED(chainAssetIn, feeAsset, maxAmountToSwap, value.swapFee.networkFee).validationError() + InsufficientBalance.NoNeedsToBuyMainAssetED(chainAssetIn, feeAsset, maxAmountToSwap, value.decimalFee.networkFee).validationError() } else { InsufficientBalance.NeedsToBuyMainAssetED( value.feeAsset.token.configuration, chainAssetIn, - value.swapFee.minimumBalanceBuyIn.requireNativeAsset(), - toBuyAmountToKeepEDInCommissionAsset = value.swapFee.minimumBalanceBuyIn.nativeMinimumBalance, + value.decimalFee.genericFee.minimumBalanceBuyIn.requireNativeAsset(), + toBuyAmountToKeepEDInCommissionAsset = value.decimalFee.genericFee.minimumBalanceBuyIn.nativeMinimumBalance, toSellAmountToKeepEDUsingAssetIn = toBuyAmountToKeepEDInFeeAsset, maxAmountToSwap, - value.swapFee.networkFee + value.decimalFee.networkFee ).validationError() } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt index c166f3313d..66cc8fa729 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt @@ -39,12 +39,12 @@ class SwapSmallRemainingBalanceValidation( TooSmallRemainingBalance.NeedsToBuyMainAssetED( feeChainAsset, chainAssetIn, - value.swapFee.minimumBalanceBuyIn.requireNativeAsset(), + value.decimalFee.genericFee.minimumBalanceBuyIn.requireNativeAsset(), assetInExistentialDeposit, - toBuyAmountToKeepEDInCommissionAsset = value.swapFee.minimumBalanceBuyIn.nativeMinimumBalance, + toBuyAmountToKeepEDInCommissionAsset = value.decimalFee.genericFee.minimumBalanceBuyIn.nativeMinimumBalance, toSellAmountToKeepEDUsingAssetIn = toBuyAmountToKeepEDInFeeAsset, remainingBalance, - value.swapFee.networkFee + value.decimalFee.networkFee ).validationError() } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt index aa4db12ade..634b63d2b7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt @@ -11,7 +11,7 @@ import io.novafoundation.nova.common.mixin.impl.observeValidations import io.novafoundation.nova.common.utils.applyStatusBarInsets import io.novafoundation.nova.common.view.bottomSheet.description.observeDescription import io.novafoundation.nova.common.view.setProgress -import io.novafoundation.nova.common.view.setTextOrHide +import io.novafoundation.nova.common.view.setMessageOrHide import io.novafoundation.nova.common.view.showValueOrHide import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.showWallet import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions @@ -86,7 +86,7 @@ class SwapConfirmationFragment : BaseFragment() { viewModel.wallet.observe { swapConfirmationWallet.showWallet(it) } viewModel.addressFlow.observe { swapConfirmationAccount.showAddress(it) } - viewModel.slippageAlertMessage.observe { swapConfirmationAlert.setTextOrHide(it) } + viewModel.slippageAlertMessage.observe { swapConfirmationAlert.setMessageOrHide(it) } viewModel.validationProgress.observe(swapConfirmationButton::setProgress) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 1bf33eb0c7..df5bacff48 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -60,7 +60,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.Max import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.getFeeOrNull +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.getDecimalFeeOrNull import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel @@ -253,7 +253,7 @@ class SwapConfirmationViewModel( } private fun executeSwap(validationPayload: SwapValidationPayload) = launch { - swapInteractor.executeSwap(validationPayload.swapExecuteArgs, validationPayload.swapFee) + swapInteractor.executeSwap(validationPayload.swapExecuteArgs, validationPayload.decimalFee) .onSuccess { navigateToNextScreen(validationPayload.swapExecuteArgs.assetIn) } .onFailure(::showError) @@ -310,7 +310,7 @@ class SwapConfirmationViewModel( private suspend fun getValidationPayload(): SwapValidationPayload? { val confirmationState = confirmationStateFlow.value ?: return null - val swapFee = feeMixin.getFeeOrNull() ?: return null + val swapFee = feeMixin.getDecimalFeeOrNull() ?: return null return swapInteractor.getValidationPayload( assetIn = confirmationState.swapQuote.assetIn, assetOut = confirmationState.swapQuote.assetOut, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt index 175f6881d2..71d86d2da6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt @@ -3,9 +3,10 @@ package io.novafoundation.nova.feature_swap_impl.presentation.confirmation.paylo import android.os.Parcelable import io.novafoundation.nova.feature_swap_api.presentation.model.SwapDirectionModel import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload -import java.math.BigDecimal import kotlinx.android.parcel.Parcelize +import java.math.BigDecimal @Parcelize class SwapConfirmationPayload( @@ -28,7 +29,7 @@ class SwapConfirmationPayload( @Parcelize class FeeDetails( - val amount: Balance, + val networkFee: FeeParcelModel, val minimumBalanceBuyIn: MinimumBalanceBuyIn ) : Parcelable { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt index ded17a3363..ee7e08b01c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt @@ -1,13 +1,15 @@ package io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload import io.novafoundation.nova.common.utils.asPercent -import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.presentation.model.mapFromModel import io.novafoundation.nova.feature_swap_api.presentation.model.mapToModel import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload import io.novafoundation.nova.runtime.ext.fullId @@ -53,11 +55,14 @@ class SwapConfirmationPayloadFormatter( SwapConfirmationPayload.FeeDetails.MinimumBalanceBuyIn.NoBuyInNeeded -> MinimumBalanceBuyIn.NoBuyInNeeded } - return SwapFee(InlineFee(model.amount), minimumBalanceBuyIn) + + val decimalFee = mapFeeFromParcel(model.networkFee) + + return SwapFee(decimalFee.networkFee, minimumBalanceBuyIn) } - fun mapFeeToModel(swapFee: SwapFee): SwapConfirmationPayload.FeeDetails { - val minimumBalanceBuyIn = when (val minimumBalanceBuyIn = swapFee.minimumBalanceBuyIn) { + fun mapFeeToModel(swapFee: GenericDecimalFee): SwapConfirmationPayload.FeeDetails { + val minimumBalanceBuyIn = when (val minimumBalanceBuyIn = swapFee.genericFee.minimumBalanceBuyIn) { is MinimumBalanceBuyIn.NeedsToBuyMinimumBalance -> { val nativeAsset = minimumBalanceBuyIn.nativeAsset.fullId.toAssetPayload() val commissionAsset = minimumBalanceBuyIn.commissionAsset.fullId.toAssetPayload() @@ -71,6 +76,6 @@ class SwapConfirmationPayloadFormatter( MinimumBalanceBuyIn.NoBuyInNeeded -> SwapConfirmationPayload.FeeDetails.MinimumBalanceBuyIn.NoBuyInNeeded } - return SwapConfirmationPayload.FeeDetails(swapFee.networkFee.amount, minimumBalanceBuyIn) + return SwapConfirmationPayload.FeeDetails(mapFeeToParcel(swapFee), minimumBalanceBuyIn) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 1952570185..dd675020c8 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -85,6 +85,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedDecimalFeeOrNullFlow import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedFeeModelOrNullFlow import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedFeeOrNullFlow import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload @@ -773,7 +774,7 @@ class SwapMainSettingsViewModel( feeAsset = swapSettings.feeAsset ?: return null, quoteArgs = quotingState.quoteArgs, swapQuote = quotingState.value, - swapFee = feeMixin.loadedFeeOrNullFlow().first() ?: return null + swapFee = feeMixin.loadedDecimalFeeOrNullFlow().first() ?: return null ) } @@ -814,7 +815,7 @@ class SwapMainSettingsViewModel( feeAsset = validPayload.feeAsset.token.configuration.fullId.toAssetPayload(), rate = validPayload.swapQuote.swapRate(), slippage = validPayload.slippage.value, - swapFee = swapConfirmationPayloadFormatter.mapFeeToModel(validPayload.swapFee) + swapFee = swapConfirmationPayloadFormatter.mapFeeToModel(validPayload.decimalFee) ) swapRouter.openSwapConfirmation(payload) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt index 79797de558..b1f4a076ac 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt @@ -7,17 +7,18 @@ import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationFlowActions import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.asDefault +import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.TooSmallRemainingBalance +import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.AmountOutIsTooLowToStayAboveED import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.FeeChangeDetected import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.InsufficientBalance -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.AmountOutIsTooLowToStayAboveED -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NotEnoughLiquidity -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NonPositiveAmount -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NewRateExceededSlippage import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.InvalidSlippage +import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NewRateExceededSlippage +import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NonPositiveAmount import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NotEnoughFunds +import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NotEnoughLiquidity +import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.TooSmallRemainingBalance import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.validation.amountIsTooBig @@ -28,9 +29,9 @@ import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatT import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleInsufficientBalanceCommission import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleNonPositiveAmount import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.CoroutineScope import java.math.BigDecimal import java.math.BigInteger -import kotlinx.coroutines.CoroutineScope fun CoroutineScope.mapSwapValidationFailureToUI( resourceManager: ResourceManager, @@ -85,7 +86,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( message = resourceManager.getString( R.string.swap_failure_too_small_remaining_balance_with_buy_ed_message, reason.assetInExistentialDeposit.formatPlanks(reason.assetIn), - reason.fee.amount.formatPlanks(reason.feeAsset), + reason.fee.amountByRequestedAccount.formatPlanks(reason.feeAsset), reason.toSellAmountToKeepEDUsingAssetIn.formatPlanks(reason.assetIn), reason.toBuyAmountToKeepEDInCommissionAsset.formatPlanks(reason.nativeAsset), reason.nativeAsset.symbol, @@ -101,7 +102,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( message = resourceManager.getString( R.string.swap_failure_insufficient_balance_message, reason.maxSwapAmount.formatPlanks(reason.assetIn), - reason.fee.amount.formatPlanks(reason.feeAsset) + reason.fee.amountByRequestedAccount.formatPlanks(reason.feeAsset) ), resourceManager = resourceManager, positiveButtonClick = amountInSwapMaxAction @@ -112,7 +113,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( message = resourceManager.getString( R.string.swap_failure_insufficient_balance_with_buy_ed_message, reason.maxSwapAmount.formatPlanks(reason.assetIn), - reason.fee.amount.formatPlanks(reason.feeAsset), + reason.fee.amountByRequestedAccount.formatPlanks(reason.feeAsset), reason.toSellAmountToKeepEDUsingAssetIn.formatPlanks(reason.assetIn), reason.toBuyAmountToKeepEDInCommissionAsset.formatPlanks(reason.nativeAsset), reason.nativeAsset.symbol @@ -126,7 +127,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( resourceManager.getString( R.string.swap_failure_balance_not_consider_consumers, reason.existentialDeposit.formatPlanks(reason.nativeAsset), - reason.swapFee.networkFee.amount.formatPlanks(reason.feeAsset) + reason.swapFee.networkFee.amountByRequestedAccount.formatPlanks(reason.feeAsset) ) ).asDefault() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsFragment.kt index c44fe4bf86..052e68b87b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsFragment.kt @@ -12,7 +12,7 @@ import io.novafoundation.nova.common.utils.bindTo import io.novafoundation.nova.common.validation.observeErrors import io.novafoundation.nova.common.view.bottomSheet.description.observeDescription import io.novafoundation.nova.common.view.setState -import io.novafoundation.nova.common.view.setTextOrHide +import io.novafoundation.nova.common.view.setMessageOrHide import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent @@ -62,7 +62,7 @@ class SwapOptionsFragment : BaseFragment() { } viewModel.buttonState.observe { swapOptionsApplyButton.setState(it) } swapOptionsSlippageInput.observeErrors(viewModel.slippageInputValidationResult, viewModel.viewModelScope) - viewModel.slippageWarningState.observe { swapOptionsAlert.setTextOrHide(it) } + viewModel.slippageWarningState.observe { swapOptionsAlert.setMessageOrHide(it) } viewModel.resetButtonEnabled.observe { swapOptionsToolbar.setRightActionEnabled(it) } } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/cache/AssetCache.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/cache/AssetCache.kt index 4238d7c53d..bf50ae6e2b 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/cache/AssetCache.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/cache/AssetCache.kt @@ -56,7 +56,7 @@ class AssetCache( chainAsset: Chain.Asset, builder: (local: AssetLocal) -> AssetLocal, ): Boolean = withContext(Dispatchers.IO) { - val applicableMetaAccount = accountRepository.findMetaAccount(accountId) + val applicableMetaAccount = accountRepository.findMetaAccount(accountId, chainAsset.chainId) applicableMetaAccount?.let { updateAsset(it.id, chainAsset, builder) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt index c8048510e8..2f0db67ad0 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt @@ -1,17 +1,13 @@ package io.novafoundation.nova.feature_wallet_api.data.mappers import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks -import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericFeeModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel -import java.math.BigDecimal fun mapFeeToFeeModel( fee: F, @@ -37,13 +33,3 @@ fun mapFeeToFeeModel( token: Token, includeZeroFiat: Boolean = true ) = mapFeeToFeeModel(SimpleFee(fee), token, includeZeroFiat) - -@Suppress("DeprecatedCallableAddReplaceWith") -@Deprecated("Backward-compatible adapter") -fun mapFeeToFeeModel( - feeAmount: BigDecimal, - token: Token, - includeZeroFiat: Boolean = true -): FeeModel { - return mapFeeToFeeModel(InlineFee(token.planksFromAmount(feeAmount)), token, includeZeroFiat) -} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt index 208365a2ed..abc18282d9 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount @@ -35,6 +36,12 @@ interface AssetBalance { chainAsset: Chain.Asset ): BigInteger + suspend fun queryAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId + ): AccountBalance + suspend fun queryTotalBalance( chain: Chain, chainAsset: Chain.Asset, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt index 0c353d9f4b..5d767e3dc0 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt @@ -3,8 +3,8 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets. import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Extractor import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents interface SubstrateRealtimeOperationFetcher { @@ -17,7 +17,7 @@ interface SubstrateRealtimeOperationFetcher { interface Extractor { suspend fun extractRealtimeHistoryUpdates( - extrinsic: ExtrinsicWithEvents, + extrinsicVisit: ExtrinsicVisit, chain: Chain, chainAsset: Chain.Asset ): RealtimeHistoryUpdate.Type? diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt index 1309f9f421..02e9d839fb 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDete import io.novafoundation.nova.feature_wallet_api.domain.validation.InsufficientBalanceToStayAboveEDError import io.novafoundation.nova.feature_wallet_api.domain.validation.NotEnoughToPayFeesError import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -68,8 +69,8 @@ sealed class AssetTransferValidationFailure { data class AssetTransferPayload( val transfer: WeightedAssetTransfer, - val originFee: BigDecimal, - val crossChainFee: BigDecimal?, + val originFee: DecimalFee, + val crossChainFee: DecimalFee?, val originCommissionAsset: Asset, val originUsedAsset: Asset ) @@ -80,11 +81,11 @@ val AssetTransferPayload.isSendingCommissionAsset val AssetTransferPayload.isReceivingCommissionAsset get() = transfer.destinationChainAsset == transfer.destinationChain.commissionAsset -val AssetTransferPayload.originFeeInUsedAsset: BigDecimal +val AssetTransferPayload.originFeeInUsedAsset: DecimalFee? get() = if (isSendingCommissionAsset) { originFee } else { - BigDecimal.ZERO + null } val AssetTransferPayload.receivingAmountInCommissionAsset: BigInteger diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt index b1b8f9407b..31af3e14bd 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.domain.model.Token @@ -7,9 +8,9 @@ import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.ext.accountIdOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId -import java.math.BigDecimal import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf +import java.math.BigDecimal interface AssetTransfer { val sender: MetaAccount @@ -71,7 +72,7 @@ interface AssetTransfers { suspend fun calculateFee(transfer: AssetTransfer): Fee - suspend fun performTransfer(transfer: WeightedAssetTransfer): Result + suspend fun performTransfer(transfer: WeightedAssetTransfer): Result suspend fun totalCanDropBelowMinimumBalance(chainAsset: Chain.Asset): Boolean { return true diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt index 7ade311ae8..5fa0be13ca 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.data.network.crosschain +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem @@ -19,5 +20,5 @@ interface CrossChainTransactor { configuration: CrossChainTransferConfiguration, transfer: AssetTransfer, crossChainFee: BigInteger, - ): Result<*> + ): Result } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt index 11a85c2ae2..8a22d8f4af 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt @@ -20,6 +20,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstan import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory +import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard @@ -44,6 +45,8 @@ interface WalletFeatureApi { fun enoughTotalToStayAboveEDValidationFactory(): EnoughTotalToStayAboveEDValidationFactory + fun proxyHaveEnoughFeeValidationFactory(): ProxyHaveEnoughFeeValidationFactory + val phishingValidationFactory: PhishingValidationFactory val crossChainTransfersRepository: CrossChainTransfersRepository diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt index d96bbfbb9b..11d334e60a 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt @@ -8,6 +8,9 @@ import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -17,18 +20,29 @@ interface NotEnoughToPayFeesError { val fee: BigDecimal } -class EnoughAmountToTransferValidation( - private val feeExtractor: AmountProducer

, +typealias EnoughAmountToTransferValidation = EnoughAmountToTransferValidationGeneric + +class EnoughAmountToTransferValidationGeneric( + private val feeExtractor: GenericFeeProducer, private val availableBalanceProducer: AmountProducer

, - private val errorProducer: (P, availableToPayFees: BigDecimal) -> E, + private val errorProducer: (ErrorContext

) -> E, private val skippable: Boolean = false, private val extraAmountExtractor: AmountProducer

= { BigDecimal.ZERO }, ) : Validation { + class ErrorContext

( + + val payload: P, + + val availableToPayFees: BigDecimal, + + val fee: BigDecimal, + ) + companion object; override suspend fun validate(value: P): ValidationStatus { - val fee = feeExtractor(value) + val fee = feeExtractor(value).networkFeeByRequestedAccountOrZero val available = availableBalanceProducer(value) val amount = extraAmountExtractor(value) @@ -39,16 +53,16 @@ class EnoughAmountToTransferValidation( val failureLevel = if (skippable) DefaultFailureLevel.WARNING else DefaultFailureLevel.ERROR - ValidationStatus.NotValid(failureLevel, errorProducer(value, maxUsable)) + ValidationStatus.NotValid(failureLevel, errorProducer(ErrorContext(value, maxUsable, fee))) } } } fun ValidationSystemBuilder.sufficientBalance( - fee: AmountProducer

= { BigDecimal.ZERO }, + fee: FeeProducer

= { null }, amount: AmountProducer

= { BigDecimal.ZERO }, available: AmountProducer

, - error: (P, availableToPayFees: BigDecimal) -> E, + error: (EnoughAmountToTransferValidationGeneric.ErrorContext

) -> E, skippable: Boolean = false ) = validate( EnoughAmountToTransferValidation( @@ -60,6 +74,22 @@ fun ValidationSystemBuilder.sufficientBalance( ) ) +fun ValidationSystemBuilder.sufficientBalanceGeneric( + fee: GenericFeeProducer = { null }, + amount: AmountProducer

= { BigDecimal.ZERO }, + available: AmountProducer

, + error: (EnoughAmountToTransferValidationGeneric.ErrorContext

) -> E, + skippable: Boolean = false +) = validate( + EnoughAmountToTransferValidationGeneric( + feeExtractor = fee, + extraAmountExtractor = amount, + errorProducer = error, + skippable = skippable, + availableBalanceProducer = available + ) +) + fun ResourceManager.notSufficientBalanceToPayFeeErrorMessage() = getString(R.string.common_not_enough_funds_title) to getString(R.string.common_not_enough_funds_message) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt index d72d859eba..bde43a625f 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.validOrError import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDeposit +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -16,7 +17,7 @@ interface InsufficientBalanceToStayAboveEDError { class EnoughBalanceToStayAboveEDValidation( private val assetSourceRegistry: AssetSourceRegistry, - private val fee: AmountProducer

, + private val fee: FeeProducer

, private val balance: AmountProducer

, private val chainWithAsset: (P) -> ChainWithAsset, private val error: (P, BigDecimal) -> E @@ -26,7 +27,7 @@ class EnoughBalanceToStayAboveEDValidation( val chain = chainWithAsset(value).chain val asset = chainWithAsset(value).asset val existentialDeposit = assetSourceRegistry.existentialDeposit(chain, asset) - return validOrError(balance(value) - fee(value) >= existentialDeposit) { + return validOrError(balance(value) - fee(value).networkFeeByRequestedAccountOrZero >= existentialDeposit) { error(value, existentialDeposit) } } @@ -35,7 +36,7 @@ class EnoughBalanceToStayAboveEDValidation( class EnoughTotalToStayAboveEDValidationFactory(private val assetSourceRegistry: AssetSourceRegistry) { fun create( - fee: AmountProducer

, + fee: FeeProducer

, balance: AmountProducer

, chainWithAsset: (P) -> ChainWithAsset, error: (P, BigDecimal) -> E @@ -52,7 +53,7 @@ class EnoughTotalToStayAboveEDValidationFactory(private val assetSourceRegistry: context(ValidationSystemBuilder) fun EnoughTotalToStayAboveEDValidationFactory.validate( - fee: AmountProducer

, + fee: FeeProducer

, balance: AmountProducer

, chainWithAsset: (P) -> ChainWithAsset, error: (P, BigDecimal) -> E diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt index 95eaeef657..57277f61ff 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt @@ -4,13 +4,14 @@ import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.validOrWarning +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import java.math.BigDecimal typealias ExistentialDepositError = (remainingAmount: BigDecimal, payload: P) -> E class ExistentialDepositValidation( private val countableTowardsEdBalance: AmountProducer

, - private val feeProducer: AmountProducer

, + private val feeProducer: FeeProducer

, private val extraAmountProducer: AmountProducer

, private val errorProducer: ExistentialDepositError, private val existentialDeposit: AmountProducer

@@ -23,7 +24,7 @@ class ExistentialDepositValidation( val fee = feeProducer(value) val extraAmount = extraAmountProducer(value) - val remainingAmount = countableTowardsEd - fee - extraAmount + val remainingAmount = countableTowardsEd - fee.networkFeeByRequestedAccountOrZero - extraAmount return validOrWarning(remainingAmount >= existentialDeposit) { errorProducer(remainingAmount, value) @@ -33,7 +34,7 @@ class ExistentialDepositValidation( fun ValidationSystemBuilder.doNotCrossExistentialDeposit( countableTowardsEdBalance: AmountProducer

, - fee: AmountProducer

= { BigDecimal.ZERO }, + fee: FeeProducer

= { null }, extraAmount: AmountProducer

= { BigDecimal.ZERO }, existentialDeposit: AmountProducer

, error: ExistentialDepositError, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt index 45ad6ffb1b..ddf3fb4154 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt @@ -14,10 +14,12 @@ import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccount +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -27,7 +29,7 @@ private val FEE_RATIO_THRESHOLD = 1.5.toBigDecimal() class FeeChangeValidation( private val calculateFee: suspend (P) -> GenericDecimalFee, - private val currentFee: (P) -> BigDecimal, + private val currentFee: GenericFeeProducer, private val chainAsset: (P) -> Chain.Asset, private val error: (FeeChangeDetectedFailure.Payload) -> E ) : Validation { @@ -36,12 +38,15 @@ class FeeChangeValidation( val oldFee = currentFee(value) val newFee = calculateFee(value) - val areFeesSame = oldFee hasTheSaveValueAs newFee.decimalAmount + val oldAmount = oldFee.networkFeeByRequestedAccountOrZero + val newAmount = newFee.networkFeeByRequestedAccount + + val areFeesSame = oldAmount hasTheSaveValueAs newAmount return areFeesSame isTrueOrError { val payload = FeeChangeDetectedFailure.Payload( - needsUserAttention = newFee.decimalAmount / oldFee > FEE_RATIO_THRESHOLD, - oldFee = oldFee, + needsUserAttention = newAmount / oldAmount > FEE_RATIO_THRESHOLD, + oldFee = oldAmount, newFee = newFee, chainAsset = chainAsset(value) ) @@ -65,7 +70,7 @@ interface FeeChangeDetectedFailure { fun ValidationSystemBuilder.checkForSimpleFeeChanges( calculateFee: suspend (P) -> Fee, - currentFee: (P) -> BigDecimal, + currentFee: FeeProducer

, chainAsset: (P) -> Chain.Asset, error: (FeeChangeDetectedFailure.Payload) -> E ) { @@ -79,7 +84,7 @@ fun ValidationSystemBuilder.checkForSimpleFeeChanges( fun ValidationSystemBuilder.checkForFeeChanges( calculateFee: suspend (P) -> F, - currentFee: (P) -> BigDecimal, + currentFee: GenericFeeProducer, chainAsset: (P) -> Chain.Asset, error: (FeeChangeDetectedFailure.Payload) -> E ) = validate( @@ -98,16 +103,16 @@ fun ValidationSystemBuilder.checkForFeeChanges( ) ) -fun CoroutineScope.handleFeeSpikeDetected( - error: FeeChangeDetectedFailure, +fun CoroutineScope.handleFeeSpikeDetected( + error: FeeChangeDetectedFailure, resourceManager: ResourceManager, - feeLoaderMixin: FeeLoaderMixin.Presentation, + feeLoaderMixin: GenericFeeLoaderMixin.Presentation, actions: ValidationFlowActions<*> ): TransformedFailure? = handleFeeSpikeDetected( error = error, resourceManager = resourceManager, actions = actions, - setFee = { feeLoaderMixin.setFee(it.newFee.fee) } + setFee = { feeLoaderMixin.setFee(it.newFee.genericFee) } ) fun CoroutineScope.handleFeeSpikeDetected( @@ -123,7 +128,7 @@ fun CoroutineScope.handleFeeSpikeDetected( val chainAsset = error.payload.chainAsset val oldFee = error.payload.oldFee.formatTokenAmount(chainAsset) - val newFee = error.payload.newFee.decimalAmount.formatTokenAmount(chainAsset) + val newFee = error.payload.newFee.networkFeeByRequestedAccount.formatTokenAmount(chainAsset) return TransformedFailure.Custom( Payload( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt index 9b7bab888c..1e421268b7 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.validation.isTrueOrError import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -22,13 +23,13 @@ interface NotEnoughFreeBalanceError { class HasEnoughFreeBalanceValidation( private val asset: (P) -> Asset, - private val fee: AmountProducer

, + private val fee: FeeProducer

, private val requestedAmount: AmountProducer

, private val error: HasEnoughFreeBalanceErrorProducer ) : Validation { override suspend fun validate(value: P): ValidationStatus { - val freeBalanceAfterFees = asset(value).free - fee(value) + val freeBalanceAfterFees = asset(value).free - fee(value).networkFeeByRequestedAccountOrZero return (freeBalanceAfterFees >= requestedAmount(value)) isTrueOrError { error(asset(value).token.configuration, freeBalanceAfterFees) @@ -38,7 +39,7 @@ class HasEnoughFreeBalanceValidation( fun ValidationSystemBuilder.hasEnoughFreeBalance( asset: (P) -> Asset, - fee: AmountProducer

, + fee: FeeProducer

, requestedAmount: AmountProducer

, error: HasEnoughFreeBalanceErrorProducer ) { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt index 720eb2376c..e2cf48675e 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_api.domain.validation -import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import java.math.BigDecimal import java.math.BigInteger @@ -8,4 +9,6 @@ typealias AmountProducer

= suspend (P) -> BigDecimal typealias PlanksProducer

= suspend (P) -> BigInteger -typealias TokenProducer

= suspend (P) -> Token +typealias FeeProducer

= suspend (P) -> DecimalFee? + +typealias GenericFeeProducer = suspend (P) -> GenericDecimalFee? diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ProxyHaveEnoughFeeValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ProxyHaveEnoughFeeValidation.kt new file mode 100644 index 0000000000..8ff9583283 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ProxyHaveEnoughFeeValidation.kt @@ -0,0 +1,101 @@ +package io.novafoundation.nova.feature_wallet_api.domain.validation + +import io.novafoundation.nova.common.utils.atLeastZero +import io.novafoundation.nova.common.validation.Validation +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.common.validation.validOrError +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateBalanceCountedTowardsEd +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +class ProxyHaveEnoughFeeValidationFactory( + private val assetSourceRegistry: AssetSourceRegistry, + private val walletRepository: WalletRepository, + private val extrinsicService: ExtrinsicService +) { + fun create( + proxyAccountId: (P) -> AccountId, + metaAccount: (P) -> MetaAccount, + call: (P) -> GenericCall.Instance, + chainWithAsset: (P) -> ChainWithAsset, + proxyNotEnoughFee: (payload: P, availableBalance: Balance, fee: Fee) -> E, + ): ProxyHaveEnoughFeeValidation { + return ProxyHaveEnoughFeeValidation( + assetSourceRegistry, + walletRepository, + extrinsicService, + metaAccount, + proxyAccountId, + call, + chainWithAsset, + proxyNotEnoughFee + ) + } +} + +class ProxyHaveEnoughFeeValidation( + private val assetSourceRegistry: AssetSourceRegistry, + private val walletRepository: WalletRepository, + private val extrinsicService: ExtrinsicService, + private val metaAccount: (P) -> MetaAccount, + private val proxyAccountId: (P) -> AccountId, + private val call: (P) -> GenericCall.Instance, + private val chainWithAsset: (P) -> ChainWithAsset, + private val proxyNotEnoughFee: (payload: P, availableBalance: Balance, fee: Fee) -> E, +) : Validation { + + override suspend fun validate(value: P): ValidationStatus { + val chain = chainWithAsset(value).chain + val chainAsset = chainWithAsset(value).asset + val fee = calculateFee(metaAccount(value), chain, call(value)) + val asset = walletRepository.getAsset(proxyAccountId(value), chainAsset)!! + + val assetSource = assetSourceRegistry.sourceFor(chainAsset) + val assetBalanceSource = assetSource.balance + + val accountData = assetBalanceSource.queryAccountBalance(chain, chainAsset, proxyAccountId(value)) + + val existentialDeposit = assetBalanceSource.existentialDeposit(chain, chainAsset) + val transferable = asset.transferableMode.calculateTransferable(accountData.free, accountData.frozen, accountData.reserved) + val balanceCountedTowardsEd = asset.edCountingMode.calculateBalanceCountedTowardsEd(accountData.free, accountData.reserved) + val balanceWithoutEd = (balanceCountedTowardsEd - existentialDeposit).atLeastZero() + + return validOrError(transferable >= fee.amount && balanceWithoutEd >= fee.amount) { + proxyNotEnoughFee(value, transferable, fee) + } + } + + private suspend fun calculateFee(metaAccount: MetaAccount, chain: Chain, callInstance: GenericCall.Instance): Fee { + return extrinsicService.estimateFee(chain, metaAccount.intoOrigin()) { + call(callInstance) + } + } +} + +fun ValidationSystemBuilder.proxyHasEnoughFeeValidation( + factory: ProxyHaveEnoughFeeValidationFactory, + metaAccount: (P) -> MetaAccount, + proxyAccountId: (P) -> AccountId, + call: (P) -> GenericCall.Instance, + chainWithAsset: (P) -> ChainWithAsset, + proxyNotEnoughFee: (payload: P, availableBalance: Balance, fee: Fee) -> E, +) = validate( + factory.create( + proxyAccountId, + metaAccount, + call, + chainWithAsset, + proxyNotEnoughFee + ) +) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserMixin.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserMixin.kt index a4e738526d..16c05843cb 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserMixin.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/AmountChooserMixin.kt @@ -13,9 +13,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import java.math.BigDecimal import java.math.BigInteger -import kotlinx.coroutines.flow.first typealias MaxClick = () -> Unit @@ -42,7 +42,6 @@ interface AmountChooserMixinBase : CoroutineScope { interface Presentation : AmountChooserMixinBase { - @Deprecated("Use amountState instead") val amount: Flow val amountState: Flow> diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt index df811d7781..4e261044e2 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow -import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.Retriable import io.novafoundation.nova.common.utils.castOrNull import io.novafoundation.nova.common.utils.inBackground @@ -23,8 +22,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform -import java.math.BigDecimal -import java.math.BigInteger sealed class FeeStatus { object Loading : FeeStatus() @@ -76,12 +73,6 @@ interface GenericFeeLoaderMixin : Retriable { suspend fun setFee(fee: F?) fun invalidateFee() - - @Deprecated( - message = "Use `awaitDecimalFee` instead since it holds more information about fee", - replaceWith = ReplaceWith("awaitDecimalFee().decimalAmount") - ) - suspend fun awaitFee(): BigDecimal = awaitDecimalFee().decimalAmount } interface Factory { @@ -95,28 +86,9 @@ interface GenericFeeLoaderMixin : Retriable { interface FeeLoaderMixin : GenericFeeLoaderMixin { - // Additional methods in this interface are only for backward-compatibility to simplify migration of the old code interface Presentation : GenericFeeLoaderMixin.Presentation, FeeLoaderMixin { - @Deprecated("Use loadFeeV2") fun loadFee( - coroutineScope: CoroutineScope, - feeConstructor: suspend (Token) -> BigInteger?, - onRetryCancelled: () -> Unit, - ) - - @Deprecated("Use setFee(fee: GenericFee)") - suspend fun setFee(feeAmount: BigDecimal?) - - @Deprecated("Use awaitFee()") - fun requireFee( - block: (BigDecimal) -> Unit, - onError: (title: String, message: String) -> Unit, - ) - - suspend fun setFee(fee: Fee?) = setFee(fee?.let(::SimpleFee)) - - fun loadFeeV2( coroutineScope: CoroutineScope, expectedChain: ChainId? = null, feeConstructor: suspend (Token) -> Fee?, @@ -157,70 +129,28 @@ fun GenericFeeLoaderMixin.loadedFeeOrNullFlow(): Flow { } } +fun GenericFeeLoaderMixin.loadedDecimalFeeOrNullFlow(): Flow?> { + return feeLiveData.asFlow().map { + it.castOrNull>()?.feeModel?.decimalFee + } +} + fun GenericFeeLoaderMixin.loadedFeeModelOrNullFlow(): Flow?> { return feeLiveData .asFlow() .map { it.castOrNull>()?.feeModel } } -fun GenericFeeLoaderMixin.getFeeOrNull(): F? { +fun GenericFeeLoaderMixin.getDecimalFeeOrNull(): GenericDecimalFee? { return feeLiveData.value .castOrNull>() ?.feeModel ?.decimalFee - ?.genericFee } fun FeeLoaderMixin.Factory.create(assetFlow: Flow) = create(assetFlow.map { it.token }) fun FeeLoaderMixin.Factory.create(tokenUseCase: TokenUseCase) = create(tokenUseCase.currentTokenFlow()) -fun FeeLoaderMixin.Presentation.requireFee( - viewModel: BaseViewModel, - block: (BigDecimal) -> Unit, -) { - requireFee(block) { title, message -> - viewModel.showError(title, message) - } -} - -fun FeeLoaderMixin.Presentation.connectWith( - inputSource: Flow, - scope: CoroutineScope, - feeConstructor: suspend Token.(input: I) -> BigInteger, - onRetryCancelled: () -> Unit = {} -) { - inputSource.onEach { input -> - loadFee( - coroutineScope = scope, - feeConstructor = { feeConstructor(it, input) }, - onRetryCancelled = onRetryCancelled - ) - } - .inBackground() - .launchIn(scope) -} - -fun FeeLoaderMixin.Presentation.connectWith( - inputSource1: Flow, - inputSource2: Flow, - scope: CoroutineScope, - feeConstructor: suspend Token.(input1: I1, input2: I2) -> BigInteger?, - onRetryCancelled: () -> Unit = {} -) { - combine( - inputSource1, - inputSource2 - ) { input1, input2 -> - loadFee( - coroutineScope = scope, - feeConstructor = { feeConstructor(it, input1, input2) }, - onRetryCancelled = onRetryCancelled - ) - } - .inBackground() - .launchIn(scope) -} - fun FeeLoaderMixin.Presentation.connectWith( inputSource1: Flow, inputSource2: Flow, @@ -237,7 +167,7 @@ fun FeeLoaderMixin.Presentation.connectWith( inputSource3, inputSource4 ) { input1, input2, input3, input4 -> - loadFeeV2( + loadFee( coroutineScope = scope, expectedChain = expectedChain?.invoke(input1, input2, input3, input4), feeConstructor = { feeConstructor(it, input1, input2, input3, input4) }, @@ -248,14 +178,14 @@ fun FeeLoaderMixin.Presentation.connectWith( .launchIn(scope) } -fun FeeLoaderMixin.Presentation.connectWithV2( +fun FeeLoaderMixin.Presentation.connectWith( inputSource: Flow, scope: CoroutineScope, feeConstructor: suspend Token.(input: I) -> Fee, onRetryCancelled: () -> Unit = {} ) { inputSource.onEach { input -> - loadFeeV2( + this.loadFee( coroutineScope = scope, feeConstructor = { feeConstructor(it, input) }, onRetryCancelled = onRetryCancelled @@ -264,3 +194,24 @@ fun FeeLoaderMixin.Presentation.connectWithV2( .inBackground() .launchIn(scope) } + +fun FeeLoaderMixin.Presentation.connectWith( + inputSource1: Flow, + inputSource2: Flow, + scope: CoroutineScope, + feeConstructor: suspend Token.(input1: I1, input2: I2) -> Fee, + onRetryCancelled: () -> Unit = {} +) { + combine( + inputSource1, + inputSource2 + ) { input1, input2 -> + this.loadFee( + coroutineScope = scope, + feeConstructor = { feeConstructor(it, input1, input2) }, + onRetryCancelled = onRetryCancelled + ) + } + .inBackground() + .launchIn(scope) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt index 6e4c5dca62..c3b555b7ce 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt @@ -1,9 +1,12 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee import android.os.Parcelable +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.EvmFee -import io.novafoundation.nova.feature_account_api.data.model.InlineFee +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee +import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.android.parcel.Parcelize import java.math.BigDecimal import java.math.BigInteger @@ -11,33 +14,55 @@ import java.math.BigInteger sealed interface FeeParcelModel : Parcelable { val amount: BigDecimal + + val submissionOrigin: SubmissionOriginParcelModel } +@Parcelize +class SubmissionOriginParcelModel( + val requested: AccountId, + val actual: AccountId +) : Parcelable + @Parcelize class EvmFeeParcelModel( val gasLimit: BigInteger, val gasPrice: BigInteger, - override val amount: BigDecimal + override val amount: BigDecimal, + override val submissionOrigin: SubmissionOriginParcelModel ) : FeeParcelModel @Parcelize class SimpleFeeParcelModel( val planks: BigInteger, - override val amount: BigDecimal + override val amount: BigDecimal, + override val submissionOrigin: SubmissionOriginParcelModel ) : FeeParcelModel -fun mapFeeToParcel(decimalFee: DecimalFee): FeeParcelModel { - return when (val fee = decimalFee.fee) { - is EvmFee -> EvmFeeParcelModel(gasLimit = fee.gasLimit, gasPrice = fee.gasPrice, amount = decimalFee.decimalAmount) - else -> SimpleFeeParcelModel(decimalFee.fee.amount, decimalFee.decimalAmount) +fun mapFeeToParcel(decimalFee: GenericDecimalFee<*>): FeeParcelModel { + val submissionOrigin = mapSubmissionOriginToParcel(decimalFee.networkFee.submissionOrigin) + + return when (val fee = decimalFee.networkFee) { + is EvmFee -> EvmFeeParcelModel(gasLimit = fee.gasLimit, gasPrice = fee.gasPrice, amount = decimalFee.networkFeeDecimalAmount, submissionOrigin) + else -> SimpleFeeParcelModel(decimalFee.networkFee.amount, decimalFee.networkFeeDecimalAmount, submissionOrigin) } } +private fun mapSubmissionOriginToParcel(submissionOrigin: SubmissionOrigin): SubmissionOriginParcelModel { + return with(submissionOrigin) { SubmissionOriginParcelModel(requested = requestedOrigin, actual = actualOrigin) } +} + fun mapFeeFromParcel(parcelFee: FeeParcelModel): DecimalFee { + val submissionOrigin = mapSubmissionOriginFromParcel(parcelFee.submissionOrigin) + val fee = when (parcelFee) { - is EvmFeeParcelModel -> EvmFee(gasLimit = parcelFee.gasLimit, gasPrice = parcelFee.gasPrice) - is SimpleFeeParcelModel -> InlineFee(parcelFee.planks) + is EvmFeeParcelModel -> EvmFee(gasLimit = parcelFee.gasLimit, gasPrice = parcelFee.gasPrice, submissionOrigin) + is SimpleFeeParcelModel -> SubstrateFee(parcelFee.planks, submissionOrigin) } return DecimalFee(SimpleFee(fee), parcelFee.amount) } + +private fun mapSubmissionOriginFromParcel(submissionOrigin: SubmissionOriginParcelModel): SubmissionOrigin { + return with(submissionOrigin) { SubmissionOrigin(requestedOrigin = requested, actualOrigin = actual) } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/GenericFeeLoaderProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/GenericFeeLoaderProvider.kt index 1ce1bbd366..b9dfcd38a3 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/GenericFeeLoaderProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/GenericFeeLoaderProvider.kt @@ -5,11 +5,9 @@ import io.novafoundation.nova.common.mixin.api.RetryPayload import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.firstNotNull -import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -19,8 +17,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal -import java.math.BigInteger class FeeLoaderProviderFactory( private val resourceManager: ResourceManager, @@ -45,47 +41,7 @@ private class FeeLoaderProvider( resourceManager: ResourceManager, configuration: GenericFeeLoaderMixin.Configuration, tokenFlow: Flow, -) : GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow), FeeLoaderMixin.Presentation { - - override fun loadFee( - coroutineScope: CoroutineScope, - feeConstructor: suspend (Token) -> BigInteger?, - onRetryCancelled: () -> Unit, - ) { - coroutineScope.launch { - loadFeeSuspending( - retryScope = coroutineScope, - feeConstructor = { token -> feeConstructor(token)?.let { SimpleFee(InlineFee(it)) } }, - onRetryCancelled = onRetryCancelled - ) - } - } - - override suspend fun setFee(feeAmount: BigDecimal?) { - val fee = feeAmount?.let { - val token = tokenFlow.firstNotNull() - InlineFee(token.planksFromAmount(feeAmount)) - } - - setFee(fee) - } - - override fun requireFee( - block: (BigDecimal) -> Unit, - onError: (title: String, message: String) -> Unit, - ) { - val feeStatus = feeLiveData.value - - if (feeStatus is FeeStatus.Loaded) { - block(feeStatus.feeModel.decimalFee.decimalAmount) - } else { - onError( - resourceManager.getString(R.string.fee_not_yet_loaded_title), - resourceManager.getString(R.string.fee_not_yet_loaded_message) - ) - } - } -} +) : GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow), FeeLoaderMixin.Presentation private open class GenericFeeLoaderProvider( protected val resourceManager: ResourceManager, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt index 24e274ee46..204780318c 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt @@ -1,12 +1,13 @@ package io.novafoundation.nova.feature_wallet_api.presentation.model +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.requestedAccountPaysFees import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal -typealias FeeModel = GenericFeeModel typealias DecimalFee = GenericDecimalFee class GenericFeeModel( @@ -20,8 +21,14 @@ class GenericDecimalFee( val networkFeeDecimalAmount: BigDecimal ) { - @Deprecated("This field has unclear semantics in a case of custom fee structure", replaceWith = ReplaceWith("networkFeeDecimalAmount")) - val decimalAmount: BigDecimal = networkFeeDecimalAmount - - val fee: Fee = genericFee.networkFee + val networkFee: Fee = genericFee.networkFee } + +val GenericDecimalFee.networkFeeByRequestedAccount: BigDecimal + get() = if (networkFee.requestedAccountPaysFees) networkFeeDecimalAmount else BigDecimal.ZERO + +val GenericDecimalFee?.networkFeeByRequestedAccountOrZero: BigDecimal + get() = this?.networkFeeByRequestedAccount.orZero() + +val GenericDecimalFee?.networkFeeDecimalAmount: BigDecimal + get() = this?.networkFeeDecimalAmount.orZero() diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/RealWalletConnectSessionInteractor.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/RealWalletConnectSessionInteractor.kt index 05433fe91d..40d3ece981 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/RealWalletConnectSessionInteractor.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/RealWalletConnectSessionInteractor.kt @@ -147,7 +147,7 @@ class RealWalletConnectSessionInteractor( walletConnectPairingRepository.allPairingAccountsFlow() ) { activeSessions, pairingAccounts -> val metaAccounts = if (metaId == null) { - accountRepository.allMetaAccounts() + accountRepository.getActiveMetaAccounts() } else { listOf(accountRepository.getMetaAccount(metaId)) } diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/approve/WalletConnectApproveSessionFragment.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/approve/WalletConnectApproveSessionFragment.kt index c93db0c4fd..d6fc409dd0 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/approve/WalletConnectApproveSessionFragment.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/approve/WalletConnectApproveSessionFragment.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.applyStatusBarInsets import io.novafoundation.nova.common.view.setState -import io.novafoundation.nova.common.view.setTextOrHide +import io.novafoundation.nova.common.view.setMessageOrHide import io.novafoundation.nova.common.view.showValueOrHide import io.novafoundation.nova.feature_account_api.presenatation.chain.showChainsOverview import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.setupSelectWalletMixin @@ -78,8 +78,8 @@ class WalletConnectApproveSessionFragment : BaseFragment - wcApproveSessionChainsAlert.setTextOrHide(sessionAlerts.unsupportedChains?.alertContent) - wcApproveSessionAccountsAlert.setTextOrHide(sessionAlerts.missingAccounts?.alertContent) + wcApproveSessionChainsAlert.setMessageOrHide(sessionAlerts.unsupportedChains?.alertContent) + wcApproveSessionAccountsAlert.setMessageOrHide(sessionAlerts.missingAccounts?.alertContent) } viewModel.allowButtonState.observe(wcApproveSessionAllow::setState) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt index b46f59fd51..c284688ced 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt @@ -23,6 +23,8 @@ class UnsupportedAssetBalance : AssetBalance { override suspend fun existentialDeposit(chain: Chain, chainAsset: Chain.Asset) = unsupported() + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId) = unsupported() + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId) = unsupported() override suspend fun startSyncingBalance( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt index 4c3601be46..e14d88cc95 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.equilibrium import android.util.Log +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding @@ -102,7 +103,7 @@ class EquilibriumAssetBalance( } } - override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): AccountBalance { val assetBalances = remoteStorageSource.query( chain.id, keyBuilder = { it.getAccountStorage().storageKey(it, accountId) }, @@ -120,7 +121,19 @@ class EquilibriumAssetBalance( .firstOrNull { it.assetId == chainAsset.id } ?.balance .orZero() - return assetBalance + reservedBalance + + val lockedBalance = assetBalances.lock.orZero().takeIf { chainAsset.isUtilityAsset } ?: BigInteger.ZERO + + return AccountBalance( + free = assetBalance, + reserved = reservedBalance, + frozen = lockedBalance + ) + } + + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + val accountBalance = queryAccountBalance(chain, chainAsset, accountId) + return accountBalance.free + accountBalance.reserved } override suspend fun startSyncingBalance( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt index b787294580..2a239fc2f4 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.evmErc20 +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.core.ethereum.log.Topic import io.novafoundation.nova.core.updater.EthereumSharedRequestsBuilder import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -64,14 +65,22 @@ class EvmErc20AssetBalance( return BigInteger.ZERO } - override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): AccountBalance { val erc20Type = chainAsset.requireErc20() val ethereumApi = chainRegistry.getCallEthereumApiOrThrow(chain.id) val accountAddress = chain.addressOf(accountId) - - return erc20Standard.querySingle(erc20Type.contractAddress, ethereumApi) + val balance = erc20Standard.querySingle(erc20Type.contractAddress, ethereumApi) .balanceOfAsync(accountAddress) .await() + return AccountBalance( + free = balance, + reserved = BigInteger.ZERO, + frozen = BigInteger.ZERO, + ) + } + + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + return queryAccountBalance(chain, chainAsset, accountId).free } override suspend fun startSyncingBalance( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt index af7fab3bb3..d8f089a4b1 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.evmNative +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.core.ethereum.Web3Api import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount @@ -49,10 +50,19 @@ class EvmNativeAssetBalance( return BigInteger.ZERO } - override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): AccountBalance { val ethereumApi = chainRegistry.getCallEthereumApiOrThrow(chain.id) - return ethereumApi.getLatestNativeBalance(chain.addressOf(accountId)) + val balance = ethereumApi.getLatestNativeBalance(chain.addressOf(accountId)) + return AccountBalance( + free = balance, + reserved = BigInteger.ZERO, + frozen = BigInteger.ZERO, + ) + } + + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + return queryAccountBalance(chain, chainAsset, accountId).free } override suspend fun startSyncingBalance( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt index 86dc8c1aa9..717b9064e1 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.utils.decodeValue import io.novafoundation.nova.common.utils.tokens import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -61,14 +62,18 @@ class OrmlAssetBalance( return chainAsset.requireOrml().existentialDeposit } - override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { - val ormlAccountData = remoteStorageSource.query( + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): AccountBalance { + return remoteStorageSource.query( chainId = chain.id, keyBuilder = { it.ormlBalanceKey(accountId, chainAsset) }, - binding = { scale, runtime -> bindOrmlAccountDataOrEmpty(scale, runtime) } + binding = { scale, runtime -> bindOrmlAccountBalanceOrEmpty(scale, runtime) } ) + } + + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + val accountBalance = queryAccountBalance(chain, chainAsset, accountId) - return ormlAccountData.free + ormlAccountData.reserved + return accountBalance.free + accountBalance.reserved } override suspend fun startSyncingBalance( @@ -82,7 +87,7 @@ class OrmlAssetBalance( return subscriptionBuilder.subscribe(runtime.ormlBalanceKey(accountId, chainAsset)) .map { - val ormlAccountData = bindOrmlAccountDataOrEmpty(it.value, runtime) + val ormlAccountData = bindOrmlAccountBalanceOrEmpty(it.value, runtime) val assetChanged = updateAssetBalance(metaAccount.id, chainAsset, ormlAccountData) @@ -97,7 +102,7 @@ class OrmlAssetBalance( private suspend fun updateAssetBalance( metaId: Long, chainAsset: Chain.Asset, - ormlAccountData: OrmlAccountData + ormlAccountData: AccountBalance ) = assetCache.updateAsset(metaId, chainAsset) { local -> with(ormlAccountData) { local.copy( @@ -114,7 +119,7 @@ class OrmlAssetBalance( return metadata.tokens().storage("Accounts").storageKey(this, accountId, chainAsset.ormlCurrencyId(this)) } - private fun bindOrmlAccountDataOrEmpty(scale: String?, runtime: RuntimeSnapshot): OrmlAccountData { - return scale?.let { bindOrmlAccountData(it, runtime) } ?: OrmlAccountData.empty() + private fun bindOrmlAccountBalanceOrEmpty(scale: String?, runtime: RuntimeSnapshot): AccountBalance { + return scale?.let { bindOrmlAccountData(it, runtime) } ?: AccountBalance.empty() } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssets.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt similarity index 72% rename from feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssets.kt rename to feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt index 45cb65fc81..5343fd9486 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssets.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.cast @@ -9,33 +10,14 @@ import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHexOrNull import jp.co.soramitsu.fearless_utils.runtime.metadata.storage -import java.math.BigInteger - -class OrmlAccountData( - val free: BigInteger, - val reserved: BigInteger, - val frozen: BigInteger, -) { - - companion object { - - fun empty(): OrmlAccountData { - return OrmlAccountData( - free = BigInteger.ZERO, - reserved = BigInteger.ZERO, - frozen = BigInteger.ZERO, - ) - } - } -} @UseCaseBinding -fun bindOrmlAccountData(scale: String, runtime: RuntimeSnapshot): OrmlAccountData { +fun bindOrmlAccountData(scale: String, runtime: RuntimeSnapshot): AccountBalance { val type = runtime.metadata.tokens().storage("Accounts").returnType() val dynamicInstance = type.fromHexOrNull(runtime, scale).cast() - return OrmlAccountData( + return AccountBalance( free = bindNumber(dynamicInstance["free"]), reserved = bindNumber(dynamicInstance["reserved"]), frozen = bindNumber(dynamicInstance["frozen"]), diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt index 0f8382c3e7..e086024a17 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.utils.decodeValue import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -55,7 +56,7 @@ class StatemineAssetBalance( return queryAssetDetails(chainAsset).minimumBalance } - override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): AccountBalance { val statemineType = chainAsset.requireStatemine() val assetAccount = remoteStorage.query(chain.id) { @@ -68,7 +69,21 @@ class StatemineAssetBalance( ) } - return assetAccount.balance + val frozenBalance = if (assetAccount.isBalanceFrozen) { + assetAccount.balance + } else { + BigInteger.ZERO + } + + return AccountBalance( + free = assetAccount.balance, + reserved = BigInteger.ZERO, + frozen = frozenBalance + ) + } + + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { + return queryAccountBalance(chain, chainAsset, accountId).free } override suspend fun startSyncingBalance( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt index 6312a45a1d..7edad10b9d 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.utility import android.util.Log +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.balances import io.novafoundation.nova.common.utils.decodeValue @@ -64,10 +65,14 @@ class NativeAssetBalance( return runtime.metadata.balances().numberConstant("ExistentialDeposit", runtime) } + override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): AccountBalance { + return substrateRemoteSource.getAccountInfo(chain.id, accountId).data + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { - val accountInfo = substrateRemoteSource.getAccountInfo(chain.id, accountId) + val accountData = queryAccountBalance(chain, chainAsset, accountId) - return accountInfo.data.free + accountInfo.data.reserved + return accountData.free + accountData.reserved } override suspend fun startSyncingBalance( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt index 5f8b5df7e2..6a6f05a175 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt @@ -14,10 +14,10 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage import io.novafoundation.nova.runtime.ext.isUtilityAsset +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getRuntime -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.metadata.call @@ -44,19 +44,19 @@ class EquilibriumAssetHistory( private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor { override suspend fun extractRealtimeHistoryUpdates( - extrinsic: ExtrinsicWithEvents, + extrinsicVisit: ExtrinsicVisit, chain: Chain, chainAsset: Chain.Asset ): RealtimeHistoryUpdate.Type? { val runtime = chainRegistry.getRuntime(chain.id) - val call = extrinsic.extrinsic.call + val call = extrinsicVisit.call if (!call.isTransfer(runtime)) return null val amount = bindNumber(call.arguments["value"]) return RealtimeHistoryUpdate.Type.Transfer( - senderId = bindAccountIdentifier(extrinsic.extrinsic.signature!!.accountIdentifier), + senderId = extrinsicVisit.origin, recipientId = bindAccountIdentifier(call.arguments["to"]), amountInPlanks = amount, chainAsset = chainAsset, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt index 09ac4e69a4..c3675ecbea 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt @@ -16,10 +16,10 @@ import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorSto import io.novafoundation.nova.runtime.ext.findAssetByOrmlCurrencyId import io.novafoundation.nova.runtime.ext.isSwapSupported import io.novafoundation.nova.runtime.ext.isUtilityAsset +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getRuntime -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.metadata.call @@ -48,20 +48,20 @@ class OrmlAssetHistory( private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor { override suspend fun extractRealtimeHistoryUpdates( - extrinsic: ExtrinsicWithEvents, + extrinsicVisit: ExtrinsicVisit, chain: Chain, chainAsset: Chain.Asset ): RealtimeHistoryUpdate.Type? { val runtime = chainRegistry.getRuntime(chain.id) - val call = extrinsic.extrinsic.call + val call = extrinsicVisit.call if (!call.isTransfer(runtime)) return null val inferredAsset = chain.findAssetByOrmlCurrencyId(runtime, call.arguments["currency_id"]) ?: return null val amount = bindNumber(call.arguments["amount"]) return RealtimeHistoryUpdate.Type.Transfer( - senderId = bindAccountIdentifier(extrinsic.extrinsic.signature!!.accountIdentifier), + senderId = extrinsicVisit.origin, recipientId = bindAccountIdentifier(call.arguments["dest"]), amountInPlanks = amount, chainAsset = inferredAsset, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt index a45de8848c..b58d8d69f2 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId -import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier import io.novafoundation.nova.common.data.network.runtime.binding.bindList import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules @@ -10,16 +9,13 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.h import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount import io.novafoundation.nova.runtime.ext.commissionAsset +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.bindMultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicStatus -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findAllEvents -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.hasEvent -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.nativeFee -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.status +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findAllOfType +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.requireNativeFee import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import kotlinx.coroutines.CoroutineScope import kotlin.coroutines.coroutineContext @@ -31,11 +27,11 @@ class AssetConversionSwapExtractor( private val calls = listOf("swap_exact_tokens_for_tokens", "swap_tokens_for_exact_tokens") override suspend fun extractRealtimeHistoryUpdates( - extrinsic: ExtrinsicWithEvents, + extrinsicVisit: ExtrinsicVisit, chain: Chain, chainAsset: Chain.Asset ): RealtimeHistoryUpdate.Type? { - val call = extrinsic.extrinsic.call + val call = extrinsicVisit.call val callArgs = call.arguments if (!call.isSwap()) return null @@ -47,29 +43,28 @@ class AssetConversionSwapExtractor( val assetIn = multiLocationConverter.toChainAsset(path.first()) ?: return null val assetOut = multiLocationConverter.toChainAsset(path.last()) ?: return null - val (amountIn, amountOut) = extrinsic.extractSwapAmounts() + val (amountIn, amountOut) = extrinsicVisit.extractSwapAmounts() - val who = bindAccountIdentifier(extrinsic.extrinsic.signature!!.accountIdentifier) val sendTo = bindAccountId(callArgs["send_to"]) - val fee = extrinsic.extractFee(chain, multiLocationConverter) + val fee = extrinsicVisit.extractFee(chain, multiLocationConverter) return RealtimeHistoryUpdate.Type.Swap( amountIn = ChainAssetWithAmount(assetIn, amountIn), amountOut = ChainAssetWithAmount(assetOut, amountOut), amountFee = fee, - senderId = who, + senderId = extrinsicVisit.origin, receiverId = sendTo ) } - private fun ExtrinsicWithEvents.extractSwapAmounts(): Pair { - val isCustomFeeTokenUsed = hasEvent(Modules.ASSET_TX_PAYMENT, "AssetTxFeePaid") - val extrinsicStatus = status() - val allSwaps = findAllEvents(Modules.ASSET_CONVERSION, "SwapExecuted") + private fun ExtrinsicVisit.extractSwapAmounts(): Pair { + // We check for custom fee usage from root extrinsic since `extrinsicVisit` will cut it out when nested calls are present + val isCustomFeeTokenUsed = rootExtrinsic.events.assetTxFeePaidEvent() != null + val allSwaps = events.findAllOfType(Modules.ASSET_CONVERSION, "SwapExecuted") val swapExecutedEvent = when { - extrinsicStatus == ExtrinsicStatus.FAILURE -> null // we wont be able to extract swap from event + !success -> null // we wont be able to extract swap from event isCustomFeeTokenUsed -> { // Swaps with custom fee token produce up to free SwapExecuted events, in the following order: @@ -96,16 +91,16 @@ class AssetConversionSwapExtractor( } // failed swap, extract from call args - extrinsic.call.function.name == "swap_exact_tokens_for_tokens" -> { - val amountIn = bindNumber(extrinsic.call.arguments["amount_in"]) - val amountOutMin = bindNumber(extrinsic.call.arguments["amount_out_min"]) + call.function.name == "swap_exact_tokens_for_tokens" -> { + val amountIn = bindNumber(call.arguments["amount_in"]) + val amountOutMin = bindNumber(call.arguments["amount_out_min"]) amountIn to amountOutMin } - extrinsic.call.function.name == "swap_tokens_for_exact_tokens" -> { - val amountOut = bindNumber(extrinsic.call.arguments["amount_out"]) - val amountInMax = bindNumber(extrinsic.call.arguments["amount_in_max"]) + call.function.name == "swap_tokens_for_exact_tokens" -> { + val amountOut = bindNumber(call.arguments["amount_out"]) + val amountInMax = bindNumber(call.arguments["amount_in_max"]) amountInMax to amountOut } @@ -114,14 +109,15 @@ class AssetConversionSwapExtractor( } } - private suspend fun ExtrinsicWithEvents.extractFee( + private suspend fun ExtrinsicVisit.extractFee( chain: Chain, multiLocationConverter: MultiLocationConverter ): ChainAssetWithAmount { - val assetFee = assetFee(multiLocationConverter) + // We check for fee usage from root extrinsic since `extrinsicVisit` will cut it out when nested calls are present + val assetFee = rootExtrinsic.events.assetFee(multiLocationConverter) if (assetFee != null) return assetFee - val nativeFee = nativeFee()!! + val nativeFee = rootExtrinsic.events.requireNativeFee() return ChainAssetWithAmount(chain.commissionAsset, nativeFee) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt index 7ed5ad9f1d..41f3891960 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt @@ -5,14 +5,18 @@ import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount import io.novafoundation.nova.runtime.multiNetwork.multiLocation.bindMultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent -suspend fun ExtrinsicWithEvents.assetFee(multiLocationConverter: MultiLocationConverter): ChainAssetWithAmount? { - val event = findEvent(Modules.ASSET_TX_PAYMENT, "AssetTxFeePaid") ?: return null +suspend fun List.assetFee(multiLocationConverter: MultiLocationConverter): ChainAssetWithAmount? { + val event = assetTxFeePaidEvent() ?: return null val (_, actualFee, tip, assetId) = event.arguments val totalFee = bindNumber(actualFee) + bindNumber(tip) val chainAsset = multiLocationConverter.toChainAsset(bindMultiLocation(assetId)) ?: return null return ChainAssetWithAmount(chainAsset, totalFee) } + +fun List.assetTxFeePaidEvent(): GenericEvent.Instance? { + return findEvent(Modules.ASSET_TX_PAYMENT, "AssetTxFeePaid") +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt index af4c67e3c2..fc402d3409 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt @@ -5,22 +5,22 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.h import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Extractor import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory import io.novafoundation.nova.feature_wallet_api.domain.model.Operation +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.api.walkToList import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicStatus -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.status internal class SubstrateRealtimeOperationFetcherFactory( private val multiLocationConverterFactory: MultiLocationConverterFactory, - private val eventsRepository: EventsRepository + private val eventsRepository: EventsRepository, + private val extrinsicWalk: ExtrinsicWalk, ) : Factory { override fun create(sources: List): SubstrateRealtimeOperationFetcher { val extractors = sources.map { it.extractor() } - return RealSubstrateRealtimeOperationFetcher(eventsRepository, extractors) + return RealSubstrateRealtimeOperationFetcher(eventsRepository, extractors, extrinsicWalk) } private fun Factory.Source.extractor(): Extractor { @@ -43,7 +43,8 @@ internal class SubstrateRealtimeOperationFetcherFactory( private class RealSubstrateRealtimeOperationFetcher( private val repository: EventsRepository, - private val extractors: List + private val extractors: List, + private val callWalk: ExtrinsicWalk, ) : SubstrateRealtimeOperationFetcher { override suspend fun extractRealtimeHistoryUpdates( @@ -54,23 +55,19 @@ private class RealSubstrateRealtimeOperationFetcher( val extrinsics = repository.getExtrinsicsWithEvents(chain.id, blockHash) return extrinsics.flatMap { extrinsic -> - extractors.mapNotNull { - val type = runCatching { it.extractRealtimeHistoryUpdates(extrinsic, chain, chainAsset) }.getOrNull() ?: return@mapNotNull null + val visits = runCatching { callWalk.walkToList(extrinsic, chain.id) }.getOrElse { emptyList() } - RealtimeHistoryUpdate( - txHash = extrinsic.extrinsicHash, - status = extrinsic.operationStatus(), - type = type - ) - } - } - } + visits.flatMap { extrinsicVisit -> + extractors.mapNotNull { + val type = runCatching { it.extractRealtimeHistoryUpdates(extrinsicVisit, chain, chainAsset) }.getOrNull() ?: return@mapNotNull null - private fun ExtrinsicWithEvents.operationStatus(): Operation.Status { - return when (status()) { - ExtrinsicStatus.SUCCESS -> Operation.Status.COMPLETED - ExtrinsicStatus.FAILURE -> Operation.Status.FAILED - null -> Operation.Status.PENDING + RealtimeHistoryUpdate( + txHash = extrinsic.extrinsicHash, + status = Operation.Status.fromSuccess(extrinsicVisit.success), + type = type + ) + } + } } } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt index 090a81de2f..513d05d38e 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt @@ -16,11 +16,11 @@ import io.novafoundation.nova.runtime.ext.isSwapSupported import io.novafoundation.nova.runtime.ext.isUtilityAsset import io.novafoundation.nova.runtime.ext.palletNameOrDefault import io.novafoundation.nova.runtime.ext.requireStatemine +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.hasSameId import io.novafoundation.nova.runtime.multiNetwork.getRuntime -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.metadata.call @@ -52,19 +52,19 @@ class StatemineAssetHistory( private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor { override suspend fun extractRealtimeHistoryUpdates( - extrinsic: ExtrinsicWithEvents, + extrinsicVisit: ExtrinsicVisit, chain: Chain, chainAsset: Chain.Asset ): RealtimeHistoryUpdate.Type? { val runtime = chainRegistry.getRuntime(chain.id) - val call = extrinsic.extrinsic.call + val call = extrinsicVisit.call if (!call.isTransfer(runtime, chainAsset)) return null val amount = bindNumber(call.arguments["amount"]) return RealtimeHistoryUpdate.Type.Transfer( - senderId = bindAccountIdentifier(extrinsic.extrinsic.signature!!.accountIdentifier), + senderId = extrinsicVisit.origin, recipientId = bindAccountIdentifier(call.arguments["target"]), amountInPlanks = amount, chainAsset = chainAsset, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt index 9101dd50e0..67be5eede8 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt @@ -14,10 +14,10 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage import io.novafoundation.nova.runtime.ext.isSwapSupported +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getRuntime -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.metadata.callOrNull @@ -49,19 +49,19 @@ class NativeAssetHistory( private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor { override suspend fun extractRealtimeHistoryUpdates( - extrinsic: ExtrinsicWithEvents, + extrinsicVisit: ExtrinsicVisit, chain: Chain, chainAsset: Chain.Asset ): RealtimeHistoryUpdate.Type? { val runtime = chainRegistry.getRuntime(chain.id) - val call = extrinsic.extrinsic.call + val call = extrinsicVisit.call if (!call.isTransfer(runtime)) return null val amount = bindNumber(call.arguments["value"]) return RealtimeHistoryUpdate.Type.Transfer( - senderId = bindAccountIdentifier(extrinsic.extrinsic.signature!!.accountIdentifier), + senderId = extrinsicVisit.origin, recipientId = bindAccountIdentifier(call.arguments["dest"]), amountInPlanks = amount, chainAsset = chainAsset, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt index 4e8bede01d..df4967b931 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt @@ -1,9 +1,11 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers @@ -47,16 +49,14 @@ abstract class BaseAssetTransfers( */ protected abstract suspend fun transferFunctions(chainAsset: Chain.Asset): List> - override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { - val senderAccountId = transfer.sender.accountIdIn(transfer.originChain)!! - - return extrinsicService.submitExtrinsicWithAnySuitableWallet(transfer.originChain, senderAccountId) { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { + return extrinsicService.submitExtrinsic(transfer.originChain, transfer.sender.intoOrigin()) { transfer(transfer) } } override suspend fun calculateFee(transfer: AssetTransfer): Fee { - return extrinsicService.estimateFeeV2(transfer.originChain) { + return extrinsicService.estimateFee(transfer.originChain, TransactionOrigin.SelectedWallet) { transfer(transfer) } } @@ -90,7 +90,7 @@ abstract class BaseAssetTransfers( recipientCanAcceptTransfer(assetSourceRegistry) } - protected fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit() = doNotCrossExistentialDeposit( + private fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit() = doNotCrossExistentialDeposit( assetSourceRegistry = assetSourceRegistry, fee = { it.originFeeInUsedAsset }, extraAmount = { it.transfer.amount }, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt index 989a07a99b..e2d8cc810c 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers @@ -16,7 +17,7 @@ open class UnsupportedAssetTransfers : AssetTransfers { throw UnsupportedOperationException("Unsupported") } - override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { return Result.failure(UnsupportedOperationException("Unsupported")) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt index 532b5143f0..bc6dbe28c6 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer @@ -52,10 +53,10 @@ class EvmErc20AssetTransfers( } } - override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { return evmTransactionService.transact( chainId = transfer.originChain.id, - presetFee = transfer.decimalFee.fee, + presetFee = transfer.decimalFee.networkFee, fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT ) { transfer(transfer) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt index 52d98388a5..33d82dc9aa 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer @@ -48,11 +49,11 @@ class EvmNativeAssetTransfers( } } - override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { return evmTransactionService.transact( chainId = transfer.originChain.id, fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT, - presetFee = transfer.decimalFee.fee, + presetFee = transfer.decimalFee.networkFee, ) { nativeTransfer(transfer) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt index cc7ae9474b..674c352673 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt @@ -13,6 +13,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED import io.novafoundation.nova.feature_wallet_api.domain.validation.AmountProducer import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory +import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeProducer import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForSimpleFeeChanges import io.novafoundation.nova.feature_wallet_api.domain.validation.doNotCrossExistentialDeposit @@ -72,7 +73,7 @@ fun AssetTransfersValidationSystemBuilder.checkForFeeChanges( fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit( assetSourceRegistry: AssetSourceRegistry, - fee: AmountProducer, + fee: FeeProducer, extraAmount: AmountProducer, ) = doNotCrossExistentialDeposit( countableTowardsEdBalance = { it.originUsedAsset.balanceCountedTowardsED() }, @@ -86,11 +87,11 @@ fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOrig available = { it.originCommissionAsset.transferable }, amount = { it.sendingAmountInCommissionAsset }, fee = { it.originFee }, - error = { payload, availableToPayFees -> + error = { context -> AssetTransferValidationFailure.NotEnoughFunds.InCommissionAsset( - chainAsset = payload.transfer.originChain.commissionAsset, - fee = payload.originFee, - maxUsable = availableToPayFees + chainAsset = context.payload.transfer.originChain.commissionAsset, + fee = context.fee, + maxUsable = context.availableToPayFees ) } ) @@ -98,8 +99,8 @@ fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOrig fun AssetTransfersValidationSystemBuilder.sufficientBalanceInUsedAsset() = sufficientBalance( available = { it.originUsedAsset.transferable }, amount = { it.transfer.amount }, - fee = { BigDecimal.ZERO }, - error = { _, _ -> AssetTransferValidationFailure.NotEnoughFunds.InUsedAsset } + fee = { null }, + error = { AssetTransferValidationFailure.NotEnoughFunds.InUsedAsset } ) fun AssetTransfersValidationSystemBuilder.recipientIsNotSystemAccount() = notSystemAccount( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt index d181775c4e..34e5f0b643 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt @@ -1,11 +1,12 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain import io.novafoundation.nova.common.data.network.runtime.binding.Weight -import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.xTokensName import io.novafoundation.nova.common.utils.xcmPalletName import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer @@ -21,6 +22,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.XcmTransferType import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDeposit import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInCommissionAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset @@ -63,12 +65,12 @@ class RealCrossChainTransactor( doNotCrossExistentialDeposit( assetSourceRegistry = assetSourceRegistry, fee = { it.originFeeInUsedAsset }, - extraAmount = { it.transfer.amount + it.crossChainFee.orZero() } + extraAmount = { it.transfer.amount + it.crossChainFee.networkFeeByRequestedAccountOrZero } ) } override suspend fun estimateOriginFee(configuration: CrossChainTransferConfiguration, transfer: AssetTransfer): Fee { - return extrinsicService.estimateFeeV2(transfer.originChain) { + return extrinsicService.estimateFee(transfer.originChain, TransactionOrigin.SelectedWallet) { crossChainTransfer(configuration, transfer, crossChainFee = Balance.ZERO) } } @@ -77,8 +79,8 @@ class RealCrossChainTransactor( configuration: CrossChainTransferConfiguration, transfer: AssetTransfer, crossChainFee: BigInteger - ): Result<*> { - return extrinsicService.submitExtrinsicWithSelectedWallet(transfer.originChain) { + ): Result { + return extrinsicService.submitExtrinsic(transfer.originChain, TransactionOrigin.SelectedWallet) { crossChainTransfer(configuration, transfer, crossChainFee) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt index 3f52e0124e..ffb72de47f 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain import io.novafoundation.nova.common.data.network.runtime.binding.Weight import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFee @@ -57,7 +58,7 @@ class RealCrossChainWeigher( Mode.Standard -> { val xcmMessage = xcmMessage(feeConfig.xcmFeeType.instructions, chain) - val paymentInfo = extrinsicService.paymentInfo(chain) { + val paymentInfo = extrinsicService.paymentInfo(chain, TransactionOrigin.SelectedWallet) { xcmExecute(xcmMessage, maxWeight) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt index edb71c5dbd..f3f21e1f85 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations -import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.isTrueOrError import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload @@ -8,13 +7,15 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidation import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeInUsedAsset +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero class CrossChainFeeValidation : AssetTransfersValidation { override suspend fun validate(value: AssetTransferPayload): ValidationStatus { - val remainingBalanceAfterTransfer = value.originUsedAsset.transferable - value.transfer.amount - value.originFeeInUsedAsset + val networkFeeInUsedAsset = value.originFeeInUsedAsset.networkFeeByRequestedAccountOrZero + val remainingBalanceAfterTransfer = value.originUsedAsset.transferable - value.transfer.amount - networkFeeInUsedAsset - val crossChainFee = value.crossChainFee.orZero() + val crossChainFee = value.crossChainFee.networkFeeByRequestedAccountOrZero val remainsEnoughToPayCrossChainFees = remainingBalanceAfterTransfer >= crossChainFee return remainsEnoughToPayCrossChainFees isTrueOrError { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt index 6b50f2e8c7..007f4c1dfe 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt @@ -132,7 +132,7 @@ class WalletRepositoryImpl( override fun assetFlow(accountId: AccountId, chainAsset: Chain.Asset): Flow { return flow { - val metaAccount = accountRepository.findMetaAccountOrThrow(accountId) + val metaAccount = accountRepository.findMetaAccountOrThrow(accountId, chainAsset.chainId) emitAll(assetFlow(metaAccount.id, chainAsset)) } @@ -237,7 +237,7 @@ class WalletRepositoryImpl( } private suspend fun getAsset(accountId: AccountId, chainId: String, assetId: Int) = withContext(Dispatchers.Default) { - val metaAccount = accountRepository.findMetaAccountOrThrow(accountId) + val metaAccount = accountRepository.findMetaAccountOrThrow(accountId, chainId) assetCache.getAssetWithToken(metaAccount.id, chainId, assetId) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt index 4ebd2ef179..85d2a245e9 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt @@ -38,6 +38,7 @@ import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRep import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository @@ -141,4 +142,6 @@ interface WalletFeatureDependencies { val computationalCache: ComputationalCache val multiLocationConverterFactory: MultiLocationConverterFactory + + val extrinsicWalk: ExtrinsicWalk } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt index a750f348ee..d2a57fe0ca 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt @@ -47,6 +47,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstan import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory +import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserProviderFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin @@ -74,6 +75,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.source.CoingeckoCoinPrice import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage import io.novafoundation.nova.feature_wallet_impl.domain.RealCrossChainTransfersUseCase import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository @@ -328,8 +330,21 @@ class WalletFeatureModule { @FeatureScope fun provideSubstrateRealtimeOperationFetcherFactory( multiLocationConverterFactory: MultiLocationConverterFactory, - eventsRepository: EventsRepository + eventsRepository: EventsRepository, + extrinsicWalk: ExtrinsicWalk ): SubstrateRealtimeOperationFetcher.Factory { - return SubstrateRealtimeOperationFetcherFactory(multiLocationConverterFactory, eventsRepository) + return SubstrateRealtimeOperationFetcherFactory(multiLocationConverterFactory, eventsRepository, extrinsicWalk) } + + @Provides + @FeatureScope + fun provideProxyHaveEnoughFeeValidationFactory( + assetSourceRegistry: AssetSourceRegistry, + walletRepository: WalletRepository, + extrinsicService: ExtrinsicService, + ) = ProxyHaveEnoughFeeValidationFactory( + assetSourceRegistry, + walletRepository, + extrinsicService + ) } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt index bb67fd505d..7228c30b96 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.extrinsic.MortalityConstructor import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.ChainSyncService import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection @@ -83,4 +84,6 @@ interface RuntimeApi { val gasPriceProviderFactory: GasPriceProviderFactory val multiLocationConverterFactory: MultiLocationConverterFactory + + val extrinsicWalk: ExtrinsicWalk } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt index a21cc404fa..f5c66ce297 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt @@ -21,6 +21,8 @@ import io.novafoundation.nova.runtime.extrinsic.MortalityConstructor import io.novafoundation.nova.runtime.extrinsic.RealExtrinsicValidityUseCase import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.extrinsic.multi.RealExtrinsicSplitter +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.RealExtrinsicWalk import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory @@ -206,4 +208,10 @@ class RuntimeModule { fun provideMultiLocationConverterFactory(chainRegistry: ChainRegistry): MultiLocationConverterFactory { return MultiLocationConverterFactory(chainRegistry) } + + @Provides + @ApplicationScope + fun provideExtrinsicWalk( + chainRegistry: ChainRegistry, + ): ExtrinsicWalk = RealExtrinsicWalk(chainRegistry) } 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 fb129a8df1..685e5ee2d3 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 @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress import io.novafoundation.nova.common.data.network.runtime.binding.bindOrNull import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.emptyEthereumAccountId +import io.novafoundation.nova.common.utils.emptySubstrateAccountId import io.novafoundation.nova.common.utils.findIsInstanceOrNull import io.novafoundation.nova.common.utils.formatNamed import io.novafoundation.nova.common.utils.substrateAccountId @@ -178,7 +179,7 @@ fun Chain.accountIdOrNull(address: String): ByteArray? { fun Chain.emptyAccountId() = if (isEthereumBased) { emptyEthereumAccountId() } else { - ByteArray(32) + emptySubstrateAccountId() } fun Chain.accountIdOrDefault(maybeAddress: String): ByteArray { 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 9b3ca28795..21009fb9a0 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 @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.core_db.dao.ChainDao import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.requireGenesisHash +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner import io.novafoundation.nova.runtime.mapper.toRuntimeVersion import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -12,8 +13,10 @@ import io.novafoundation.nova.runtime.network.rpc.RpcCalls import jp.co.soramitsu.fearless_utils.extensions.fromHex import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.Nonce import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer import jp.co.soramitsu.fearless_utils.wsrpc.request.runtime.chain.RuntimeVersion +import java.math.BigInteger class ExtrinsicBuilderFactory( private val chainDao: ChainDao, @@ -26,9 +29,10 @@ class ExtrinsicBuilderFactory( * Create with special signer for fee calculation */ suspend fun createForFee( + signer: NovaSigner, chain: Chain, ): ExtrinsicBuilder { - return createMultiForFee(chain).first() + return createMultiForFee(signer, chain).first() } /** @@ -43,11 +47,10 @@ class ExtrinsicBuilderFactory( } suspend fun createMultiForFee( + signer: NovaSigner, chain: Chain, ): Sequence { - val signer = FeeSigner(chain) - - return createMulti(chain, signer, signer.accountId()) + return createMulti(chain, signer, signer.signerAccountId(chain)) } suspend fun createMulti( @@ -62,13 +65,14 @@ class ExtrinsicBuilderFactory( val runtimeVersion = getRuntimeVersion(chain) val mortality = mortalityConstructor.constructMortality(chain.id) - var nonce = rpcCalls.getNonce(chain.id, accountAddress) + val baseNonce = rpcCalls.getNonce(chain.id, accountAddress) + var nonceOffset = BigInteger.ZERO return generateSequence { val newElement = ExtrinsicBuilder( tip = chain.additional?.defaultTip.orZero(), runtime = runtime, - nonce = nonce, + nonce = Nonce(baseNonce, nonceOffset), runtimeVersion = runtimeVersion, genesisHash = chain.requireGenesisHash().fromHex(), blockHash = mortality.blockHash.fromHex(), @@ -78,7 +82,7 @@ class ExtrinsicBuilderFactory( accountId = accountId ) - nonce++ + nonceOffset++ newElement } 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 0d2c6962a0..997480d0e1 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 @@ -5,7 +5,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.Weight import io.novafoundation.nova.common.utils.times import io.novafoundation.nova.runtime.ext.requireGenesisHash import io.novafoundation.nova.runtime.extrinsic.CustomSignedExtensions -import io.novafoundation.nova.runtime.extrinsic.FeeSigner +import io.novafoundation.nova.runtime.extrinsic.signer.NovaSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.network.rpc.RpcCalls import io.novafoundation.nova.runtime.repository.BlockLimitsRepository @@ -14,6 +14,7 @@ import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Era import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.Nonce import jp.co.soramitsu.fearless_utils.wsrpc.request.runtime.chain.RuntimeVersion import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -24,10 +25,11 @@ typealias SplitCalls = List> interface ExtrinsicSplitter { - suspend fun split(callBuilder: CallBuilder, chain: Chain): SplitCalls + suspend fun split(signer: NovaSigner, callBuilder: CallBuilder, chain: Chain): SplitCalls } private typealias CallWeightsByType = Map> + private const val LEAVE_SOME_SPACE_MULTIPLIER = 0.8 internal class RealExtrinsicSplitter( @@ -35,8 +37,8 @@ internal class RealExtrinsicSplitter( private val blockLimitsRepository: BlockLimitsRepository, ) : ExtrinsicSplitter { - override suspend fun split(callBuilder: CallBuilder, chain: Chain): SplitCalls = coroutineScope { - val weightByCallId = estimateWeightByCallType(callBuilder, chain) + override suspend fun split(signer: NovaSigner, callBuilder: CallBuilder, chain: Chain): SplitCalls = coroutineScope { + val weightByCallId = estimateWeightByCallType(signer, callBuilder, chain) val blockLimit = blockLimitsRepository.maxWeightForNormalExtrinsics(chain.id) * LEAVE_SOME_SPACE_MULTIPLIER @@ -51,11 +53,11 @@ internal class RealExtrinsicSplitter( } @Suppress("SuspendFunctionOnCoroutineScope") - private suspend fun CoroutineScope.estimateWeightByCallType(callBuilder: CallBuilder, chain: Chain): CallWeightsByType { + private suspend fun CoroutineScope.estimateWeightByCallType(signer: NovaSigner, callBuilder: CallBuilder, chain: Chain): CallWeightsByType { return callBuilder.calls.groupBy { it.uniqueId } .mapValues { (_, calls) -> val sample = calls.first() - val sampleExtrinsic = wrapInFakeExtrinsic(sample, callBuilder.runtime, chain) + val sampleExtrinsic = wrapInFakeExtrinsic(signer, sample, callBuilder.runtime, chain) async { rpcCalls.getExtrinsicFee(chain, sampleExtrinsic).weight } } @@ -90,21 +92,20 @@ internal class RealExtrinsicSplitter( return split } - private suspend fun wrapInFakeExtrinsic(call: GenericCall.Instance, runtime: RuntimeSnapshot, chain: Chain): String { - val signer = FeeSigner(chain) + private suspend fun wrapInFakeExtrinsic(signer: NovaSigner, call: GenericCall.Instance, runtime: RuntimeSnapshot, chain: Chain): String { val genesisHash = chain.requireGenesisHash().fromHex() return ExtrinsicBuilder( tip = BalanceOf.ZERO, runtime = runtime, - nonce = BalanceOf.ZERO, + nonce = Nonce.zero(), runtimeVersion = RuntimeVersion(specVersion = 0, transactionVersion = 0), genesisHash = genesisHash, blockHash = genesisHash, era = Era.Immortal, customSignedExtensions = CustomSignedExtensions.extensionsWithValues(), signer = signer, - accountId = signer.accountId() + accountId = signer.signerAccountId(chain) ) .call(call) .build() diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/NovaSigner.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/NovaSigner.kt new file mode 100644 index 0000000000..23d04326ca --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/NovaSigner.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.runtime.extrinsic.signer + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer + +interface NovaSigner : Signer { + + suspend fun signerAccountId(chain: Chain): AccountId +} + +interface FeeSigner : NovaSigner { + + /** + * In contrast with [signerAccountId] which for [FeeSigner] is supposed to return an account id derived from a fake keypair, + * This method returns a real account id that later will sign the transaction we're calculating fee + * This is useful for the client code to understand which account which actually pay the fee since it might differ from the requested account id + */ + suspend fun actualFeeSignerId(chain: Chain): AccountId + + /** + * Similar to [actualFeeSignerId] but returns accountId that was specified as the transaction origin + * It might not be equal to [actualFeeSignerId] if [Signer] modifies the payload + */ + suspend fun requestedFeeSignerId(chain: Chain): AccountId +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalk.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalk.kt new file mode 100644 index 0000000000..e94225b5d1 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalk.kt @@ -0,0 +1,50 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.api + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent + +interface ExtrinsicWalk { + + suspend fun walk( + source: ExtrinsicWithEvents, + chainId: ChainId, + visitor: ExtrinsicVisitor + ) +} + +fun interface ExtrinsicVisitor { + + fun visit(visit: ExtrinsicVisit) +} + +class ExtrinsicVisit( + + /** + * Whole extrinsic object. Useful for accessing data outside if the current visit scope, e.g. some top-level events + */ + val rootExtrinsic: ExtrinsicWithEvents, + + /** + * Call that is currently visiting + */ + val call: GenericCall.Instance, + + /** + * Whether call succeeded or not. + * Call is considered successful when it succeeds itself as well as its outer parents succeeds + */ + val success: Boolean, + + /** + * All events that are related to this specific call + */ + val events: List, + + /** + * Origin's account id that this call has been dispatched with + */ + val origin: AccountId +) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalkExt.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalkExt.kt new file mode 100644 index 0000000000..fbafac57fb --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/api/ExtrinsicWalkExt.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.api + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents + +suspend fun ExtrinsicWalk.walkToList(source: ExtrinsicWithEvents, chainId: ChainId): List { + return buildList { + walk(source, chainId) { visitedCall -> + add(visitedCall) + } + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ExtrinsicVisitorLogger.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ExtrinsicVisitorLogger.kt new file mode 100644 index 0000000000..53d8300788 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ExtrinsicVisitorLogger.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import android.util.Log + +internal interface ExtrinsicVisitorLogger { + + fun info(message: String) + + fun error(message: String) +} + +internal class IndentVisitorLogger( + private val tag: String = "ExtrinsicVisitor", + private val indent: Int = 0 +) : ExtrinsicVisitorLogger { + + private val indentPrefix = " ".repeat(indent) + + override fun info(message: String) { + Log.d(tag, indentPrefix + message) + } + + override fun error(message: String) { + Log.e(tag, indentPrefix + message) + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt new file mode 100644 index 0000000000..5c390069b3 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt @@ -0,0 +1,131 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.common.utils.instanceOf +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event + +internal interface MutableEventQueue : EventQueue { + + /** + * Removes last event matching one of eventTypes + */ + fun popFromEnd(vararg eventTypes: Event) + + /** + * Takes and removes all events that go after last event matching one of eventTypes. If no matched event found, + * all available events are returned + */ + fun takeTail(vararg eventTypes: Event): List + + /** + * Takes and removes all events that go after specified inclusive index + * @param endInclusive + */ + fun takeAllAfterInclusive(endInclusive: Int): List + + /** + * Takes and removes last event matching one of eventTypes + */ + fun takeFromEnd(vararg eventTypes: Event): GenericEvent.Instance? +} + +internal interface EventQueue { + + fun all(): List + + fun peekItemFromEnd(vararg eventTypes: Event, endExclusive: Int): EventWithIndex? + + fun indexOfLast(vararg eventTypes: Event, endExclusive: Int): Int? +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun EventQueue.peekItemFromEndOrThrow(vararg eventTypes: Event, endExclusive: Int): EventWithIndex { + return requireNotNull(peekItemFromEnd(*eventTypes, endExclusive = endExclusive)) { + "No required event found for types ${eventTypes.joinToString { it.name }}" + } +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun MutableEventQueue.takeFromEndOrThrow(vararg eventTypes: Event): GenericEvent.Instance { + return requireNotNull(takeFromEnd(*eventTypes)) { + "No required event found for types ${eventTypes.joinToString { it.name }}" + } +} + +data class EventWithIndex(val event: GenericEvent.Instance, val eventIndex: Int) + +class RealEventQueue(event: List) : MutableEventQueue { + + private val events: MutableList = event.toMutableList() + + override fun all(): List { + return events + } + + override fun peekItemFromEnd(vararg eventTypes: Event, endExclusive: Int): EventWithIndex? { + return findEventAndIndex(eventTypes, endExclusive) + } + + override fun indexOfLast(vararg eventTypes: Event, endExclusive: Int): Int? { + return findEventAndIndex(eventTypes, endExclusive)?.eventIndex + } + + override fun popFromEnd(vararg eventTypes: Event) { + takeFromEnd(*eventTypes) + } + + override fun takeTail(vararg eventTypes: Event): List { + val eventWithIndex = this.findEventAndIndex(eventTypes) + + return if (eventWithIndex != null) { + this.removeAllAfterExclusive(eventWithIndex.eventIndex) + } else { + this.removeAllAfterInclusive(0) + } + } + + override fun takeAllAfterInclusive(endInclusive: Int): List { + return removeAllAfterInclusive(endInclusive) + } + + override fun takeFromEnd(vararg eventTypes: Event): GenericEvent.Instance? { + return findEventAndIndex(eventTypes)?.let { (event, index) -> + removeAllAfterInclusive(index) + + event + } + } + + private fun findEventAndIndex(eventTypes: Array, endExclusive: Int = this.events.size): EventWithIndex? { + val eventsQueue = this.events + val limit = endExclusive.coerceAtMost(eventsQueue.size) + + for (i in (limit - 1) downTo 0) { + val nextEvent = eventsQueue[i] + + eventTypes.forEach { event -> + if (nextEvent.instanceOf(event)) return EventWithIndex(nextEvent, i) + } + } + + return null + } + + private fun removeAllAfterInclusive(index: Int): List { + val subList = this.events.subList(index, this.events.size) + val subListCopy = subList.toList() + + subList.clear() + + return subListCopy + } + + private fun removeAllAfterExclusive(index: Int): List { + val subList = this.events.subList(index + 1, this.events.size) + val subListCopy = subList.toList() + + subList.clear() + + return subListCopy + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/NestedCallNode.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/NestedCallNode.kt new file mode 100644 index 0000000000..f71dbfd07a --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/NestedCallNode.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisitor +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +internal interface NestedCallNode { + + fun canVisit(call: GenericCall.Instance): Boolean + + /** + * Calculates exclusive end index that is needed to skip all internal events related to this nested call + * For example, utility.batch supposed to skip BatchCompleted/BatchInterrupted and all ItemCompleted events + * This function is used by `visit` to skip internal events of nested nodes of the same type (batch inside batch or proxy inside proxy) + * so they wont interfere + * Should not be called on failed nested calls since they emit no events and its trivial to proceed + */ + fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int + + fun visit(call: GenericCall.Instance, context: VisitingContext) +} + +internal interface VisitingContext { + + val rootExtrinsic: ExtrinsicWithEvents + + val runtime: RuntimeSnapshot + + val origin: AccountId + + val callSucceeded: Boolean + + val visitor: ExtrinsicVisitor + + val logger: ExtrinsicVisitorLogger + + val eventQueue: MutableEventQueue + + fun nestedVisit(visit: ExtrinsicVisit) + + fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance): Int +} + +internal interface EventCountingContext { + + val runtime: RuntimeSnapshot + + val eventQueue: EventQueue + + val endExclusive: Int + + fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, endExclusive: Int): Int +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt new file mode 100644 index 0000000000..65a5064519 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt @@ -0,0 +1,136 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisitor +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.proxy.ProxyNode +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.getRuntime +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.isSuccess +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.signer +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAddress + +internal class RealExtrinsicWalk( + private val chainRegistry: ChainRegistry, + private val knownNodes: List = defaultNodes(), +) : ExtrinsicWalk { + + companion object { + + fun defaultNodes() = listOf(ProxyNode()) + } + + override suspend fun walk( + source: ExtrinsicWithEvents, + chainId: ChainId, + visitor: ExtrinsicVisitor + ) { + val runtime = chainRegistry.getRuntime(chainId) + + val rootVisit = ExtrinsicVisit( + rootExtrinsic = source, + call = source.extrinsic.call, + success = source.isSuccess(), + events = source.events, + origin = source.extrinsic.signer() + ) + + nestedVisit(runtime, visitor, rootVisit, depth = 0) + } + + private fun nestedVisit( + runtime: RuntimeSnapshot, + visitor: ExtrinsicVisitor, + visitedCall: ExtrinsicVisit, + depth: Int, + ) { + val nestedNode = findNestedNode(visitedCall.call) + + if (nestedNode == null) { + val call = visitedCall.call + val display = "${call.module.name}.${call.function.name}" + val origin = visitedCall.origin + val newLogger = IndentVisitorLogger(indent = depth + 1) + + newLogger.info("Visiting leaf: $display, success: ${visitedCall.success}, origin: ${origin.toAddress(42)}") + + visitor.visit(visitedCall) + } else { + val eventQueue = RealEventQueue(visitedCall.events) + val newLogger = IndentVisitorLogger(indent = depth) + + val context = RealVisitingContext( + rootExtrinsic = visitedCall.rootExtrinsic, + eventsSize = visitedCall.events.size, + depth = depth, + runtime = runtime, + origin = visitedCall.origin, + callSucceeded = visitedCall.success, + visitor = visitor, + logger = newLogger, + eventQueue = eventQueue + ) + + nestedNode.visit(visitedCall.call, context) + } + } + + private fun endExclusiveToSkipInternalEvents( + runtime: RuntimeSnapshot, + call: GenericCall.Instance, + eventQueue: EventQueue, + endExclusive: Int, + ): Int { + val nestedNode = this.findNestedNode(call) + + return if (nestedNode != null) { + val context: EventCountingContext = RealEventCountingContext(runtime, eventQueue, endExclusive) + + nestedNode.endExclusiveToSkipInternalEvents(call, context) + } else { + // no internal events to skip since its a leaf + endExclusive + } + } + + private fun findNestedNode(call: GenericCall.Instance): NestedCallNode? { + return knownNodes.find { it.canVisit(call) } + } + + private inner class RealVisitingContext( + private val eventsSize: Int, + private val depth: Int, + override val rootExtrinsic: ExtrinsicWithEvents, + override val runtime: RuntimeSnapshot, + override val origin: AccountId, + override val callSucceeded: Boolean, + override val visitor: ExtrinsicVisitor, + override val logger: ExtrinsicVisitorLogger, + override val eventQueue: MutableEventQueue + ) : VisitingContext { + + override fun nestedVisit(visit: ExtrinsicVisit) { + return this@RealExtrinsicWalk.nestedVisit(runtime, visitor, visit, depth + 1) + } + + override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance): Int { + return this@RealExtrinsicWalk.endExclusiveToSkipInternalEvents(runtime, call, eventQueue, endExclusive = eventsSize) + } + } + + private inner class RealEventCountingContext( + override val runtime: RuntimeSnapshot, + override val eventQueue: EventQueue, + override val endExclusive: Int + ) : EventCountingContext { + + override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, endExclusive: Int): Int { + return this@RealExtrinsicWalk.endExclusiveToSkipInternalEvents(runtime, call, eventQueue, endExclusive) + } + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/proxy/ProxyNode.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/proxy/ProxyNode.kt new file mode 100644 index 0000000000..bf0a7ced8c --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/proxy/ProxyNode.kt @@ -0,0 +1,103 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.proxy + +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier +import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCall +import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.proxy +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.EventCountingContext +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.NestedCallNode +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.VisitingContext +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.peekItemFromEndOrThrow +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.takeFromEndOrThrow +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.event +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event + +internal class ProxyNode : NestedCallNode { + + private val proxyCalls = arrayOf("proxy", "proxyAnnounced") + + override fun canVisit(call: GenericCall.Instance): Boolean { + return call.module.name == Modules.PROXY && call.function.name in proxyCalls + } + + override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int { + var endExclusive = context.endExclusive + val completionEventType = context.runtime.proxyCompletionEvent() + + val (completionEvent, completionIdx) = context.eventQueue.peekItemFromEndOrThrow(completionEventType, endExclusive = endExclusive) + endExclusive = completionIdx + + if (completionEvent.isProxySucceeded()) { + val (innerCall) = this.callAndOriginFromProxy(call) + endExclusive = context.endExclusiveToSkipInternalEvents(innerCall, endExclusive) + } + + return endExclusive + } + + override fun visit(call: GenericCall.Instance, context: VisitingContext) { + if (!context.callSucceeded) { + this.visitFailedProxyCall(call, context) + context.logger.info("Proxy: reverted by outer parent") + return + } + + val completionEventType = context.runtime.proxyCompletionEvent() + val completionEvent = context.eventQueue.takeFromEndOrThrow(completionEventType) + + if (completionEvent.isProxySucceeded()) { + context.logger.info("Proxy: execution succeeded") + + this.visitSucceededProxyCall(call, context) + } else { + context.logger.info("Proxy: execution failed") + + this.visitFailedProxyCall(call, context) + } + } + + private fun visitFailedProxyCall(call: GenericCall.Instance, context: VisitingContext) { + this.visitProxyCall(call, context, success = false, events = emptyList()) + } + + private fun visitSucceededProxyCall(call: GenericCall.Instance, context: VisitingContext) { + this.visitProxyCall(call, context, success = true, events = context.eventQueue.all()) + } + + private fun visitProxyCall( + call: GenericCall.Instance, + context: VisitingContext, + success: Boolean, + events: List + ) { + val (innerCall, innerOrigin) = this.callAndOriginFromProxy(call) + + val visit = ExtrinsicVisit( + rootExtrinsic = context.rootExtrinsic, + call = innerCall, + success = success, + events = events, + origin = innerOrigin + ) + + context.nestedVisit(visit) + } + + private fun GenericEvent.Instance.isProxySucceeded(): Boolean { + return arguments.first().castToDictEnum().name == "Ok" + } + + private fun callAndOriginFromProxy(proxyCall: GenericCall.Instance): Pair { + return bindGenericCall(proxyCall.arguments["call"]) to bindAccountIdentifier(proxyCall.arguments["real"]) + } + + private fun RuntimeSnapshot.proxyCompletionEvent(): Event { + return metadata.proxy().event("ProxyExecuted") + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt index 44bfacb195..f79518b3d5 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt @@ -311,6 +311,7 @@ fun mapChainLocalToChain(chainLocal: JoinedChainInfo, gson: Gson): Chain { isEthereumBased = isEthereumBased, isTestNet = isTestNet, hasCrowdloans = hasCrowdloans, + supportProxy = supportProxy, hasSubstrateRuntime = hasSubstrateRuntime, governance = mapGovernanceListFromLocal(governance), swap = mapSwapListFromLocal(swap), 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 4df435c21c..2a3786b106 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 @@ -20,6 +20,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemot private const val ETHEREUM_OPTION = "ethereumBased" private const val CROWDLOAN_OPTION = "crowdloans" private const val TESTNET_OPTION = "testnet" +private const val PROXY_OPTION = "proxy" private const val SWAP_HUB = "swap-hub" private const val NO_SUBSTRATE_RUNTIME = "noSubstrateRuntime" private const val FULL_SYNC_BY_DEFAULT = "fullSyncByDefault" @@ -69,6 +70,7 @@ fun mapRemoteChainToLocal( isEthereumBased = ETHEREUM_OPTION in optionsOrEmpty, isTestNet = TESTNET_OPTION in optionsOrEmpty, hasCrowdloans = CROWDLOAN_OPTION in optionsOrEmpty, + supportProxy = PROXY_OPTION in optionsOrEmpty, hasSubstrateRuntime = NO_SUBSTRATE_RUNTIME !in optionsOrEmpty, governance = mapGovernanceRemoteOptionsToLocal(optionsOrEmpty), swap = mapSwapRemoteOptionsToLocal(optionsOrEmpty), 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 e55f424c48..859a1753f0 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 @@ -28,6 +28,7 @@ data class Chain( val isTestNet: Boolean, val hasSubstrateRuntime: Boolean, val hasCrowdloans: Boolean, + val supportProxy: Boolean, val governance: List, val swap: List, val connectionState: ConnectionState, diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeSyncService.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeSyncService.kt index dc0c2b3a04..17a0b438d2 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeSyncService.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeSyncService.kt @@ -97,22 +97,27 @@ class RuntimeSyncService( cancelExistingSync(chainId) syncingChains[chainId] = launch(syncDispatcher) { - sync(chainId, forceFullSync) + runCatching { + val syncResult = sync(chainId, forceFullSync) + syncResult?.let { _syncStatusFlow.emit(it) } + } + + syncFinished(chainId) } } private suspend fun sync( chainId: String, forceFullSync: Boolean, - ) { + ): SyncResult? { val syncInfo = knownChains[chainId] if (syncInfo == null) { Log.w(LOG_TAG, "Unknown chain with id $chainId requested to be synced") - return + return null } - val runtimeInfo = chainDao.runtimeInfo(chainId) ?: return + val runtimeInfo = chainDao.runtimeInfo(chainId) ?: return null val shouldSyncMetadata = runtimeInfo.shouldSyncMetadata() || forceFullSync @@ -138,14 +143,10 @@ class RuntimeSyncService( } } - syncFinished(chainId) - - _syncStatusFlow.emit( - SyncResult( - metadataHash = metadataHash, - typesHash = typesHash, - chainId = chainId - ) + return SyncResult( + metadataHash = metadataHash, + typesHash = typesHash, + chainId = chainId ) } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt index 957a4dafc8..95dd5b6dc4 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt @@ -1,8 +1,10 @@ package io.novafoundation.nova.runtime.multiNetwork.runtime.repository +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.instanceOf +import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent import java.math.BigInteger @@ -30,21 +32,43 @@ fun ExtrinsicWithEvents.status(): ExtrinsicStatus? { } } -fun ExtrinsicWithEvents.nativeFee(): BigInteger? { +fun ExtrinsicWithEvents.isSuccess(): Boolean { + val status = requireNotNull(status()) { + "Not able to identify extrinsic status" + } + + return status == ExtrinsicStatus.SUCCESS +} + +fun Extrinsic.DecodedInstance.signer(): AccountId { + val accountIdentifier = requireNotNull(signature?.accountIdentifier) { + "Extrinsic is unsigned" + } + + return bindAccountIdentifier(accountIdentifier) +} + +fun List.nativeFee(): BigInteger? { val event = findEvent(Modules.TRANSACTION_PAYMENT, "TransactionFeePaid") ?: return null val (_, actualFee, tip) = event.arguments return bindNumber(actualFee) + bindNumber(tip) } -fun ExtrinsicWithEvents.findEvent(module: String, event: String): GenericEvent.Instance? { - return events.find { it.instanceOf(module, event) } +fun List.requireNativeFee(): BigInteger { + return requireNotNull(nativeFee()) { + "No native fee event found" + } +} + +fun List.findEvent(module: String, event: String): GenericEvent.Instance? { + return find { it.instanceOf(module, event) } } -fun ExtrinsicWithEvents.hasEvent(module: String, event: String): Boolean { - return events.any { it.instanceOf(module, event) } +fun List.hasEvent(module: String, event: String): Boolean { + return any { it.instanceOf(module, event) } } -fun ExtrinsicWithEvents.findAllEvents(module: String, event: String): List { - return events.filter { it.instanceOf(module, event) } +fun List.findAllOfType(module: String, event: String): List { + return filter { it.instanceOf(module, event) } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt index b365252405..fb18c8013d 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt @@ -9,11 +9,14 @@ import io.novafoundation.nova.common.data.network.runtime.calls.GetBlockHashRequ import io.novafoundation.nova.common.data.network.runtime.calls.GetBlockRequest import io.novafoundation.nova.common.data.network.runtime.calls.GetFinalizedHeadRequest import io.novafoundation.nova.common.data.network.runtime.calls.GetHeaderRequest +import io.novafoundation.nova.common.data.network.runtime.calls.GetStorageSize import io.novafoundation.nova.common.data.network.runtime.calls.NextAccountIndexRequest import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.data.network.runtime.model.SignedBlock import io.novafoundation.nova.common.data.network.runtime.model.SignedBlock.Block.Header +import io.novafoundation.nova.common.utils.asGsonParsedNumber import io.novafoundation.nova.common.utils.extrinsicHash +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.ext.feeViaRuntimeCall import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus @@ -141,6 +144,10 @@ class RpcCalls( return socketFor(chainId).executeAsync(GetBlockHashRequest(blockNumber), mapper = pojo().nonNull()) } + suspend fun getStorageSize(chainId: ChainId, storageKey: String): BigInteger { + return socketFor(chainId).executeAsync(GetStorageSize(storageKey)).result?.asGsonParsedNumber().orZero() + } + private suspend fun socketFor(chainId: ChainId) = chainRegistry.getSocket(chainId) private fun bindPartialFee(decoded: Any?): FeeResponse { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/network/updaters/SingleChainUpdateSystem.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/network/updaters/SingleChainUpdateSystem.kt index 883565bceb..5f552f0b7c 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/network/updaters/SingleChainUpdateSystem.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/network/updaters/SingleChainUpdateSystem.kt @@ -26,7 +26,7 @@ abstract class SingleChainUpdateSystem( val updaters = getUpdaters(selectedOption) runUpdaters(chain, updaters) - }.shareIn(CoroutineScope(Dispatchers.Default), replay = 1, started = SharingStarted.WhileSubscribed()) + }.shareIn(CoroutineScope(Dispatchers.IO), replay = 1, started = SharingStarted.WhileSubscribed()) override fun start(): Flow = updateFlow } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilder.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilder.kt index a55409f6e0..a5a92b1c05 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilder.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilder.kt @@ -1,13 +1,40 @@ package io.novafoundation.nova.runtime.storage.source.multi +import io.novafoundation.nova.runtime.storage.source.query.DynamicInstanceBinder +import io.novafoundation.nova.runtime.storage.source.query.StorageKeyComponents import io.novafoundation.nova.runtime.storage.source.query.wrapSingleArgumentKeys import jp.co.soramitsu.fearless_utils.runtime.metadata.module.StorageEntry interface MultiQueryBuilder { - fun StorageEntry.queryKey(vararg args: Any?) + interface Descriptor { - fun StorageEntry.queryKeys(keysArgs: List>) + fun parseKey(key: String): K - fun StorageEntry.querySingleArgKeys(singleArgKeys: List) = queryKeys(singleArgKeys.wrapSingleArgumentKeys()) + fun parseValue(value: String?): V + } + + interface Result { + + operator fun get(descriptor: Descriptor): Map + } + + fun StorageEntry.queryKey( + vararg args: Any?, + binding: DynamicInstanceBinder + ): Descriptor + + fun StorageEntry.queryKeys( + keysArgs: List>, + keyExtractor: (StorageKeyComponents) -> K, + binding: DynamicInstanceBinder + ): Descriptor + + fun StorageEntry.querySingleArgKeys( + keysArgs: Iterable, + keyExtractor: (StorageKeyComponents) -> K, + binding: DynamicInstanceBinder + ): Descriptor = queryKeys(keysArgs.wrapSingleArgumentKeys(), keyExtractor, binding) } + +fun MultiQueryBuilder.Result.singleValueOf(descriptor: MultiQueryBuilder.Descriptor<*, V>): V = get(descriptor).values.first() diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilderImpl.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilderImpl.kt index dfbfe43aa1..295926cc1e 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilderImpl.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/multi/MultiQueryBuilderImpl.kt @@ -1,6 +1,11 @@ package io.novafoundation.nova.runtime.storage.source.multi +import io.novafoundation.nova.common.utils.splitKeyToComponents +import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder.Descriptor +import io.novafoundation.nova.runtime.storage.source.query.DynamicInstanceBinder +import io.novafoundation.nova.runtime.storage.source.query.StorageKeyComponents import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHex import jp.co.soramitsu.fearless_utils.runtime.metadata.module.StorageEntry import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKeys @@ -9,19 +14,68 @@ class MultiQueryBuilderImpl( private val runtime: RuntimeSnapshot ) : MultiQueryBuilder { + private val descriptors: MutableMap, List> = mutableMapOf() private val keys: MutableMap> = mutableMapOf() - override fun StorageEntry.queryKey(vararg args: Any?) { - keysForEntry(this).add(storageKey(runtime, *args)) + override fun StorageEntry.queryKey( + vararg args: Any?, + binding: DynamicInstanceBinder + ): Descriptor { + val key = storageKey(runtime, *args) + + keysForEntry(this).add(key) + return registerDescriptor(listOf(key), this, keyExtractor = { it }, binding) + } + + override fun StorageEntry.queryKeys( + keysArgs: List>, + keyExtractor: (StorageKeyComponents) -> K, + binding: DynamicInstanceBinder + ): Descriptor { + val keys = storageKeys(runtime, keysArgs) + + keysForEntry(this).addAll(keys) + return registerDescriptor(keys, this, keyExtractor, binding) } - override fun StorageEntry.queryKeys(keysArgs: List>) { - keysForEntry(this).addAll(storageKeys(runtime, keysArgs)) + fun descriptors(): Map, List> { + return descriptors } - fun build(): Map> { + fun keys(): Map> { return keys } private fun keysForEntry(entry: StorageEntry) = keys.getOrPut(entry, ::mutableListOf) + + private fun registerDescriptor( + keys: List, + storageEntry: StorageEntry, + keyExtractor: (StorageKeyComponents) -> K, + binding: DynamicInstanceBinder + ): Descriptor { + val newDescriptor = RealDescriptor(storageEntry, keyExtractor, binding) + descriptors[newDescriptor] = keys + + return newDescriptor + } + + private inner class RealDescriptor( + private val storageEntry: StorageEntry, + private val keyExtractor: (StorageKeyComponents) -> K, + private val valueBinding: (decoded: Any?) -> V + ) : Descriptor { + override fun parseKey(key: String): K { + val keyComponents = storageEntry.splitKeyToComponents(runtime, key) + + return keyExtractor(keyComponents) + } + + override fun parseValue(value: String?): V { + val valueType = storageEntry.type.value!! + val decoded = value?.let { valueType.fromHex(runtime, value) } + + return valueBinding(decoded) + } + } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt index 16bb84c680..5be9f7d720 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.fromByteArrayO import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible import io.novafoundation.nova.common.data.network.runtime.binding.incompatible import io.novafoundation.nova.common.utils.ComponentHolder -import io.novafoundation.nova.common.utils.splitKeyToComponents +import io.novafoundation.nova.common.utils.mapValuesNotNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilderImpl @@ -60,7 +60,8 @@ abstract class BaseStorageQueryContext( override suspend fun StorageEntry.entries( vararg prefixArgs: Any?, keyExtractor: (StorageKeyComponents) -> K, - binding: DynamicInstanceBinderWithKey + binding: DynamicInstanceBinderWithKey, + recover: (exception: Exception, rawValue: String?) -> Unit ): Map { val prefix = storageKey(runtime, *prefixArgs) @@ -70,14 +71,16 @@ abstract class BaseStorageQueryContext( entries = entries, storageEntry = this, keyExtractor = keyExtractor, - binding = binding + binding = binding, + recover = recover ) } override suspend fun StorageEntry.entries( keysArguments: List>, keyExtractor: (StorageKeyComponents) -> K, - binding: DynamicInstanceBinderWithKey + binding: DynamicInstanceBinderWithKey, + recover: (exception: Exception, rawValue: String?) -> Unit ): Map { val entries = queryKeys(storageKeys(runtime, keysArguments), at) @@ -85,7 +88,8 @@ abstract class BaseStorageQueryContext( entries = entries, storageEntry = this, keyExtractor = keyExtractor, - binding = binding + binding = binding, + recover = recover ) } @@ -189,22 +193,23 @@ abstract class BaseStorageQueryContext( } } - override suspend fun multi( + @Suppress("OVERRIDE_DEPRECATION", "OverridingDeprecatedMember") + override suspend fun multiInternal( builderBlock: MultiQueryBuilder.() -> Unit - ): Map> { - val keysByStorageEntry = MultiQueryBuilderImpl(runtime).apply(builderBlock).build() + ): MultiQueryBuilder.Result { + val builder = MultiQueryBuilderImpl(runtime).apply(builderBlock) - val keys = keysByStorageEntry.flatMap { (_, keys) -> keys } + val keys = builder.keys().flatMap { (_, keys) -> keys } val values = queryKeys(keys, at) - return keysByStorageEntry.mapValues { (storageEntry, keys) -> - val valueType = storageEntry.type.value!! - + val delegate = builder.descriptors().mapValues { (descriptor, keys) -> keys.associateBy( - keySelector = { key -> storageEntry.splitKeyToComponents(runtime, key) }, - valueTransform = { key -> values[key]?.let { valueType.fromHex(runtime, it) } } + keySelector = { key -> descriptor.parseKey(key) }, + valueTransform = { key -> descriptor.parseValue(values[key]) } ) } + + return MultiQueryResult(delegate) } override suspend fun Constant.getAs(binding: DynamicInstanceBinder): V { @@ -237,6 +242,7 @@ abstract class BaseStorageQueryContext( storageEntry: StorageEntry, keyExtractor: (StorageKeyComponents) -> K, binding: DynamicInstanceBinderWithKey, + recover: (exception: Exception, rawValue: String?) -> Unit = { exception, _ -> throw exception } ): Map { val returnType = storageEntry.type.value ?: incompatible() @@ -244,10 +250,22 @@ abstract class BaseStorageQueryContext( val keyComponents = ComponentHolder(storageEntry.splitKey(runtime, key)) keyExtractor(keyComponents) - }.mapValues { (key, value) -> - val decoded = value?.let { returnType.fromHexOrIncompatible(value, runtime) } + }.mapValuesNotNull { (key, value) -> + try { + val decoded = value?.let { returnType.fromHexOrIncompatible(value, runtime) } + binding(decoded, key) + } catch (e: Exception) { + recover(e, value) + null + } + } + } - binding(decoded, key) + @JvmInline + private value class MultiQueryResult(val delegate: Map, Map>) : MultiQueryBuilder.Result { + @Suppress("UNCHECKED_CAST") + override fun get(descriptor: MultiQueryBuilder.Descriptor): Map { + return delegate.getValue(descriptor) as Map } } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/StorageQueryContext.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/StorageQueryContext.kt index 7c5bdaf8ca..0d9e79718d 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/StorageQueryContext.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/StorageQueryContext.kt @@ -9,6 +9,9 @@ import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Constant import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module import jp.co.soramitsu.fearless_utils.runtime.metadata.module.StorageEntry import kotlinx.coroutines.flow.Flow +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract typealias StorageKeyComponents = ComponentHolder typealias DynamicInstanceBinder = (dynamicInstance: Any?) -> V @@ -49,7 +52,8 @@ interface StorageQueryContext { suspend fun StorageEntry.entries( vararg prefixArgs: Any?, keyExtractor: (StorageKeyComponents) -> K, - binding: DynamicInstanceBinderWithKey + binding: DynamicInstanceBinderWithKey, + recover: (exception: Exception, rawValue: String?) -> Unit = { exception, _ -> throw exception } ): Map suspend fun StorageEntry.entriesRaw( @@ -63,7 +67,8 @@ interface StorageQueryContext { suspend fun StorageEntry.entries( keysArguments: List>, keyExtractor: (StorageKeyComponents) -> K, - binding: DynamicInstanceBinderWithKey + binding: DynamicInstanceBinderWithKey, + recover: (exception: Exception, rawValue: String?) -> Unit = { exception, _ -> throw exception } ): Map suspend fun StorageEntry.query( @@ -75,9 +80,10 @@ interface StorageQueryContext { vararg keyArguments: Any? ): String? - suspend fun multi( + @Deprecated("Use multi for better smart-casting", replaceWith = ReplaceWith(expression = "multi(builderBlock)")) + suspend fun multiInternal( builderBlock: MultiQueryBuilder.() -> Unit - ): Map> + ): MultiQueryBuilder.Result // no keyExtractor short-cut suspend fun StorageEntry.entries( @@ -103,9 +109,19 @@ interface StorageQueryContext { suspend fun Constant.getAs(binding: DynamicInstanceBinder): V } -fun Map>.singleValueOf(storageEntry: StorageEntry) = getValue(storageEntry).values.first() +@Suppress("DEPRECATION") +@OptIn(ExperimentalContracts::class) +suspend fun StorageQueryContext.multi( + builderBlock: MultiQueryBuilder.() -> Unit +): MultiQueryBuilder.Result { + contract { + callsInPlace(builderBlock, InvocationKind.EXACTLY_ONCE) + } + + return multiInternal(builderBlock) +} -fun Collection<*>.wrapSingleArgumentKeys(): List> = map(::listOf) +fun Iterable<*>.wrapSingleArgumentKeys(): List> = map(::listOf) val StorageQueryContext.metadata: RuntimeMetadata get() = runtime.metadata diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt new file mode 100644 index 0000000000..60f41d62a9 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.runtime.util + +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.RuntimeType +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.MULTI_ADDRESS_ID +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.primitives.FixedByteArray +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.skipAliases + +fun RuntimeType<*, *>.constructAccountLookupInstance(accountId: AccountId): Any { + return when (skipAliases()) { + is DictEnum -> { // MultiAddress + DictEnum.Entry(MULTI_ADDRESS_ID, accountId) + } + is FixedByteArray -> { // GenericAccountId or similar + accountId + } + else -> throw UnsupportedOperationException("Unknown address type: ${this.name}") + } +} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt new file mode 100644 index 0000000000..b939390bcd --- /dev/null +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt @@ -0,0 +1,330 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress +import io.novafoundation.nova.common.data.network.runtime.binding.bindMultiAddress +import io.novafoundation.nova.runtime.ext.Geneses +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.api.walkToList +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.proxy.ProxyNode +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents +import io.novafoundation.nova.test_shared.any +import io.novafoundation.nova.test_shared.whenever +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.call +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.MetadataFunction +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnitRunner +import java.math.BigInteger + +@RunWith(MockitoJUnitRunner::class) +internal class ProxyWalkTest { + + @Mock + private lateinit var runtimeProvider: RuntimeProvider + + @Mock + private lateinit var chainRegistry: ChainRegistry + + @Mock + private lateinit var runtime: RuntimeSnapshot + + @Mock + private lateinit var metadata: RuntimeMetadata + + @Mock + private lateinit var proxyModule: Module + + private lateinit var extrinsicWalk: ExtrinsicWalk + + private val proxyExecutedType = Event("ProxyExecuted", index = 0 to 0, documentation = emptyList(), arguments = emptyList()) + + private val proxy = byteArrayOf(0x00) + private val proxied = byteArrayOf(0x01) + + private val testModule = createTestModuleWithCall(moduleName = "Test", callName = "test") + private val testInnerCall = GenericCall.Instance( + module = testModule, + function = testModule.call("test"), + arguments = emptyMap() + ) + + @Before + fun setup() = runBlocking { + whenever(proxyModule.events).thenReturn(mapOf("ProxyExecuted" to proxyExecutedType)) + whenever(metadata.modules).thenReturn(mapOf("Proxy" to proxyModule)) + whenever(runtime.metadata).thenReturn(metadata) + whenever(runtimeProvider.get()).thenReturn(runtime) + whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider) + + extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(ProxyNode())) + } + + @Test + fun shouldVisitSucceededSimpleCall() = runBlocking { + val events = listOf(testEvent(), extrinsicSuccess()) + + val extrinsic = createExtrinsic( + signer = proxied, + call = testInnerCall, + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(true, visit.success) + assertArrayEquals(proxied, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(events, visit.events) + } + + @Test + fun shouldVisitFailedSimpleCall() = runBlocking { + val events = listOf(extrinsicFailed()) + + val extrinsic = createExtrinsic( + signer = proxied, + call = testInnerCall, + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(false, visit.success) + assertArrayEquals(proxied, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(events, visit.events) + } + + @Test + fun shouldVisitSucceededSingleProxyCall() = runBlocking { + val innerProxyEvents = listOf(testEvent()) + val events = innerProxyEvents + listOf(proxyExecuted(success = true), extrinsicSuccess()) + + val extrinsic = createExtrinsic( + signer = proxy, + call = proxyCall( + real = proxied, + innerCall = testInnerCall + ), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(true, visit.success) + assertArrayEquals(proxied, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerProxyEvents, visit.events) + } + + @Test + fun shouldVisitFailedSingleProxyCall() = runBlocking { + val innerProxyEvents = emptyList() + val events = listOf(proxyExecuted(success = false), extrinsicSuccess()) + + val extrinsic = createExtrinsic( + signer = proxy, + call = proxyCall( + real = proxied, + innerCall = testInnerCall + ), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(false, visit.success) + assertArrayEquals(proxied, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerProxyEvents, visit.events) + } + + @Test + fun shouldVisitSucceededMultipleProxyCalls() = runBlocking { + val innerProxyEvents = listOf(testEvent()) + val events = innerProxyEvents + listOf(proxyExecuted(success = true), proxyExecuted(success = true), proxyExecuted(success = true), extrinsicSuccess()) + + val proxy1 = byteArrayOf(0x00) + val proxy2 = byteArrayOf(0x01) + val proxy3 = byteArrayOf(0x02) + val proxied = byteArrayOf(0x10) + + val extrinsic = createExtrinsic( + signer = proxy1, + call = proxyCall( + real = proxy2, + innerCall = proxyCall( + real = proxy3, + innerCall = proxyCall( + real = proxied, + innerCall = testInnerCall + ) + ) + ), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(true, visit.success) + assertArrayEquals(proxied, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerProxyEvents, visit.events) + } + + @Test + fun shouldVisitFailedMultipleProxyCalls() = runBlocking { + val innerProxyEvents = emptyList() + val events = listOf(proxyExecuted(success = false), proxyExecuted(success = true), extrinsicSuccess()) // only outer-most proxy succeeded + + val proxy1 = byteArrayOf(0x00) + val proxy2 = byteArrayOf(0x01) + val proxy3 = byteArrayOf(0x02) + val proxied = byteArrayOf(0x10) + + val extrinsic = createExtrinsic( + signer = proxy1, + call = proxyCall( + real = proxy2, + innerCall = proxyCall( + real = proxy3, + innerCall = proxyCall( + real = proxied, + innerCall = testInnerCall + ) + ) + ), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(false, visit.success) + assertArrayEquals(proxied, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerProxyEvents, visit.events) + } + + private suspend fun ExtrinsicWalk.walkToList(extrinsicWithEvents: ExtrinsicWithEvents): List { + return walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT) + } + + private suspend fun ExtrinsicWalk.walkSingle(extrinsicWithEvents: ExtrinsicWithEvents): ExtrinsicVisit { + val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT) + assertEquals(1, visits.size) + + return visits.single() + } + + private fun createExtrinsic( + signer: AccountId, + call: GenericCall.Instance, + events: List + ) = ExtrinsicWithEvents( + extrinsic = Extrinsic.DecodedInstance( + signature = Extrinsic.Signature( + accountIdentifier = bindMultiAddress(MultiAddress.Id(signer)), + signature = null, + signedExtras = emptyMap() + ), + call = call, + ), + extrinsicHash = "0x", + events = events + ) + + private fun createTestModuleWithCall( + moduleName: String, + callName: String + ): Module { + return Module( + name = moduleName, + storage = null, + calls = mapOf( + callName to MetadataFunction( + name = callName, + arguments = emptyList(), + documentation = emptyList(), + index = 0 to 0 + ) + ), + events = emptyMap(), + constants = emptyMap(), + errors = emptyMap(), + index = BigInteger.ZERO + ) + } + + private fun extrinsicSuccess(): GenericEvent.Instance { + return mockEvent("System", "ExtrinsicSuccess") + } + + private fun extrinsicFailed(): GenericEvent.Instance { + return mockEvent("System", "ExtrinsicFailed") + } + + private fun proxyExecuted(success: Boolean): GenericEvent.Instance { + val outcomeVariant = if (success) "Ok" else "Err" + val outcome = DictEnum.Entry(name = outcomeVariant, value = null) + + return GenericEvent.Instance(proxyModule, proxyExecutedType, arguments = listOf(outcome)) + } + + + private fun testEvent(): GenericEvent.Instance { + return mockEvent(testModule.name, "test") + } + + private fun proxyCall(real: AccountId, innerCall: GenericCall.Instance): GenericCall.Instance { + return mockCall( + moduleName = "Proxy", + callName = "proxy", + arguments = mapOf( + "real" to bindMultiAddress(MultiAddress.Id(real)), + "call" to innerCall, + // other args are not relevant + ) + ) + } + + private fun mockEvent(moduleName: String, eventName: String, arguments: List = emptyList()): GenericEvent.Instance { + val module = Mockito.mock(Module::class.java) + whenever(module.name).thenReturn(moduleName) + + val event = Mockito.mock(Event::class.java) + whenever(event.name).thenReturn(eventName) + + return GenericEvent.Instance( + module = module, + event = event, + arguments = arguments + ) + } + + private fun mockCall(moduleName: String, callName: String, arguments: Map = emptyMap()): GenericCall.Instance { + val module = Mockito.mock(Module::class.java) + whenever(module.name).thenReturn(moduleName) + + val function = Mockito.mock(MetadataFunction::class.java) + whenever(function.name).thenReturn(callName) + + return GenericCall.Instance( + module = module, + function = function, + arguments = arguments + ) + } +}