From abb89ae977b6e256696fc174cf16072ee62ee120 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 7 Oct 2024 18:27:37 +0200 Subject: [PATCH 01/19] add transitive closure for proxy detection --- .../Proxy/ChainProxySyncService.swift | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/Services/Proxy/ChainProxySyncService.swift b/novawallet/Common/Services/Proxy/ChainProxySyncService.swift index e49f2deea3..e43641fbf1 100644 --- a/novawallet/Common/Services/Proxy/ChainProxySyncService.swift +++ b/novawallet/Common/Services/Proxy/ChainProxySyncService.swift @@ -178,7 +178,7 @@ final class ChainProxySyncService: ObservableSyncService, ChainProxySyncServiceP let proxyList = try proxyListWrapper.targetOperation.extractNoCancellableResultData() let chainMetaAccounts = try metaAccountsWrapper.targetOperation.extractNoCancellableResultData() - let notProxiedAccountIdList: [AccountId] = chainMetaAccounts.compactMap { wallet in + let possibleProxiesList: [AccountId] = chainMetaAccounts.compactMap { wallet in guard wallet.info.type != .proxied else { return nil } @@ -186,17 +186,26 @@ final class ChainProxySyncService: ObservableSyncService, ChainProxySyncServiceP return wallet.info.fetch(for: chainModel.accountRequest())?.accountId } - let notProxiedAccountIds = Set(notProxiedAccountIdList) + var possibleProxiesIds = Set(possibleProxiesList) + var prevProxiesIds = possibleProxiesIds + var proxies: [ProxiedAccountId: [ProxyAccount]] = [:] - // We only need remote proxieds for proxies we have locally and we don't support delaed proxies - let proxies = proxyList.compactMapValues { accounts in - accounts.filter { - !$0.hasDelay && notProxiedAccountIds.contains($0.accountId) - } - }.filter { !$0.value.isEmpty } + repeat { + // We only need remote proxieds for current proxies and we don't support delaed proxies + proxies = proxyList.compactMapValues { accounts in + accounts.filter { + !$0.hasDelay && possibleProxiesIds.contains($0.accountId) + } + }.filter { !$0.value.isEmpty } + + prevProxiesIds = possibleProxiesIds + possibleProxiesIds = possibleProxiesIds.union(Set(proxies.keys)) + + } while possibleProxiesIds != prevProxiesIds return proxies } + proxyListOperation.addDependency(proxyListWrapper.targetOperation) proxyListOperation.addDependency(metaAccountsWrapper.targetOperation) From 9779bd0017ce2aa0a4940c7a8eb6f4e89ee2969b Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 8 Oct 2024 07:29:51 +0200 Subject: [PATCH 02/19] draft ledger for DApp browser --- .../DAppBrowserSigningState.swift | 15 ++- .../DAppEthereumConfirmInteractor.swift | 6 +- .../DAppEthereumSignBytesInteractor.swift | 4 +- ...erationConfirmInteractor+Proccessing.swift | 12 +- ...pOperationConfirmInteractor+Protocol.swift | 2 +- .../DAppOperationConfirmInteractor.swift | 21 +++- .../DAppOperationConfirmViewFactory.swift | 10 +- .../DAppSignBytesConfirmInteractor.swift | 4 +- ...DAppExtrinsicBuilderOperationFactory.swift | 116 +++++++++++++++++- .../Model/DAppOperationRequest.swift | 1 + .../Model/DAppParsedExtrinsic.swift | 2 + .../PolkadotExtensionExtrinsic.swift | 11 ++ .../PolkadotExtensionSignerResult.swift | 2 + .../Model/WalletConnectSignModelFactory.swift | 9 +- .../States/WalletConnectStateSigning.swift | 3 +- 15 files changed, 185 insertions(+), 33 deletions(-) diff --git a/novawallet/Modules/DApp/DAppBrowser/StateMachine/PolkadotExtensionStates/DAppBrowserSigningState.swift b/novawallet/Modules/DApp/DAppBrowser/StateMachine/PolkadotExtensionStates/DAppBrowserSigningState.swift index 77f6af64dc..15245ba83c 100644 --- a/novawallet/Modules/DApp/DAppBrowser/StateMachine/PolkadotExtensionStates/DAppBrowserSigningState.swift +++ b/novawallet/Modules/DApp/DAppBrowser/StateMachine/PolkadotExtensionStates/DAppBrowserSigningState.swift @@ -9,7 +9,11 @@ final class DAppBrowserSigningState: DAppBrowserBaseState { super.init(stateMachine: stateMachine) } - private func provideOperationResponse(with signature: Data, nextState: DAppBrowserStateProtocol) throws { + private func provideOperationResponse( + with signature: Data, + modifiedTransaction: Data?, + nextState: DAppBrowserStateProtocol + ) throws { guard let msgType = signingType.msgType else { return } @@ -17,7 +21,8 @@ final class DAppBrowserSigningState: DAppBrowserBaseState { let identifier = (0 ... UInt32.max).randomElement() ?? 0 let result = PolkadotExtensionSignerResult( identifier: UInt(identifier), - signature: signature.toHex(includePrefix: true) + signature: signature.toHex(includePrefix: true), + signedTransaction: modifiedTransaction?.toHex(includePrefix: true) ) try provideResponse(for: msgType, result: result, nextState: nextState) @@ -54,7 +59,11 @@ extension DAppBrowserSigningState: DAppBrowserStateProtocol { if let signature = response.signature { do { - try provideOperationResponse(with: signature, nextState: nextState) + try provideOperationResponse( + with: signature, + modifiedTransaction: response.modifiedTransaction, + nextState: nextState + ) } catch { stateMachine?.emit(error: error, nextState: nextState) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift index 88e9fac9f7..dd837a7de7 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift @@ -202,7 +202,7 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { switch result { case let .success(txHash): let txHashData = try Data(hexString: txHash) - let response = DAppOperationResponse(signature: txHashData) + let response = DAppOperationResponse(signature: txHashData, modifiedTransaction: nil) let result: Result = .success(response) self.presenter?.didReceive(responseResult: result, for: self.request) case let .failure(error): @@ -234,7 +234,7 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { do { switch result { case let .success(signedTransaction): - let response = DAppOperationResponse(signature: signedTransaction) + let response = DAppOperationResponse(signature: signedTransaction, modifiedTransaction: nil) let result: Result = .success(response) self.presenter?.didReceive(responseResult: result, for: self.request) case let .failure(error): @@ -304,7 +304,7 @@ extension DAppEthereumConfirmInteractor: DAppOperationConfirmInteractorInputProt } func reject() { - let response = DAppOperationResponse(signature: nil) + let response = DAppOperationResponse(signature: nil, modifiedTransaction: nil) presenter?.didReceive(responseResult: .success(response), for: request) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift index 37ae9340ee..e063f6f17d 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift @@ -103,7 +103,7 @@ extension DAppEthereumSignBytesInteractor: DAppOperationConfirmInteractorInputPr do { let signature = try signingOperation.extractNoCancellableResultData() - let response = DAppOperationResponse(signature: signature) + let response = DAppOperationResponse(signature: signature, modifiedTransaction: nil) self.presenter?.didReceive(responseResult: .success(response), for: self.request) } catch { @@ -119,7 +119,7 @@ extension DAppEthereumSignBytesInteractor: DAppOperationConfirmInteractorInputPr } func reject() { - let response = DAppOperationResponse(signature: nil) + let response = DAppOperationResponse(signature: nil, modifiedTransaction: nil) presenter?.didReceive(responseResult: .success(response), for: request) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift index afff3fff7e..f45ab0d7dc 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift @@ -79,19 +79,13 @@ extension DAppOperationConfirmInteractor { ) } - // TODO: Find out whether to return this validation - guard - let specVersion = BigUInt.fromHexString(extrinsic.specVersion) /* , - codingFactory.specVersion == specVersion */ else { + let specVersion = BigUInt.fromHexString(extrinsic.specVersion) else { throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "specVersion") } - // TODO: Find out whether to return this validation - guard - let transactionVersion = BigUInt.fromHexString(extrinsic.transactionVersion) /* , - codingFactory.txVersion == transactionVersion */ else { + let transactionVersion = BigUInt.fromHexString(extrinsic.transactionVersion) else { throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "transactionVersion") } @@ -135,6 +129,8 @@ extension DAppOperationConfirmInteractor { specVersion: UInt32(specVersion), tip: tip, transactionVersion: UInt32(transactionVersion), + metadataHash: extrinsic.metadataHash, + withSignedTransaction: extrinsic.withSignedTransaction ?? false, signedExtensions: expectedSignedExtensions, version: extrinsic.version ) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift index ad26218617..23d0819331 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift @@ -67,7 +67,7 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro return } - let response = DAppOperationResponse(signature: nil) + let response = DAppOperationResponse(signature: nil, modifiedTransaction: nil) presenter?.didReceive(responseResult: .success(response), for: request) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift index 433b29659c..d9025d5e3e 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift @@ -5,6 +5,11 @@ import BigInt import SoraKeystore final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { + struct SignatureResult { + let signature: Data + let modifiedExtrinsic: Data? + } + let request: DAppOperationRequest let chain: ChainModel @@ -12,6 +17,7 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { let signingWrapperFactory: SigningWrapperFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let runtimeProvider: RuntimeProviderProtocol + let metadataHashFactory: MetadataHashOperationFactoryProtocol let feeEstimationRegistry: ExtrinsicFeeEstimationRegistring let operationQueue: OperationQueue @@ -28,6 +34,7 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { feeEstimationRegistry: ExtrinsicFeeEstimationRegistring, connection: JSONRPCEngine, signingWrapperFactory: SigningWrapperFactoryProtocol, + metadataHashFactory: MetadataHashOperationFactoryProtocol, priceProviderFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, operationQueue: OperationQueue @@ -38,6 +45,7 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { self.feeEstimationRegistry = feeEstimationRegistry self.connection = connection self.signingWrapperFactory = signingWrapperFactory + self.metadataHashFactory = metadataHashFactory priceLocalSubscriptionFactory = priceProviderFactory self.operationQueue = operationQueue super.init() @@ -81,7 +89,10 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { func completeSetup(for result: DAppOperationProcessedResult) { extrinsicFactory = DAppExtrinsicBuilderOperationFactory( processedResult: result, - runtimeProvider: runtimeProvider + chain: chain, + runtimeProvider: runtimeProvider, + connection: connection, + metadataHashOperationFactory: metadataHashFactory ) let confirmationModel = DAppOperationConfirmModel( @@ -101,14 +112,14 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { func createSignatureOperation( for extrinsicFactory: DAppExtrinsicBuilderOperationFactory, signer: SigningWrapperProtocol - ) -> CompoundOperationWrapper { + ) -> CompoundOperationWrapper { let signatureWrapper = extrinsicFactory.createRawSignatureWrapper { data, context in try signer.sign(data, context: context).rawData() } let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() - let signatureOperation = ClosureOperation { + let signatureOperation = ClosureOperation { let signatureResult = try signatureWrapper.targetOperation.extractNoCancellableResultData() let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() @@ -147,7 +158,9 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { ) } - return try scaleEncoder.encode() + let signature = try scaleEncoder.encode() + + return SignatureResult(signature: signature, modifiedExtrinsic: signatureResult.signedExtrinsic) } signatureOperation.addDependency(codingFactoryOperation) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift index 549df6602b..d11a20e698 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift @@ -151,6 +151,7 @@ struct DAppOperationConfirmViewFactory { } let operationQueue = OperationManagerFacade.sharedDefaultQueue + let substrateStorageFacade = SubstrateDataStorageFacade.shared let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( account: account, @@ -159,13 +160,19 @@ struct DAppOperationConfirmViewFactory { connection: connection, operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( chain: chain, estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeProvider, userStorageFacade: UserDataStorageFacade.shared, - substrateStorageFacade: SubstrateDataStorageFacade.shared, + substrateStorageFacade: substrateStorageFacade, + operationQueue: operationQueue + ) + + let metadataHashFactory = MetadataHashOperationFactory( + metadataRepositoryFactory: RuntimeMetadataRepositoryFactory(storageFacade: substrateStorageFacade), operationQueue: operationQueue ) @@ -176,6 +183,7 @@ struct DAppOperationConfirmViewFactory { feeEstimationRegistry: feeEstimationRegistry, connection: connection, signingWrapperFactory: SigningWrapperFactory(keystore: Keychain()), + metadataHashFactory: metadataHashFactory, priceProviderFactory: PriceProviderFactory.shared, currencyManager: currencyManager, operationQueue: operationQueue diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift index 2fb0c59d18..0e971f3af9 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift @@ -113,7 +113,7 @@ extension DAppSignBytesConfirmInteractor: DAppOperationConfirmInteractorInputPro do { let signature = try signingOperation.extractNoCancellableResultData() - let response = DAppOperationResponse(signature: signature) + let response = DAppOperationResponse(signature: signature, modifiedTransaction: nil) self.presenter?.didReceive(responseResult: .success(response), for: self.request) } catch { @@ -129,7 +129,7 @@ extension DAppSignBytesConfirmInteractor: DAppOperationConfirmInteractorInputPro } func reject() { - let response = DAppOperationResponse(signature: nil) + let response = DAppOperationResponse(signature: nil, modifiedTransaction: nil) presenter?.didReceive(responseResult: .success(response), for: request) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift index 42d9c80894..9af200a2dd 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift @@ -3,6 +3,12 @@ import SubstrateSdk import Operation_iOS struct DAppExtrinsicRawSignatureResult { + let sender: ExtrinsicSenderResolution + let signature: Data + let modifiedExtrinsic: Data? +} + +struct DAppExtrinsicRawExtrinsicResult { let sender: ExtrinsicSenderResolution let signedExtrinsic: Data } @@ -11,25 +17,99 @@ final class DAppExtrinsicBuilderOperationFactory { struct ExtrinsicSenderBuilderResult { let sender: ExtrinsicSenderResolution let builder: ExtrinsicBuilderProtocol + let modifiedOriginalExtrinsic: Bool + } + + struct MetadataHashResult { + let modifiedOriginal: Bool + let metadataHash: Data? } let processedResult: DAppOperationProcessedResult + let chain: ChainModel let runtimeProvider: RuntimeCodingServiceProtocol + let connection: JSONRPCEngine + let metadataHashOperationFactory: MetadataHashOperationFactoryProtocol init( processedResult: DAppOperationProcessedResult, - runtimeProvider: RuntimeCodingServiceProtocol + chain: ChainModel, + runtimeProvider: RuntimeCodingServiceProtocol, + connection: JSONRPCEngine, + metadataHashOperationFactory: MetadataHashOperationFactoryProtocol ) { + self.chain = chain self.processedResult = processedResult self.runtimeProvider = runtimeProvider + self.connection = connection + self.metadataHashOperationFactory = metadataHashOperationFactory + } + + private func createActualMetadataHashWrapper( + for result: DAppOperationProcessedResult, + chain: ChainModel, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper { + do { + let optMetadataHash = try result.extrinsic.metadataHash.map { try Data(hexString: $0) } + + /** + * If a dapp haven't declared a permission to modify extrinsic - return whatever metadataHash present in payload + */ + if !result.extrinsic.withSignedTransaction { + return .createWithResult( + .init( + modifiedOriginal: false, + metadataHash: optMetadataHash + ) + ) + } + + // If a dapp have specified metadata hash explicitly - use it + if let metadataHash = optMetadataHash { + return .createWithResult( + .init( + modifiedOriginal: false, + metadataHash: metadataHash + ) + ) + } + + let metadataHashWrapper = metadataHashOperationFactory.createCheckMetadataHashWrapper( + for: chain, + connection: connection, + runtimeProvider: runtimeProvider + ) + + let mappingOperation = ClosureOperation { + let metadataHash = try metadataHashWrapper.targetOperation.extractNoCancellableResultData() + + return MetadataHashResult(modifiedOriginal: true, metadataHash: metadataHash) + } + + mappingOperation.addDependency(metadataHashWrapper.targetOperation) + + return metadataHashWrapper.insertingTail(operation: mappingOperation) + } catch { + return .createWithError(error) + } } private func createBaseBuilderWrapper( for result: DAppOperationProcessedResult, codingFactoryOperation: BaseOperation ) -> CompoundOperationWrapper { + let metadataHashWrapper = createActualMetadataHashWrapper( + for: processedResult, + chain: chain, + connection: connection, + runtimeProvider: runtimeProvider + ) + let builderOperation = ClosureOperation { let runtimeContext = try codingFactoryOperation.extractNoCancellableResultData().createRuntimeJsonContext() + let metadataHashResult = try metadataHashWrapper.targetOperation.extractNoCancellableResultData() let extrinsic = result.extrinsic @@ -53,6 +133,10 @@ final class DAppExtrinsicBuilderOperationFactory { .with(nonce: UInt32(extrinsic.nonce)) .with(era: extrinsic.era, blockHash: extrinsic.blockHash) + if let metadataHash = metadataHashResult.metadataHash { + builder = builder.with(metadataHash: metadataHash) + } + for signedExtension in signedExtensionFactory.createExtensions() { builder = builder.adding(extrinsicSignedExtension: signedExtension) } @@ -63,16 +147,22 @@ final class DAppExtrinsicBuilderOperationFactory { builder = builder.with(tip: extrinsic.tip) } - return ExtrinsicSenderBuilderResult(sender: sender, builder: builder) + return ExtrinsicSenderBuilderResult( + sender: sender, + builder: builder, + modifiedOriginalExtrinsic: metadataHashResult.modifiedOriginal + ) } - return CompoundOperationWrapper(targetOperation: builderOperation) + builderOperation.addDependency(metadataHashWrapper.targetOperation) + + return metadataHashWrapper.insertingTail(operation: builderOperation) } private func createRawSignatureOperation( for result: DAppOperationProcessedResult, signingClosure: @escaping (Data, ExtrinsicSigningContext) throws -> Data - ) -> CompoundOperationWrapper { + ) -> CompoundOperationWrapper { let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() let builderWrapper = createBaseBuilderWrapper( @@ -100,7 +190,7 @@ final class DAppExtrinsicBuilderOperationFactory { ) .build(encodingBy: codingFactory.createEncoder(), metadata: codingFactory.metadata) - return DAppExtrinsicRawSignatureResult(sender: builderResult.sender, signedExtrinsic: signedExtrinsic) + return DAppExtrinsicRawExtrinsicResult(sender: builderResult.sender, signedExtrinsic: signedExtrinsic) } payloadOperation.addDependency(codingFactoryOperation) @@ -176,8 +266,22 @@ extension DAppExtrinsicBuilderOperationFactory: ExtrinsicBuilderOperationFactory encoder: codingFactory.createEncoder(), metadata: codingFactory.metadata ) + + let modifiedExtrinsic = if builderResult.modifiedOriginalExtrinsic { + try builder.signing( + with: { _, _ in rawSignature }, + context: context, + codingFactory: codingFactory + ).build(encodingBy: codingFactory.createEncoder(), metadata: codingFactory.metadata) + } else { + nil + } - return DAppExtrinsicRawSignatureResult(sender: builderResult.sender, signedExtrinsic: rawSignature) + return DAppExtrinsicRawSignatureResult( + sender: builderResult.sender, + signature: rawSignature, + modifiedExtrinsic: modifiedExtrinsic + ) } signOperation.addDependency(builderWrapper.targetOperation) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppOperationRequest.swift b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppOperationRequest.swift index e8b8baf68d..1504149ff0 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppOperationRequest.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppOperationRequest.swift @@ -13,4 +13,5 @@ struct DAppOperationRequest { struct DAppOperationResponse { let signature: Data? + let modifiedTransaction: Data? } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppParsedExtrinsic.swift b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppParsedExtrinsic.swift index 7bc7f4f012..7d5adbe075 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppParsedExtrinsic.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppParsedExtrinsic.swift @@ -13,6 +13,8 @@ struct DAppParsedExtrinsic: Encodable { let specVersion: UInt32 let tip: BigUInt let transactionVersion: UInt32 + let metadataHash: String? + let withSignedTransaction: Bool let signedExtensions: [String] let version: UInt } diff --git a/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionExtrinsic.swift b/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionExtrinsic.swift index 776db4738a..dca08a9886 100644 --- a/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionExtrinsic.swift +++ b/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionExtrinsic.swift @@ -51,6 +51,17 @@ struct PolkadotExtensionExtrinsic: Codable { */ let transactionVersion: String + /** + * Metadata hash to use for signing + */ + + let metadataHash: String? + + /** + * Whether transaction modification is allowed + */ + let withSignedTransaction: Bool? + /** * The applicable signed extensions for this runtime */ diff --git a/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionSignerResult.swift b/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionSignerResult.swift index d8cdfbbf0d..82cb8c5eb7 100644 --- a/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionSignerResult.swift +++ b/novawallet/Modules/DApp/Model/PolkadotExtensionProtocol/PolkadotExtensionSignerResult.swift @@ -4,8 +4,10 @@ struct PolkadotExtensionSignerResult: Codable { enum CodingKeys: String, CodingKey { case identifier = "id" case signature + case signedTransaction } let identifier: UInt let signature: String + let signedTransaction: String? } diff --git a/novawallet/Modules/DApp/WalletConnect/Model/WalletConnectSignModelFactory.swift b/novawallet/Modules/DApp/WalletConnect/Model/WalletConnectSignModelFactory.swift index e0ad15d2b6..f880db46ed 100644 --- a/novawallet/Modules/DApp/WalletConnect/Model/WalletConnectSignModelFactory.swift +++ b/novawallet/Modules/DApp/WalletConnect/Model/WalletConnectSignModelFactory.swift @@ -268,13 +268,18 @@ extension WalletConnectSignModelFactory { } } - static func createSigningResponse(for method: WalletConnectMethod, signature: Data) -> AnyCodable { + static func createSigningResponse( + for method: WalletConnectMethod, + signature: Data, + modifiedTransaction: Data? + ) -> AnyCodable { switch method { case .polkadotSignTransaction, .polkadotSignMessage: let identifier = (0 ... UInt32.max).randomElement() ?? 0 let result = PolkadotExtensionSignerResult( identifier: UInt(identifier), - signature: signature.toHex(includePrefix: true) + signature: signature.toHex(includePrefix: true), + signedTransaction: modifiedTransaction?.toHex(includePrefix: true) ) return AnyCodable(result) diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift index f698dfaab9..36982eaf12 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift @@ -33,7 +33,8 @@ extension WalletConnectStateSigning: WalletConnectStateProtocol { let method = WalletConnectMethod(rawValue: request.method) { let result = WalletConnectSignModelFactory.createSigningResponse( for: method, - signature: signature + signature: signature, + modifiedTransaction: response.modifiedTransaction ) stateMachine.emit( From a41e9c8bfe07d4ef9d02f2d0ea53d75de2c28f04 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 8 Oct 2024 10:59:59 +0200 Subject: [PATCH 03/19] fix tests --- ...pOperationConfirmInteractor+Protocol.swift | 99 +++++++++---------- .../DAppOperationConfirmInteractor.swift | 12 +-- ...DAppExtrinsicBuilderOperationFactory.swift | 22 ++--- .../DAppOperationConfirmTests.swift | 15 ++- 4 files changed, 72 insertions(+), 76 deletions(-) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift index 23d0819331..f66681e7e6 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift @@ -13,7 +13,7 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro } func confirm() { - guard signWrapper == nil, let extrinsicFactory = extrinsicFactory else { + guard !signCancellable.hasCall, let extrinsicFactory = extrinsicFactory else { return } @@ -24,46 +24,43 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro let signWrapper = createSignatureOperation(for: extrinsicFactory, signer: signer) - self.signWrapper = signWrapper - - signWrapper.targetOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard self?.signWrapper != nil else { - return - } - - self?.signWrapper = nil + executeCancellable( + wrapper: signWrapper, + inOperationQueue: operationQueue, + backingCallIn: signCancellable, + runningCallbackIn: .main + ) { [weak self] result in + guard let request = self?.request else { + return + } - guard let request = self?.request else { - return + switch result { + case let .success(signatureResult): + let response = DAppOperationResponse( + signature: signatureResult.signature, + modifiedTransaction: signatureResult.modifiedExtrinsic + ) + + self?.presenter?.didReceive(responseResult: .success(response), for: request) + case let .failure(error): + let interactorError: Error + if let noKeysError = error as? NoKeysSigningWrapperError { + interactorError = noKeysError + } else if let hardwareSigningError = error as? HardwareSigningError { + interactorError = hardwareSigningError + } else if let operationError = error as? DAppOperationConfirmInteractorError { + interactorError = operationError + } else { + interactorError = DAppOperationConfirmInteractorError.signingFailed } - do { - let signature = try signWrapper.targetOperation.extractNoCancellableResultData() - let response = DAppOperationResponse(signature: signature) - self?.presenter?.didReceive(responseResult: .success(response), for: request) - } catch { - let interactorError: Error - if let noKeysError = error as? NoKeysSigningWrapperError { - interactorError = noKeysError - } else if let hardwareSigningError = error as? HardwareSigningError { - interactorError = hardwareSigningError - } else if let operationError = error as? DAppOperationConfirmInteractorError { - interactorError = operationError - } else { - interactorError = DAppOperationConfirmInteractorError.signingFailed - } - - self?.presenter?.didReceive(responseResult: .failure(interactorError), for: request) - } + self?.presenter?.didReceive(responseResult: .failure(interactorError), for: request) } } - - operationQueue.addOperations(signWrapper.allOperations, waitUntilFinished: false) } func reject() { - guard signWrapper == nil else { + guard !signCancellable.hasCall else { return } @@ -72,7 +69,7 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro } func estimateFee() { - guard feeWrapper == nil, let extrinsicFactory = extrinsicFactory else { + guard !feeCancellable.hasCall, let extrinsicFactory = extrinsicFactory else { return } @@ -89,29 +86,21 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro builder } - feeWrapper.targetOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard self?.feeWrapper != nil else { - return - } - - self?.feeWrapper = nil - - do { - let info = try feeWrapper.targetOperation.extractNoCancellableResultData() - - // TODO: Consider fee payer here - let feeModel = FeeOutputModel(value: info, validationProvider: nil) - self?.presenter?.didReceive(feeResult: .success(feeModel)) - } catch { - self?.presenter?.didReceive(feeResult: .failure(error)) - } + executeCancellable( + wrapper: feeWrapper, + inOperationQueue: operationQueue, + backingCallIn: feeCancellable, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case let .success(info): + // TODO: Consider fee payer here + let feeModel = FeeOutputModel(value: info, validationProvider: nil) + self?.presenter?.didReceive(feeResult: .success(feeModel)) + case let .failure(error): + self?.presenter?.didReceive(feeResult: .failure(error)) } } - - self.feeWrapper = feeWrapper - - operationQueue.addOperations(feeWrapper.allOperations, waitUntilFinished: false) } func prepareTxDetails() { diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift index d9025d5e3e..7469e83319 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor.swift @@ -9,7 +9,7 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { let signature: Data let modifiedExtrinsic: Data? } - + let request: DAppOperationRequest let chain: ChainModel @@ -24,8 +24,8 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { var extrinsicFactory: DAppExtrinsicBuilderOperationFactory? var priceProvider: StreamableProvider? - var feeWrapper: CompoundOperationWrapper? - var signWrapper: CompoundOperationWrapper? + let feeCancellable = CancellableCallStore() + var signCancellable = CancellableCallStore() init( request: DAppOperationRequest, @@ -125,7 +125,7 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { let scaleEncoder = codingFactory.createEncoder() - let rawSignature = signatureResult.signedExtrinsic + let rawSignature = signatureResult.signature switch extrinsicFactory.processedResult.account.cryptoType { case .sr25519: @@ -159,8 +159,8 @@ final class DAppOperationConfirmInteractor: DAppOperationBaseInteractor { } let signature = try scaleEncoder.encode() - - return SignatureResult(signature: signature, modifiedExtrinsic: signatureResult.signedExtrinsic) + + return SignatureResult(signature: signature, modifiedExtrinsic: signatureResult.modifiedExtrinsic) } signatureOperation.addDependency(codingFactoryOperation) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift index 9af200a2dd..e909090fdb 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/Model/DAppExtrinsicBuilderOperationFactory.swift @@ -54,9 +54,7 @@ final class DAppExtrinsicBuilderOperationFactory { do { let optMetadataHash = try result.extrinsic.metadataHash.map { try Data(hexString: $0) } - /** - * If a dapp haven't declared a permission to modify extrinsic - return whatever metadataHash present in payload - */ + // If a dapp haven't declared a permission to modify extrinsic - return metadataHash from payload if !result.extrinsic.withSignedTransaction { return .createWithResult( .init( @@ -172,7 +170,7 @@ final class DAppExtrinsicBuilderOperationFactory { builderWrapper.addDependency(operations: [codingFactoryOperation]) - let payloadOperation = ClosureOperation { + let payloadOperation = ClosureOperation { let builderResult = try builderWrapper.targetOperation.extractNoCancellableResultData() let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() @@ -251,23 +249,21 @@ extension DAppExtrinsicBuilderOperationFactory: ExtrinsicBuilderOperationFactory let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() let builder = builderResult.builder - let context = ExtrinsicSigningContext.substrateExtrinsic( - .init( - senderResolution: builderResult.sender, - extrinsicMemo: builder.makeMemo(), - codingFactory: codingFactory - ) + let context = ExtrinsicSigningContext.Substrate( + senderResolution: builderResult.sender, + extrinsicMemo: builder.makeMemo(), + codingFactory: codingFactory ) let rawSignature = try builder.buildRawSignature( using: { data in - try signingClosure(data, context) + try signingClosure(data, .substrateExtrinsic(context)) }, encoder: codingFactory.createEncoder(), metadata: codingFactory.metadata ) - - let modifiedExtrinsic = if builderResult.modifiedOriginalExtrinsic { + + let modifiedExtrinsic: Data? = if builderResult.modifiedOriginalExtrinsic { try builder.signing( with: { _, _ in rawSignature }, context: context, diff --git a/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift b/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift index 5d70c777e7..5fc4d79ba5 100644 --- a/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift +++ b/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift @@ -19,6 +19,8 @@ class DAppOperationConfirmTests: XCTestCase { specVersion: BigUInt(9260).serialize().toHex(includePrefix: true), tip: "0x00000000000000000000000000000000", transactionVersion: "0x00000003", + metadataHash: nil, + withSignedTransaction: nil, signedExtensions: [ "CheckNonZeroSender", "CheckSpecVersion", @@ -119,13 +121,21 @@ class DAppOperationConfirmTests: XCTestCase { operationQueue: operationQueue ) + let storageFacade = SubstrateStorageTestFacade() let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( chain: chain, estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeProvider, userStorageFacade: UserDataStorageTestFacade(), - substrateStorageFacade: SubstrateStorageTestFacade(), + substrateStorageFacade: storageFacade, + operationQueue: operationQueue + ) + + let metadataHashFactory = MetadataHashOperationFactory( + metadataRepositoryFactory: RuntimeMetadataRepositoryFactory( + storageFacade: storageFacade + ), operationQueue: operationQueue ) @@ -135,7 +145,8 @@ class DAppOperationConfirmTests: XCTestCase { runtimeProvider: runtimeProvider, feeEstimationRegistry: feeEstimationRegistry, connection: connection, - signingWrapperFactory: signingWrapperFactory, + signingWrapperFactory: signingWrapperFactory, + metadataHashFactory: metadataHashFactory, priceProviderFactory: priceProvider, currencyManager: CurrencyManagerStub(), operationQueue: operationQueue From b00abc9c71ea8e4d27436164a3008f0d833df87f Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 8 Oct 2024 12:11:53 +0200 Subject: [PATCH 04/19] fix icon --- novawallet/Common/ViewModel/RemoteImageViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Common/ViewModel/RemoteImageViewModel.swift b/novawallet/Common/ViewModel/RemoteImageViewModel.swift index e506aa11c4..88309db25f 100644 --- a/novawallet/Common/ViewModel/RemoteImageViewModel.swift +++ b/novawallet/Common/ViewModel/RemoteImageViewModel.swift @@ -76,7 +76,7 @@ final class RemoteImageSerializer: CacheSerializer { return uiImage } else { let imsvg = SVGKImage(data: data) - return imsvg?.uiImage ?? UIImage() + return imsvg?.uiImage } } } From d037a390dc423796222042ab425556a26e5a1e6f Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 8 Oct 2024 15:17:05 +0200 Subject: [PATCH 05/19] support intermediate proxied nodes in proxy path finder --- .../Proxy/ProxyResolutionPathFinder.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicSenderResolution/Proxy/ProxyResolutionPathFinder.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicSenderResolution/Proxy/ProxyResolutionPathFinder.swift index b6f9240b5d..8376f0e014 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicSenderResolution/Proxy/ProxyResolutionPathFinder.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicSenderResolution/Proxy/ProxyResolutionPathFinder.swift @@ -27,9 +27,18 @@ extension ProxyResolution { } let accounts: [AccountId: [MetaChainAccountResponse]] + let proxieds: [AccountId: MetaChainAccountResponse] init(accounts: [AccountId: [MetaChainAccountResponse]]) { self.accounts = accounts + + proxieds = accounts.reduce(into: [AccountId: MetaChainAccountResponse]()) { accum, keyValue in + guard let account = keyValue.value.first(where: { $0.chainAccount.type == .proxied }) else { + return + } + + accum[keyValue.key] = account + } } private func buildResult( @@ -79,8 +88,10 @@ extension ProxyResolution { } let components = try solution.components.map { oldComponent in + let optAccount = accounts[oldComponent.proxyAccountId] ?? proxieds[oldComponent.proxyAccountId] + guard - let account = accounts[oldComponent.proxyAccountId], + let account = optAccount, let proxyType = oldComponent.applicableTypes.first else { throw PathFinderError.noAccount } From 5b676e9d5296bb157f7b85696bde875b6d687303 Mon Sep 17 00:00:00 2001 From: svojsu Date: Tue, 8 Oct 2024 21:25:48 +0300 Subject: [PATCH 06/19] pass destination asset info to validator --- .../TransferConfirmCrossChainViewFactory.swift | 6 +++++- .../TransferConfirm/TransferConfirmOnChainViewFactory.swift | 1 + .../TransferSetupPresenterFactory+CrossChain.swift | 6 +++++- .../OnChain/TransferSetupPresenterFactory+OnChain.swift | 3 ++- .../Transfer/Validation/TransferDataValidatorFactory.swift | 5 ++++- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift index 771241db6b..e7e2d6ded2 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift @@ -49,7 +49,10 @@ struct TransferConfirmCrossChainViewFactory { utilityBalanceViewModelFactory = nil } - guard let utilityAssetInfo = originChainAsset.chain.utilityAssets().first?.displayInfo else { + guard + let utilityAssetInfo = originChainAsset.chain.utilityAssetDisplayInfo(), + let destUtilityAssetInfo = destinationAsset.chain.utilityAssetDisplayInfo() + else { return nil } @@ -57,6 +60,7 @@ struct TransferConfirmCrossChainViewFactory { presentable: wireframe, assetDisplayInfo: originChainAsset.assetDisplayInfo, utilityAssetInfo: utilityAssetInfo, + destUtilityAssetInfo: destUtilityAssetInfo, priceAssetInfoFactory: priceAssetInfoFactory ) diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index f8fd50c81d..38efcbc129 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -85,6 +85,7 @@ struct TransferConfirmOnChainViewFactory { presentable: wireframe, assetDisplayInfo: chainAsset.assetDisplayInfo, utilityAssetInfo: utilityAssetInfo, + destUtilityAssetInfo: utilityAssetInfo, priceAssetInfoFactory: priceAssetInfoFactory ) diff --git a/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift b/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift index 938a33c05d..566d44079b 100644 --- a/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift @@ -47,7 +47,10 @@ extension TransferSetupPresenterFactory { utilityBalanceViewModelFactory = nil } - guard let utilityAssetInfo = originChainAsset.chain.utilityAssets().first?.displayInfo else { + guard + let utilityAssetInfo = originChainAsset.chain.utilityAssetDisplayInfo(), + let destUtilityAssetInfo = destinationChainAsset.chain.utilityAssetDisplayInfo() + else { return nil } @@ -55,6 +58,7 @@ extension TransferSetupPresenterFactory { presentable: wireframe, assetDisplayInfo: originChainAsset.assetDisplayInfo, utilityAssetInfo: utilityAssetInfo, + destUtilityAssetInfo: destUtilityAssetInfo, priceAssetInfoFactory: priceAssetInfoFactory ) diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift index 317c52bb20..9b936e0534 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift @@ -69,7 +69,8 @@ extension TransferSetupPresenterFactory { let dataValidatingFactory = TransferDataValidatorFactory( presentable: wireframe, assetDisplayInfo: chainAsset.assetDisplayInfo, - utilityAssetInfo: utilityChainAsset.asset.displayInfo, + utilityAssetInfo: utilityChainAsset.assetDisplayInfo, + destUtilityAssetInfo: utilityChainAsset.assetDisplayInfo, priceAssetInfoFactory: priceAssetInfoFactory ) diff --git a/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift b/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift index b4c0536934..aafa75803f 100644 --- a/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift +++ b/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift @@ -81,6 +81,7 @@ final class TransferDataValidatorFactory: TransferDataValidatorFactoryProtocol { var basePresentable: BaseErrorPresentable { presentable } let assetDisplayInfo: AssetBalanceDisplayInfo let utilityAssetInfo: AssetBalanceDisplayInfo + let destUtilityAssetInfo: AssetBalanceDisplayInfo let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol let presentable: TransferErrorPresentable @@ -89,11 +90,13 @@ final class TransferDataValidatorFactory: TransferDataValidatorFactoryProtocol { presentable: TransferErrorPresentable, assetDisplayInfo: AssetBalanceDisplayInfo, utilityAssetInfo: AssetBalanceDisplayInfo, + destUtilityAssetInfo: AssetBalanceDisplayInfo, priceAssetInfoFactory: PriceAssetInfoFactoryProtocol ) { self.presentable = presentable self.assetDisplayInfo = assetDisplayInfo self.utilityAssetInfo = utilityAssetInfo + self.destUtilityAssetInfo = destUtilityAssetInfo self.priceAssetInfoFactory = priceAssetInfoFactory } @@ -160,7 +163,7 @@ final class TransferDataValidatorFactory: TransferDataValidatorFactoryProtocol { return } - let assetInfo = strongSelf.utilityAssetInfo + let assetInfo = strongSelf.destUtilityAssetInfo self?.presentable.presentNoReceiverAccount( for: assetInfo.symbol, From e80c6f18ec0640aee36b4f38be9e2a38aff15e9e Mon Sep 17 00:00:00 2001 From: svojsu Date: Tue, 8 Oct 2024 22:02:01 +0300 Subject: [PATCH 07/19] validate row model isActive --- .../Setup/GetTokenOptions/GetTokenOptionsViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift index 71f02f224c..3cbf5ba6db 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift @@ -28,6 +28,10 @@ final class GetTokenOptionsViewController: ModalPickerViewController< override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) + guard viewModels[indexPath.row].value(for: selectedLocale).isActive else { + return + } + operationPresenter.selectOption(at: indexPath.row) } } From 0ac36bbf01e00a549263bba1437eed7a17553351 Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 9 Oct 2024 17:09:35 +0300 Subject: [PATCH 08/19] pre-configured network management fixes - save network with all flags and options - show all network nodes --- .../CustomNetworkAddInteractor.swift | 2 +- .../CustomNetworkSetupFinishStrategy.swift | 55 ++++++++++++++++++- .../NetworkDetailsViewModelFactory.swift | 2 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkAddInteractor.swift b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkAddInteractor.swift index 5b09dea307..b0990b062b 100644 --- a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkAddInteractor.swift +++ b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkAddInteractor.swift @@ -43,7 +43,7 @@ final class CustomNetworkAddInteractor: CustomNetworkBaseInteractor { extension CustomNetworkAddInteractor: CustomNetworkAddInteractorInputProtocol { func addNetwork(with request: CustomNetwork.AddRequest) { - setupFinishStrategy = setupFinishStrategyFactory.createAddNewStrategy() + setupFinishStrategy = setupFinishStrategyFactory.createAddNewStrategy(preConfiguredNetwork: networkToAdd) let type: ChainType = if let networkToAdd { networkToAdd.isEthereumBased ? .evm : .substrate diff --git a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift index 0461ed1701..e4e59bfb0e 100644 --- a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift +++ b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift @@ -16,9 +16,10 @@ struct CustomNetworkSetupFinishStrategyFactory { self.operationQueue = operationQueue } - func createAddNewStrategy() -> CustomNetworkSetupFinishStrategy { + func createAddNewStrategy(preConfiguredNetwork: ChainModel? = nil) -> CustomNetworkSetupFinishStrategy { CustomNetworkAddNewStrategy( repository: repository, + preConfiguredNetwork: preConfiguredNetwork, operationQueue: operationQueue, chainRegistry: chainRegistry ) @@ -90,6 +91,7 @@ extension CustomNetworkSetupFinishStrategy { struct CustomNetworkAddNewStrategy: CustomNetworkSetupFinishStrategy { let repository: AnyDataProviderRepository + let preConfiguredNetwork: ChainModel? let operationQueue: OperationQueue let chainRegistry: ChainRegistryProtocol @@ -99,8 +101,14 @@ struct CustomNetworkAddNewStrategy: CustomNetworkSetupFinishStrategy { output: CustomNetworkBaseInteractorOutputProtocol? ) { processWithCheck(network, output: output) { + let networkToSave = if let preConfiguredNetwork { + updatePreConfigured(network: preConfiguredNetwork, using: network) + } else { + network + } + let saveOperation = repository.saveOperation( - { [network] }, + { [networkToSave] }, { [] } ) @@ -120,6 +128,49 @@ struct CustomNetworkAddNewStrategy: CustomNetworkSetupFinishStrategy { } } } + + private func updatePreConfigured( + network: ChainModel, + using setUpNetwork: ChainModel + ) -> ChainModel { + let explorers: [ChainModel.Explorer]? = if let newExplorers = setUpNetwork.explorers, !newExplorers.isEmpty { + newExplorers + } else { + network.explorers + } + + let assets: Set = { + if + let asset = setUpNetwork.assets.first, + !network.assets.contains(where: { $0.assetId == asset.assetId }) { + [asset] + } else { + network.assets + } + }() + + let nodes: Set = setUpNetwork.nodes.union(network.nodes) + + return ChainModel( + chainId: network.chainId, + parentId: network.parentId, + name: setUpNetwork.name, + assets: assets, + nodes: nodes, + nodeSwitchStrategy: network.nodeSwitchStrategy, + addressPrefix: network.addressPrefix, + types: network.types, + icon: network.icon, + options: network.options, + externalApis: network.externalApis, + explorers: explorers, + order: network.order, + additional: network.additional, + syncMode: network.syncMode, + source: .user, + connectionMode: network.connectionMode + ) + } } // MARK: - Provide diff --git a/novawallet/Modules/NetworkManagement/NetworkDetails/NetworkDetailsViewModelFactory.swift b/novawallet/Modules/NetworkManagement/NetworkDetails/NetworkDetailsViewModelFactory.swift index c9b4bf3f26..d55bf4ae81 100644 --- a/novawallet/Modules/NetworkManagement/NetworkDetails/NetworkDetailsViewModelFactory.swift +++ b/novawallet/Modules/NetworkManagement/NetworkDetails/NetworkDetailsViewModelFactory.swift @@ -43,7 +43,7 @@ class NetworkDetailsViewModelFactory { ) ] - if chain.source == .remote { + if !remoteNodes.isEmpty { let defaultNodesSection = createNodesSection( with: remoteNodes, selectedNode: selectedNode, From 8554d6465b333ba2d3c2ee4738a09453512395cd Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 9 Oct 2024 21:23:37 +0300 Subject: [PATCH 09/19] fix modify network --- .../CustomNetworkBaseInteractor.swift | 30 ++-- .../CustomNetworkSetupFinishStrategy.swift | 159 ++++++++++++------ 2 files changed, 127 insertions(+), 62 deletions(-) diff --git a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkBaseInteractor.swift b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkBaseInteractor.swift index ce49743564..c729dc9d1c 100644 --- a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkBaseInteractor.swift +++ b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkBaseInteractor.swift @@ -1,8 +1,7 @@ import SubstrateSdk import Operation_iOS -class CustomNetworkBaseInteractor: NetworkNodeCreatorTrait, - NetworkNodeConnectingTrait { +class CustomNetworkBaseInteractor: NetworkNodeCreatorTrait { weak var presenter: CustomNetworkBaseInteractorOutputProtocol? let chainRegistry: ChainRegistryProtocol @@ -22,6 +21,8 @@ class CustomNetworkBaseInteractor: NetworkNodeCreatorTrait, private var partialChain: PartialCustomChainModel? + private var setupNetworkCancellable = CancellableCallStore() + init( chainRegistry: ChainRegistryProtocol, customNetworkSetupFactory: CustomNetworkSetupFactoryProtocol, @@ -45,6 +46,10 @@ class CustomNetworkBaseInteractor: NetworkNodeCreatorTrait, } func modify(with request: CustomNetwork.ModifyRequest) { + setupFinishStrategy = setupFinishStrategyFactory.createModifyStrategy( + networkToModify: request.existingNetwork + ) + let mainAsset = request.existingNetwork.assets.first(where: { $0.assetId == 0 }) let evmChainId: UInt64? = if let chainId = request.chainId, let intChainId = Int(chainId) { @@ -80,7 +85,6 @@ class CustomNetworkBaseInteractor: NetworkNodeCreatorTrait, setupConnection( for: partialChain, node: request.node, - replacing: request.node, networkSetupType: .full ) } catch { @@ -138,7 +142,6 @@ class CustomNetworkBaseInteractor: NetworkNodeCreatorTrait, setupConnection( for: partialChain, node: node, - replacing: request.replacingNode, networkSetupType: request.networkSetupType ) } catch { @@ -183,25 +186,27 @@ private extension CustomNetworkBaseInteractor { func setupConnection( for partialChain: PartialCustomChainModel, node: ChainNodeModel, - replacing existingNode: ChainNodeModel?, networkSetupType: CustomNetworkSetupOperationType ) { do { - let connection = try connect( - to: node, - replacing: existingNode, + guard NSPredicate.ws.evaluate(with: node.url) else { + throw NetworkNodeConnectingError.wrongFormat + } + + let connection = try connectionFactory.createConnection( + for: node, chain: partialChain, - urlPredicate: NSPredicate.ws + delegate: self ) + currentConnection = connection + setupNetworkWrapper = customNetworkSetupFactory.createOperation( with: partialChain, connection: connection, node: node, type: networkSetupType ) - - currentConnection = connection } catch { let customNetworkError = CustomNetworkBaseInteractorError(from: error) presenter?.didReceive(customNetworkError) @@ -242,9 +247,10 @@ private extension CustomNetworkBaseInteractor { func handleConnected() { guard let setupNetworkWrapper else { return } - execute( + executeCancellable( wrapper: setupNetworkWrapper, inOperationQueue: operationQueue, + backingCallIn: setupNetworkCancellable, runningCallbackIn: .main ) { [weak self] result in switch result { diff --git a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift index e4e59bfb0e..80310c5355 100644 --- a/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift +++ b/novawallet/Modules/NetworkManagement/CustomNetwork/Interactor/CustomNetworkSetupFinishStrategy.swift @@ -25,6 +25,15 @@ struct CustomNetworkSetupFinishStrategyFactory { ) } + func createModifyStrategy(networkToModify: ChainModel) -> CustomNetworkSetupFinishStrategy { + CustomNetworkModifyStrategy( + repository: repository, + networkToModify: networkToModify, + operationQueue: operationQueue, + chainRegistry: chainRegistry + ) + } + func createProvideStrategy() -> CustomNetworkSetupFinishStrategy { CustomNetworkProvideStrategy(chainRegistry: chainRegistry) } @@ -53,6 +62,61 @@ protocol CustomNetworkSetupFinishStrategy { } extension CustomNetworkSetupFinishStrategy { + func updatePreConfigured( + network: ChainModel, + using setUpNetwork: ChainModel + ) -> ChainModel { + let explorers: [ChainModel.Explorer]? = { + if + let newExplorers = setUpNetwork.explorers, + !newExplorers.isEmpty { + newExplorers + } else { + network.explorers + } + }() + + let assets: Set = { + if + let asset = setUpNetwork.assets.first, + !network.assets.contains(where: { $0.assetId == asset.assetId }) { + [asset] + } else { + network.assets + } + }() + + let nodes: Set = { + if + let node = setUpNetwork.nodes.first, + !network.nodes.contains(where: { $0.url == node.url }) { + setUpNetwork.nodes.union(network.nodes) + } else { + network.nodes + } + }() + + return ChainModel( + chainId: network.chainId, + parentId: network.parentId, + name: setUpNetwork.name, + assets: assets, + nodes: nodes, + nodeSwitchStrategy: network.nodeSwitchStrategy, + addressPrefix: network.addressPrefix, + types: network.types, + icon: network.icon, + options: network.options, + externalApis: network.externalApis, + explorers: explorers, + order: network.order, + additional: network.additional, + syncMode: network.syncMode, + source: .user, + connectionMode: network.connectionMode + ) + } + func processWithCheck( _ network: ChainModel, output: CustomNetworkBaseInteractorOutputProtocol?, @@ -128,49 +192,6 @@ struct CustomNetworkAddNewStrategy: CustomNetworkSetupFinishStrategy { } } } - - private func updatePreConfigured( - network: ChainModel, - using setUpNetwork: ChainModel - ) -> ChainModel { - let explorers: [ChainModel.Explorer]? = if let newExplorers = setUpNetwork.explorers, !newExplorers.isEmpty { - newExplorers - } else { - network.explorers - } - - let assets: Set = { - if - let asset = setUpNetwork.assets.first, - !network.assets.contains(where: { $0.assetId == asset.assetId }) { - [asset] - } else { - network.assets - } - }() - - let nodes: Set = setUpNetwork.nodes.union(network.nodes) - - return ChainModel( - chainId: network.chainId, - parentId: network.parentId, - name: setUpNetwork.name, - assets: assets, - nodes: nodes, - nodeSwitchStrategy: network.nodeSwitchStrategy, - addressPrefix: network.addressPrefix, - types: network.types, - icon: network.icon, - options: network.options, - externalApis: network.externalApis, - explorers: explorers, - order: network.order, - additional: network.additional, - syncMode: network.syncMode, - source: .user, - connectionMode: network.connectionMode - ) - } } // MARK: - Provide @@ -182,14 +203,12 @@ struct CustomNetworkProvideStrategy: CustomNetworkSetupFinishStrategy { for network: ChainModel, output: CustomNetworkBaseInteractorOutputProtocol? ) { - processWithCheck(network, output: output) { - guard let selectedNode = network.nodes.first else { return } + guard let selectedNode = network.nodes.first else { return } - output?.didReceive( - chain: network, - selectedNode: selectedNode - ) - } + output?.didReceive( + chain: network, + selectedNode: selectedNode + ) } } @@ -247,3 +266,43 @@ struct CustomNetworkEditStrategy: CustomNetworkSetupFinishStrategy { } } } + +// MARK: - Modify + +struct CustomNetworkModifyStrategy: CustomNetworkSetupFinishStrategy { + let repository: AnyDataProviderRepository + let networkToModify: ChainModel + let operationQueue: OperationQueue + + let chainRegistry: ChainRegistryProtocol + + func handleSetupFinished( + for network: ChainModel, + output: CustomNetworkBaseInteractorOutputProtocol? + ) { + let networkToSave = updatePreConfigured( + network: networkToModify, + using: network + ) + + let saveOperation = repository.saveOperation( + { [networkToSave] }, + { [] } + ) + + execute( + operation: saveOperation, + inOperationQueue: operationQueue, + runningCallbackIn: .main + ) { result in + switch result { + case .success: + output?.didFinishWorkWithNetwork() + case .failure: + output?.didReceive( + .common(innerError: .dataCorruption) + ) + } + } + } +} From b926f09339df4b059cca9334107f4aaade85c81b Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 9 Oct 2024 21:47:56 +0300 Subject: [PATCH 10/19] elevate support section and reorder rows --- .../Settings/ViewModel/SettingsViewModelFactory.swift | 10 +++++----- novawallet/en.lproj/Localizable.strings | 4 ++-- novawallet/ru.lproj/Localizable.strings | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/novawallet/Modules/Settings/ViewModel/SettingsViewModelFactory.swift b/novawallet/Modules/Settings/ViewModel/SettingsViewModelFactory.swift index 03627fcb72..70354d7148 100644 --- a/novawallet/Modules/Settings/ViewModel/SettingsViewModelFactory.swift +++ b/novawallet/Modules/Settings/ViewModel/SettingsViewModelFactory.swift @@ -60,16 +60,16 @@ final class SettingsViewModelFactory: SettingsViewModelFactoryProtocol { ), createCommonViewViewModel(row: .changePin, locale: locale) ].compactMap { $0 }), + (.support, [ + createCommonViewViewModel(row: .email, locale: locale), + createCommonViewViewModel(row: .wiki, locale: locale), + createCommonViewViewModel(row: .rateUs, locale: locale) + ]), (.community, [ createCommonViewViewModel(row: .telegram, locale: locale), createCommonViewViewModel(row: .twitter, locale: locale), createCommonViewViewModel(row: .youtube, locale: locale) ]), - (.support, [ - createCommonViewViewModel(row: .rateUs, locale: locale), - createCommonViewViewModel(row: .wiki, locale: locale), - createCommonViewViewModel(row: .email, locale: locale) - ]), (.about, [ createCommonViewViewModel(row: .website, locale: locale), createCommonViewViewModel(row: .github, locale: locale), diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index a9e57f9b0c..b50d158a65 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "waiting for the next era (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Support & Feedback"; -"settings.email" = "Email"; +"settings.email" = "Get support via Email"; "wallet.manage.assets.title" = "Manage assets"; "staking.reward.payouts.title_v2_2_0" = "Unpaid rewards"; "staking.setup.restake.title_v2_2_0" = "Restake rewards"; @@ -1420,7 +1420,7 @@ "common.swap.title" = "Swap"; "common.action.repeat.operation" = "Repeat the operation"; "swaps.error.rate.was.updated.message" = "Old rate: %@.\nNew rate: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki & Help Center"; "common.not.enough.fee.message_v3.8.0" = "You don’t have enough balance to pay the network fee of %@.\nAvailable balance to pay fee after operation: %@"; "deeplink.error.invalid.chainId.message" = "Chain is not found"; "deeplink.error.no.governance.type.message" = "Governance type is not specified"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 4a36a5fe31..5d64cbc0c8 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "ожидание следующей эры (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Поддержка и обратная связь"; -"settings.email" = "Написать разработчикам"; +"settings.email" = "Получить поддержку по Email"; "wallet.manage.assets.title" = "Управление ассетами"; "staking.reward.payouts.title_v2_2_0" = "Невыплаченные награды"; "staking.setup.restake.title_v2_2_0" = "Увеличивать стейк"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Обмен"; "common.action.repeat.operation" = "Повторить операцию"; "swaps.error.rate.was.updated.message" = "Было: %@.\nСтало: %@"; -"settings.wiki" = "Руководство пользователя"; +"settings.wiki" = "Руководство и Центр Помощи"; "common.not.enough.fee.message_v3.8.0" = "У вас недостаточно средств для оплаты комиссии сети в размере %@.\nДоступный баланс для оплаты комиссии после операции: %@"; "deeplink.error.invalid.chainId.message" = "Сеть не найдена"; "deeplink.error.no.governance.type.message" = "Не указан тип управления"; @@ -1756,4 +1756,4 @@ "swipe.gov.referenda.excluded.alert.message" = "Некоторые референдумы больше недоступны для голосования или у вас может быть недостаточно токенов для голосования. Доступно для голосования: %@."; "gov.referendum.completed.title.with.index" = "Референдум #%li завершен"; "gov.referendum.completed.message.with.index" = "Референдум №%li завершен, голосование окончено"; -"common.use.max.due.fee.message" = "Вы можете использовать до %@, так как вам нужно заплатить %@ комиссию сети."; \ No newline at end of file +"common.use.max.due.fee.message" = "Вы можете использовать до %@, так как вам нужно заплатить %@ комиссию сети."; From df5abe46f35d1b0d79c622b5813603d41b1d486c Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 11 Oct 2024 06:05:08 +0200 Subject: [PATCH 11/19] remove nested proxies --- .../Storage/WalletsUpdateMediator.swift | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/novawallet/Common/Storage/WalletsUpdateMediator.swift b/novawallet/Common/Storage/WalletsUpdateMediator.swift index 87afd5a460..24e31d6a3b 100644 --- a/novawallet/Common/Storage/WalletsUpdateMediator.swift +++ b/novawallet/Common/Storage/WalletsUpdateMediator.swift @@ -34,6 +34,44 @@ final class WalletUpdateMediator { self.operationQueue = operationQueue } + private static func nonProxiedWalletReachable( + from proxiedWallet: ManagedMetaAccountModel, + wallets: [ManagedMetaAccountModel], + removeIds: Set + ) -> Bool { + var currentProxieds: Set = [proxiedWallet.info.metaId] + var prevProxieds = currentProxieds + var foundWallets: [MetaAccountModel.Id: ManagedMetaAccountModel] = [proxiedWallet.info.metaId: proxiedWallet] + + repeat { + let newReachableWallets: [ManagedMetaAccountModel] = currentProxieds.flatMap { proxiedId in + guard + let proxied = foundWallets[proxiedId], + let chainAccount = proxied.info.chainAccounts.first(where: { $0.proxy != nil }), + let proxy = chainAccount.proxy else { + return [ManagedMetaAccountModel]() + } + + return wallets.filter { $0.info.has(accountId: proxy.accountId, chainId: chainAccount.chainId) } + } + + if newReachableWallets.contains( + where: { $0.info.type != .proxied && !removeIds.contains($0.info.metaId) } + ) { + return true + } + + foundWallets = newReachableWallets.reduce(into: foundWallets) { + $0[$1.info.metaId] = $1 + } + + prevProxieds = currentProxieds + currentProxieds = Set(foundWallets.keys) + } while prevProxieds != currentProxieds + + return false + } + private static func includeProxiedsToRemoveSet( starting removeIds: Set, wallets: [ManagedMetaAccountModel] @@ -46,20 +84,12 @@ final class WalletUpdateMediator { // we can have nested proxieds so we make sure to remove them all repeat { - let newProxiedIdsToRemove = allProxieds.filter { proxiedWallet in - guard - let chainAccount = proxiedWallet.info.chainAccounts.first(where: { $0.proxy != nil }), - let proxy = chainAccount.proxy else { - return false - } - - return wallets.allSatisfy { wallet in - guard !newRemovedIds.contains(wallet.identifier) else { - return true - } - - return !wallet.info.has(accountId: proxy.accountId, chainId: chainAccount.chainId) - } + let newProxiedIdsToRemove = allProxieds.filter { proxied in + !nonProxiedWalletReachable( + from: proxied, + wallets: wallets, + removeIds: newRemovedIds + ) }.map(\.identifier) oldRemovedIds = newRemovedIds From 7203939957d1b37f1e5e31b9a16a78732148d232 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 11 Oct 2024 06:24:30 +0200 Subject: [PATCH 12/19] fix typo --- novawallet/Common/Services/Proxy/ChainProxySyncService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Common/Services/Proxy/ChainProxySyncService.swift b/novawallet/Common/Services/Proxy/ChainProxySyncService.swift index e43641fbf1..cebf2d2e0f 100644 --- a/novawallet/Common/Services/Proxy/ChainProxySyncService.swift +++ b/novawallet/Common/Services/Proxy/ChainProxySyncService.swift @@ -191,7 +191,7 @@ final class ChainProxySyncService: ObservableSyncService, ChainProxySyncServiceP var proxies: [ProxiedAccountId: [ProxyAccount]] = [:] repeat { - // We only need remote proxieds for current proxies and we don't support delaed proxies + // We only need remote proxieds for current proxies and we don't support delayed proxies proxies = proxyList.compactMapValues { accounts in accounts.filter { !$0.hasDelay && possibleProxiesIds.contains($0.accountId) From c77e3f21bfefead878e68944cd8f84561f6f5b74 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 11 Oct 2024 07:38:47 +0200 Subject: [PATCH 13/19] fix tests --- .../Storage/WalletUpdateMediatorTests.swift | 106 ++++++++++++++++-- novawalletTests/Helper/AccountGenerator.swift | 5 +- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/novawalletTests/Common/Storage/WalletUpdateMediatorTests.swift b/novawalletTests/Common/Storage/WalletUpdateMediatorTests.swift index 69d33e7878..b614aa7f21 100644 --- a/novawalletTests/Common/Storage/WalletUpdateMediatorTests.swift +++ b/novawalletTests/Common/Storage/WalletUpdateMediatorTests.swift @@ -85,10 +85,14 @@ final class WalletUpdateMediatorTests: XCTestCase { var proxiedForProxiedWallet1: ManagedMetaAccountModel + var recursiveProxiedForProxiedWallet1: ManagedMetaAccountModel + init(reversedOrder: Bool = false) { - let allOrders: [UInt32] = (0...4).map({ $0 }) + let allOrders: [UInt32] = (0...5).map({ $0 }) let orders = reversedOrder ? allOrders.reversed() : allOrders + let chainId = Data.random(of: 32)!.toHex() + proxyWallet1 = ManagedMetaAccountModel( info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 0), isSelected: false, @@ -105,7 +109,7 @@ final class WalletUpdateMediatorTests: XCTestCase { type: .any, accountId: proxyWallet1.info.substrateAccountId!, status: .active - )) + ), chainId: chainId) proxiedForWallet1 = ManagedMetaAccountModel( info: AccountGenerator.generateMetaAccount(with: [proxied1ChainAccount], type: .proxied), @@ -117,7 +121,7 @@ final class WalletUpdateMediatorTests: XCTestCase { type: .staking, accountId: proxyWallet2.info.substrateAccountId!, status: .active - )) + ), chainId: chainId) proxiedForWallet2 = ManagedMetaAccountModel( info: AccountGenerator.generateMetaAccount(with: [proxied2ChainAccount], type: .proxied), @@ -131,18 +135,42 @@ final class WalletUpdateMediatorTests: XCTestCase { type: .any, accountId: proxied1ChainAccount.accountId, status: .active - )) + ), chainId: chainId) proxiedForProxiedWallet1 = ManagedMetaAccountModel( info: AccountGenerator.generateMetaAccount(with: [proxied3ChainAccount], type: .proxied), isSelected: false, order: orders[4] ) + + // and cyclic proxied from proxied1 to proxied3 + + let proxied4ChainAccount = ChainAccountModel( + chainId: proxied1ChainAccount.chainId, + accountId: proxied1ChainAccount.accountId, + publicKey: proxied1ChainAccount.publicKey, + cryptoType: 0, + proxy: .init( + type: .any, + accountId: proxied3ChainAccount.accountId, + status: .active + ) + ) + + recursiveProxiedForProxiedWallet1 = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(with: [proxied4ChainAccount], type: .proxied), + isSelected: false, + order: orders[5] + ) } - var allWallets: [ManagedMetaAccountModel] { + var allWithoutRecursive: [ManagedMetaAccountModel] { [proxyWallet1, proxyWallet2, proxiedForWallet1, proxiedForWallet2, proxiedForProxiedWallet1] } + + var all: [ManagedMetaAccountModel] { + [proxyWallet1, proxyWallet2, proxiedForWallet1, proxiedForWallet2, proxiedForProxiedWallet1, recursiveProxiedForProxiedWallet1] + } } func testAutoSwitchWalletIfSelectedOneRemoved() { @@ -226,7 +254,35 @@ final class WalletUpdateMediatorTests: XCTestCase { let common = Common() let proxyWallets = ProxyWallets(reversedOrder: true) - common.setup(with: proxyWallets.allWallets) + common.setup(with: proxyWallets.allWithoutRecursive) + try common.select(walletId: proxyWallets.proxiedForWallet1.identifier) + + XCTAssertEqual(common.selectedAccountSettings.value.identifier, proxyWallets.proxiedForWallet1.identifier) + + // then + + do { + let result = try common.update(with: [], remove: [proxyWallets.proxyWallet1]) + + let remainedWallets = try common.allWallets() + let remainedIdentifiers = remainedWallets.map { $0.identifier } + + XCTAssertTrue(result.isWalletSwitched) + XCTAssertEqual(result.selectedWallet?.identifier, common.selectedAccountSettings.value.identifier) + XCTAssertEqual(common.selectedAccountSettings.value.identifier, proxyWallets.proxyWallet2.identifier) + XCTAssertEqual(Set(remainedIdentifiers), [proxyWallets.proxyWallet2.identifier, proxyWallets.proxiedForWallet2.identifier]) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testRemoveRecursiveProxiedsWhenProxyRemoved() throws { + // given + + let common = Common() + let proxyWallets = ProxyWallets(reversedOrder: true) + + common.setup(with: proxyWallets.all) try common.select(walletId: proxyWallets.proxiedForWallet1.identifier) XCTAssertEqual(common.selectedAccountSettings.value.identifier, proxyWallets.proxiedForWallet1.identifier) @@ -248,13 +304,49 @@ final class WalletUpdateMediatorTests: XCTestCase { } } + func testRecursiveWalletNotRemovedIfReachable() throws { + // given + + let common = Common() + let proxyWallets = ProxyWallets(reversedOrder: true) + + common.setup(with: proxyWallets.all) + try common.select(walletId: proxyWallets.proxiedForWallet2.identifier) + + XCTAssertEqual(common.selectedAccountSettings.value.identifier, proxyWallets.proxiedForWallet2.identifier) + + // then + + do { + let result = try common.update(with: [], remove: [proxyWallets.proxyWallet2]) + + let remainedWallets = try common.allWallets() + let remainedIdentifiers = remainedWallets.map { $0.identifier } + + XCTAssertTrue(result.isWalletSwitched) + XCTAssertEqual(result.selectedWallet?.identifier, common.selectedAccountSettings.value.identifier) + XCTAssertEqual(common.selectedAccountSettings.value.identifier, proxyWallets.proxyWallet1.identifier) + XCTAssertEqual( + Set(remainedIdentifiers), + [ + proxyWallets.proxyWallet1.identifier, + proxyWallets.proxiedForWallet1.identifier, + proxyWallets.proxiedForProxiedWallet1.identifier, + proxyWallets.recursiveProxiedForProxiedWallet1.identifier + ] + ) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + func testAutoSwitchWalletIfProxiedRevoked() throws { // given let common = Common() let proxyWallets = ProxyWallets() - common.setup(with: proxyWallets.allWallets) + common.setup(with: proxyWallets.all) let proxied = proxyWallets.proxiedForWallet2 try common.select(walletId: proxied.identifier) diff --git a/novawalletTests/Helper/AccountGenerator.swift b/novawalletTests/Helper/AccountGenerator.swift index ba6f8d96e9..50c24d7c12 100644 --- a/novawalletTests/Helper/AccountGenerator.swift +++ b/novawalletTests/Helper/AccountGenerator.swift @@ -49,10 +49,11 @@ enum AccountGenerator { } static func generateProxiedChainAccount( - for model: ProxyAccountModel + for model: ProxyAccountModel, + chainId: ChainModel.Id ) -> ChainAccountModel { ChainAccountModel( - chainId: Data.random(of: 32)!.toHex(), + chainId: chainId, accountId: Data.random(of: 32)!, publicKey: Data.random(of: 32)!, cryptoType: 0, From 15f1d07ddb490e005b1d5314a2779d32e2328679 Mon Sep 17 00:00:00 2001 From: svojsu Date: Fri, 11 Oct 2024 11:25:50 +0300 Subject: [PATCH 14/19] validate has chain account for swipe gov banner --- .../Presenter/ReferendumsPresenter+ReferendumSections.swift | 6 +++++- .../Referendums/Presenter/ReferendumsPresenter.swift | 3 +++ .../Modules/Vote/Parent/VoteChildPresenterFactory.swift | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift index c0b5032d07..1506356278 100644 --- a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift +++ b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift @@ -41,7 +41,11 @@ extension ReferendumsPresenter { } func createSwipeGovSection() -> ReferendumsSection? { - guard supportsSwipeGov == true else { + guard + supportsSwipeGov == true, + let chain, + selectedMetaAccount.fetch(for: chain.accountRequest()) != nil + else { return nil } diff --git a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift index 360eececf6..3cea622fa9 100644 --- a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift +++ b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift @@ -14,6 +14,7 @@ final class ReferendumsPresenter { let statusViewModelFactory: ReferendumStatusViewModelFactoryProtocol let assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol let sorting: ReferendumsSorting + let selectedMetaAccount: MetaAccountModel let logger: LoggerProtocol private(set) lazy var chainBalanceFactory = ChainBalanceViewModelFactory() @@ -73,6 +74,7 @@ final class ReferendumsPresenter { activityViewModelFactory: ReferendumsActivityViewModelFactoryProtocol, statusViewModelFactory: ReferendumStatusViewModelFactoryProtocol, assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol, + selectedMetaAccount: MetaAccountModel, sorting: ReferendumsSorting, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol @@ -85,6 +87,7 @@ final class ReferendumsPresenter { self.activityViewModelFactory = activityViewModelFactory self.statusViewModelFactory = statusViewModelFactory self.assetBalanceFormatterFactory = assetBalanceFormatterFactory + self.selectedMetaAccount = selectedMetaAccount self.sorting = sorting self.logger = logger self.localizationManager = localizationManager diff --git a/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift b/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift index bf5864b4de..f6c1ed0d85 100644 --- a/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift +++ b/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift @@ -214,6 +214,7 @@ extension VoteChildPresenterFactory: VoteChildPresenterFactoryProtocol { activityViewModelFactory: activityViewModelFactory, statusViewModelFactory: statusViewModelFactory, assetBalanceFormatterFactory: assetBalanceFormatterFactory, + selectedMetaAccount: wallet, sorting: ReferendumsTimeSortingProvider(), localizationManager: localizationManager, logger: logger From 8742287e1b400c09ad2b89807f87a3959aad65f9 Mon Sep 17 00:00:00 2001 From: svojsu Date: Fri, 11 Oct 2024 13:35:24 +0300 Subject: [PATCH 15/19] use common account validation logic --- .../ReferendumDetailsPresenter.swift | 86 ++++++++++++------- .../ReferendumDetailsProtocols.swift | 2 +- ...erendumsPresenter+ReferendumSections.swift | 6 +- .../Presenter/ReferendumsPresenter.swift | 50 ++++++++++- .../Referendums/ReferendumsProtocols.swift | 9 +- .../Referendums/ReferendumsWireframe.swift | 11 +++ .../Parent/VoteChildPresenterFactory.swift | 1 + 7 files changed, 123 insertions(+), 42 deletions(-) diff --git a/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift b/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift index 9a3c5e8eb3..8dd4c008e0 100644 --- a/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift +++ b/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift @@ -392,6 +392,40 @@ extension ReferendumDetailsPresenter { return button } + + private func createAccountValidationHandlers() -> ( + success: () -> Void, + newAccount: () -> Void + ) { + let successHandler: () -> Void = { [weak self] in + guard let self else { return } + + let initData = ReferendumVotingInitData( + votesResult: nil, + blockNumber: blockNumber, + blockTime: blockTime, + referendum: referendum, + lockDiff: nil + ) + + wireframe.showVote( + from: view, + referendum: referendum, + initData: initData + ) + } + + let newAccountHandler: () -> Void = { [weak self] in + guard let self else { return } + + wireframe.showWalletDetails( + from: view, + wallet: wallet + ) + } + + return (successHandler, newAccountHandler) + } } // MARK: ReferendumDetailsPresenterProtocol @@ -408,42 +442,28 @@ extension ReferendumDetailsPresenter: ReferendumDetailsPresenterProtocol { return } - if wallet.fetch(for: chain.accountRequest()) != nil { - let initData = ReferendumVotingInitData( - votesResult: nil, - blockNumber: blockNumber, - blockTime: blockTime, - referendum: referendum, - lockDiff: nil - ) + let addAccountAskMessage = R.string.localizable.commonChainCrowdloanAccountMissingMessage( + chain.name, + preferredLanguages: selectedLocale.rLanguages + ) - wireframe.showVote(from: view, referendum: referendum, initData: initData) - } else if accountManagementFilter.canAddAccount(to: wallet, chain: chain) { - let message = R.string.localizable.commonChainCrowdloanAccountMissingMessage( - chain.name, - preferredLanguages: selectedLocale.rLanguages - ) + let handlers = createAccountValidationHandlers() - wireframe.presentAddAccount( - from: view, - chainName: chain.name, - message: message, - locale: selectedLocale - ) { [weak self] in - guard let wallet = self?.wallet else { - return - } + let params = WalletNoAccountHandlingParams( + wallet: wallet, + chain: chain, + accountManagementFilter: accountManagementFilter, + successHandler: handlers.success, + newAccountHandler: handlers.newAccount, + addAccountAskMessage: addAccountAskMessage + ) - self?.wireframe.showWalletDetails(from: self?.view, wallet: wallet) - } - } else { - wireframe.presentNoAccountSupport( - from: view, - walletType: wallet.type, - chainName: chain.name, - locale: selectedLocale - ) - } + validateAccount( + from: params, + view: view, + wireframe: wireframe, + locale: selectedLocale + ) } func showProposerDetails() { diff --git a/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsProtocols.swift b/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsProtocols.swift index 4304ce3248..af8a4367c9 100644 --- a/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsProtocols.swift +++ b/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsProtocols.swift @@ -12,7 +12,7 @@ protocol ReferendumDetailsViewProtocol: ControllerBackedProtocol { func didReceive(shouldHideFullDetails: Bool) } -protocol ReferendumDetailsPresenterProtocol: AnyObject { +protocol ReferendumDetailsPresenterProtocol: AnyObject, WalletNoAccountHandling { func setup() func showProposerDetails() func readFullDescription() diff --git a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift index 1506356278..c0b5032d07 100644 --- a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift +++ b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter+ReferendumSections.swift @@ -41,11 +41,7 @@ extension ReferendumsPresenter { } func createSwipeGovSection() -> ReferendumsSection? { - guard - supportsSwipeGov == true, - let chain, - selectedMetaAccount.fetch(for: chain.accountRequest()) != nil - else { + guard supportsSwipeGov == true else { return nil } diff --git a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift index 3cea622fa9..4c34650784 100644 --- a/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift +++ b/novawallet/Modules/Vote/Governance/Referendums/Presenter/ReferendumsPresenter.swift @@ -15,6 +15,7 @@ final class ReferendumsPresenter { let assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol let sorting: ReferendumsSorting let selectedMetaAccount: MetaAccountModel + let accountManagementFilter: AccountManagementFilterProtocol let logger: LoggerProtocol private(set) lazy var chainBalanceFactory = ChainBalanceViewModelFactory() @@ -75,6 +76,7 @@ final class ReferendumsPresenter { statusViewModelFactory: ReferendumStatusViewModelFactoryProtocol, assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol, selectedMetaAccount: MetaAccountModel, + accountManagementFilter: AccountManagementFilterProtocol, sorting: ReferendumsSorting, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol @@ -88,6 +90,7 @@ final class ReferendumsPresenter { self.statusViewModelFactory = statusViewModelFactory self.assetBalanceFormatterFactory = assetBalanceFormatterFactory self.selectedMetaAccount = selectedMetaAccount + self.accountManagementFilter = accountManagementFilter self.sorting = sorting self.logger = logger self.localizationManager = localizationManager @@ -227,8 +230,53 @@ extension ReferendumsPresenter: ReferendumsPresenterProtocol { } } + private func createAccountValidationHandlers() -> ( + success: () -> Void, + newAccount: () -> Void + ) { + let successHandler: () -> Void = { [weak self] in + guard let self, let view else { return } + + wireframe.showSwipeGov(from: view) + } + + let newAccountHandler: () -> Void = { [weak self] in + guard let self else { return } + + wireframe.showWalletDetails( + from: view, + wallet: selectedMetaAccount + ) + } + + return (successHandler, newAccountHandler) + } + func selectSwipeGov() { - wireframe.showSwipeGov(from: view) + guard let chain, let view else { return } + + let addAccountAskMessage = R.string.localizable.commonChainCrowdloanAccountMissingMessage( + chain.name, + preferredLanguages: selectedLocale.rLanguages + ) + + let handlers = createAccountValidationHandlers() + + let params = WalletNoAccountHandlingParams( + wallet: selectedMetaAccount, + chain: chain, + accountManagementFilter: accountManagementFilter, + successHandler: handlers.success, + newAccountHandler: handlers.newAccount, + addAccountAskMessage: addAccountAskMessage + ) + + validateAccount( + from: params, + view: view, + wireframe: wireframe, + locale: selectedLocale + ) } func showReferendumDetailsIfNeeded() { diff --git a/novawallet/Modules/Vote/Governance/Referendums/ReferendumsProtocols.swift b/novawallet/Modules/Vote/Governance/Referendums/ReferendumsProtocols.swift index e0a6851409..f0e0de33c0 100644 --- a/novawallet/Modules/Vote/Governance/Referendums/ReferendumsProtocols.swift +++ b/novawallet/Modules/Vote/Governance/Referendums/ReferendumsProtocols.swift @@ -9,7 +9,7 @@ protocol ReferendumsViewProtocol: ControllerBackedProtocol { func updateReferendums(time: [ReferendumIdLocal: StatusTimeViewModel?]) } -protocol ReferendumsPresenterProtocol: AnyObject { +protocol ReferendumsPresenterProtocol: AnyObject, WalletNoAccountHandling { func select(referendumIndex: ReferendumIdLocal) func selectUnlocks() func selectDelegations() @@ -46,7 +46,7 @@ protocol ReferendumsInteractorOutputProtocol: AnyObject { func didReceiveError(_ error: ReferendumsInteractorError) } -protocol ReferendumsWireframeProtocol: AlertPresentable, ErrorPresentable, CommonRetryable { +protocol ReferendumsWireframeProtocol: WalletNoAccountHandlingWireframe, ErrorPresentable, CommonRetryable { func selectChain( from view: ControllerBackedProtocol?, delegate: GovernanceAssetSelectionDelegate, @@ -73,4 +73,9 @@ protocol ReferendumsWireframeProtocol: AlertPresentable, ErrorPresentable, Commo referendumsState: Observable, delegate: ReferendumSearchDelegate? ) + + func showWalletDetails( + from view: ControllerBackedProtocol?, + wallet: MetaAccountModel + ) } diff --git a/novawallet/Modules/Vote/Governance/Referendums/ReferendumsWireframe.swift b/novawallet/Modules/Vote/Governance/Referendums/ReferendumsWireframe.swift index 0457692e5e..63c4bf6a45 100644 --- a/novawallet/Modules/Vote/Governance/Referendums/ReferendumsWireframe.swift +++ b/novawallet/Modules/Vote/Governance/Referendums/ReferendumsWireframe.swift @@ -121,4 +121,15 @@ final class ReferendumsWireframe: ReferendumsWireframeProtocol { view?.controller.present(searchView.controller, animated: true, completion: nil) } + + func showWalletDetails( + from view: ControllerBackedProtocol?, + wallet: MetaAccountModel + ) { + guard let accountManagementView = AccountManagementViewFactory.createView(for: wallet.identifier) else { + return + } + + view?.controller.navigationController?.pushViewController(accountManagementView.controller, animated: true) + } } diff --git a/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift b/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift index f6c1ed0d85..0b0d3b2f10 100644 --- a/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift +++ b/novawallet/Modules/Vote/Parent/VoteChildPresenterFactory.swift @@ -215,6 +215,7 @@ extension VoteChildPresenterFactory: VoteChildPresenterFactoryProtocol { statusViewModelFactory: statusViewModelFactory, assetBalanceFormatterFactory: assetBalanceFormatterFactory, selectedMetaAccount: wallet, + accountManagementFilter: AccountManagementFilter(), sorting: ReferendumsTimeSortingProvider(), localizationManager: localizationManager, logger: logger From d0185a10597fc9bd99c6bcef41ab37e6e4c3ee03 Mon Sep 17 00:00:00 2001 From: leohar Date: Fri, 11 Oct 2024 17:08:36 +0100 Subject: [PATCH 16/19] add localisation --- novawallet/es.lproj/Localizable.strings | 4 ++-- novawallet/fr.lproj/Localizable.strings | 4 ++-- novawallet/id.lproj/Localizable.strings | 4 ++-- novawallet/it.lproj/Localizable.strings | 4 ++-- novawallet/ja.lproj/Localizable.strings | 4 ++-- novawallet/ko.lproj/Localizable.strings | 6 +++--- novawallet/pl.lproj/Localizable.strings | 4 ++-- novawallet/pt-PT.lproj/Localizable.strings | 4 ++-- novawallet/ru.lproj/Localizable.strings | 4 ++-- novawallet/tr.lproj/Localizable.strings | 4 ++-- novawallet/vi.lproj/Localizable.strings | 4 ++-- novawallet/zh-Hans.lproj/Localizable.strings | 4 ++-- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/novawallet/es.lproj/Localizable.strings b/novawallet/es.lproj/Localizable.strings index 3d8f5645a7..2b8954c6b9 100644 --- a/novawallet/es.lproj/Localizable.strings +++ b/novawallet/es.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "esperando la próxima era (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Soporte & Retroalimentación"; -"settings.email" = "Email"; +"settings.email" = "Obtenga ayuda por Email"; "wallet.manage.assets.title" = "Gestionar activos"; "staking.reward.payouts.title_v2_2_0" = "Recompensas sin pagar"; "staking.setup.restake.title_v2_2_0" = "Reinvertir recompensas"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Intercambiar"; "common.action.repeat.operation" = "Repetir la operación"; "swaps.error.rate.was.updated.message" = "Antes: %@.\nAhora: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki y centro de ayuda"; "common.not.enough.fee.message_v3.8.0" = "No tienes suficiente saldo para pagar la tarifa de red de %@.\nSaldo disponible para pagar tarifa después de la operación: %@"; "deeplink.error.invalid.chainId.message" = "Cadena no encontrada"; "deeplink.error.no.governance.type.message" = "Tipo de gobernanza no especificado"; diff --git a/novawallet/fr.lproj/Localizable.strings b/novawallet/fr.lproj/Localizable.strings index 7a46b0e24f..8994ffb5ac 100644 --- a/novawallet/fr.lproj/Localizable.strings +++ b/novawallet/fr.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "en attente de la prochaine ère (%@)"; "settings.youtube" = "YouTube"; "settings.support" = "Assistance et commentaires"; -"settings.email" = "E-mail"; +"settings.email" = "Obtenez de l'aide par Email"; "wallet.manage.assets.title" = "Gérer les actifs"; "staking.reward.payouts.title_v2_2_0" = "Récompenses impayées"; "staking.setup.restake.title_v2_2_0" = "Récompenses réengagées"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Échanger"; "common.action.repeat.operation" = "Répétez l'opération"; "swaps.error.rate.was.updated.message" = "Taux ancien : %@.\nNouveau taux : %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki et centre d'aide"; "common.not.enough.fee.message_v3.8.0" = "Vous n'avez pas assez de solde pour payer les frais de réseau de %@.\nSolde disponible pour payer les frais après l'opération : %@"; "deeplink.error.invalid.chainId.message" = "Chaîne non trouvée"; "deeplink.error.no.governance.type.message" = "Le type de gouvernance n'est pas spécifié"; diff --git a/novawallet/id.lproj/Localizable.strings b/novawallet/id.lproj/Localizable.strings index 2d8da75712..1cee143c80 100644 --- a/novawallet/id.lproj/Localizable.strings +++ b/novawallet/id.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "menunggu era selanjutnya (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Dukungan & Masukan"; -"settings.email" = "Email"; +"settings.email" = "Dapatkan dukungan melalui Email"; "wallet.manage.assets.title" = "Kelola aset"; "staking.reward.payouts.title_v2_2_0" = "Imbalan yang belum dibayar"; "staking.setup.restake.title_v2_2_0" = "Imbalan ulang staking"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Tukar"; "common.action.repeat.operation" = "Ulangi operasi"; "swaps.error.rate.was.updated.message" = "Nilai lama: %@.\nNilai baru: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki & Pusat Bantuan"; "common.not.enough.fee.message_v3.8.0" = "Saldo Anda tidak cukup untuk membayar biaya jaringan sebesar %@.\nSaldo yang tersedia untuk membayar biaya setelah operasi: %@"; "deeplink.error.invalid.chainId.message" = "Rantai tidak ditemukan"; "deeplink.error.no.governance.type.message" = "Tipe tata kelola tidak ditentukan"; diff --git a/novawallet/it.lproj/Localizable.strings b/novawallet/it.lproj/Localizable.strings index b12d20845c..6eaf593a48 100644 --- a/novawallet/it.lproj/Localizable.strings +++ b/novawallet/it.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "in attesa della prossima era (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Supporto e Feedback"; -"settings.email" = "Email"; +"settings.email" = "Ottieni supporto via Email"; "wallet.manage.assets.title" = "Gestione asset"; "staking.reward.payouts.title_v2_2_0" = "Ricompense non pagate"; "staking.setup.restake.title_v2_2_0" = "Ri-staking ricompense"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Scambia"; "common.action.repeat.operation" = "Ripeti l'operazione"; "swaps.error.rate.was.updated.message" = "Vecchio tasso: %@.\nNuovo tasso: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki e Centro assistenza"; "common.not.enough.fee.message_v3.8.0" = "Non hai abbastanza saldo per pagare la commissione di rete di %@.\nSaldo disponibile per pagare la commissione dopo l'operazione: %@"; "deeplink.error.invalid.chainId.message" = "Identificativo della rete non trovato"; "deeplink.error.no.governance.type.message" = "Il tipo di governance non è specificato"; diff --git a/novawallet/ja.lproj/Localizable.strings b/novawallet/ja.lproj/Localizable.strings index a014cd5130..bb2e6be614 100644 --- a/novawallet/ja.lproj/Localizable.strings +++ b/novawallet/ja.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "次の時代を待っています (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "サポート&フィードバック"; -"settings.email" = "Eメール"; +"settings.email" = "メールでサポートを受ける"; "wallet.manage.assets.title" = "資産を管理"; "staking.reward.payouts.title_v2_2_0" = "未払いの報酬"; "staking.setup.restake.title_v2_2_0" = "報酬を再ステークする"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "スワップ"; "common.action.repeat.operation" = "操作を繰り返す"; "swaps.error.rate.was.updated.message" = "以前のレート: %@。\n新しいレート: %@"; -"settings.wiki" = "ウィキ"; +"settings.wiki" = "ウィキとヘルプセンター"; "common.not.enough.fee.message_v3.8.0" = "ネットワーク手数料の支払に必要な残高が不足しています: %@。\n操作後に手数料支払いに利用可能な残高: %@"; "deeplink.error.invalid.chainId.message" = "チェーンが見つかりません"; "deeplink.error.no.governance.type.message" = "ガバナンスタイプが指定されていません"; diff --git a/novawallet/ko.lproj/Localizable.strings b/novawallet/ko.lproj/Localizable.strings index dd45eba008..beb5525bc2 100644 --- a/novawallet/ko.lproj/Localizable.strings +++ b/novawallet/ko.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "다음 시대를 기다리는 중 (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "지원 및 피드백"; -"settings.email" = "이메일"; +"settings.email" = "이메일을 통해 지원 받기"; "wallet.manage.assets.title" = "자산 관리"; "staking.reward.payouts.title_v2_2_0" = "지급되지 않은 보상"; "staking.setup.restake.title_v2_2_0" = "보상 재Stake"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "교환"; "common.action.repeat.operation" = "작업 반복"; "swaps.error.rate.was.updated.message" = "이전 비율: %@.\n새로운 비율: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "위키 및 도움말 센터"; "common.not.enough.fee.message_v3.8.0" = "네트워크 수수료 %@를 지불할 잔액이 부족합니다.\n작업 후 수수료를 지불할 수 있는 잔액: %@"; "deeplink.error.invalid.chainId.message" = "체인을 찾을 수 없음"; "deeplink.error.no.governance.type.message" = "거버넌스 유형이 지정되지 않음"; @@ -1609,7 +1609,7 @@ "cloud.backup.not.available.title" = "iCloud 인증 실패"; "cloud.backup.not.available.message" = "iCloud 계정에 로그인했는지 확인하십시오"; "cloud.backup.not.enough.storage.message" = "사용할 수 있는 iCloud 저장 공간이 충분한지 확인하십시오. 설정에서 저장 공간을 관리할 수 있습니다."; -"ledger.instructions.step4.highlighted" = "계정을 선택"; +"ledger.instructions.step4.highlighted" = "계정을 선택"; "cloud.backup.create.title" = "백업 비밀번호 생성"; "cloud.backup.create.hint.min.char" = "최소 %@ 문자"; "cloud.backup.create.hint.numbers" = "숫자"; diff --git a/novawallet/pl.lproj/Localizable.strings b/novawallet/pl.lproj/Localizable.strings index 5d31ec0069..e67bde3be8 100644 --- a/novawallet/pl.lproj/Localizable.strings +++ b/novawallet/pl.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "oczekiwanie na następną erę (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Wsparcie i opinie"; -"settings.email" = "Email"; +"settings.email" = "Uzyskaj wsparcie przez Email"; "wallet.manage.assets.title" = "Zarządzaj aktywami"; "staking.reward.payouts.title_v2_2_0" = "Niewypłacone nagrody"; "staking.setup.restake.title_v2_2_0" = "Ponowne stakingowanie nagród"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Swap"; "common.action.repeat.operation" = "Powtórz operację"; "swaps.error.rate.was.updated.message" = "Stara stawka: %@.\nNowa stawka: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki i Centrum Pomocy"; "common.not.enough.fee.message_v3.8.0" = "Nie masz wystarczającego salda, aby zapłacić opłatę sieciową w wysokości %@.\nDostępne saldo po operacji: %@"; "deeplink.error.invalid.chainId.message" = "Sieć nie została znaleziona"; "deeplink.error.no.governance.type.message" = "Typ zarządzania nie jest określony"; diff --git a/novawallet/pt-PT.lproj/Localizable.strings b/novawallet/pt-PT.lproj/Localizable.strings index a3fd4e8b63..27018f28b5 100644 --- a/novawallet/pt-PT.lproj/Localizable.strings +++ b/novawallet/pt-PT.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "aguardando a próxima era (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Suporte & Feedback"; -"settings.email" = "Escrever para os desenvolvedores"; +"settings.email" = "Obtenha suporte via Email"; "wallet.manage.assets.title" = "Gerenciar ativos"; "staking.reward.payouts.title_v2_2_0" = "Recompensas não pagas"; "staking.setup.restake.title_v2_2_0" = "Reapostar recompensas"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Trocar"; "common.action.repeat.operation" = "Repetir a operação"; "swaps.error.rate.was.updated.message" = "Antiga: %@.\nNova: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki e Central de Ajuda"; "common.not.enough.fee.message_v3.8.0" = "Você não tem saldo suficiente para pagar a taxa de rede de %@.\nSaldo disponível para pagar a taxa após a operação: %@"; "deeplink.error.invalid.chainId.message" = "Cadeia não encontrada"; "deeplink.error.no.governance.type.message" = "Tipo de governança não especificado"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 5d64cbc0c8..48932feafc 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "ожидание следующей эры (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Поддержка и обратная связь"; -"settings.email" = "Получить поддержку по Email"; +"settings.email" = "Получите поддержку по Email"; "wallet.manage.assets.title" = "Управление ассетами"; "staking.reward.payouts.title_v2_2_0" = "Невыплаченные награды"; "staking.setup.restake.title_v2_2_0" = "Увеличивать стейк"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Обмен"; "common.action.repeat.operation" = "Повторить операцию"; "swaps.error.rate.was.updated.message" = "Было: %@.\nСтало: %@"; -"settings.wiki" = "Руководство и Центр Помощи"; +"settings.wiki" = "Вики и справочный центр"; "common.not.enough.fee.message_v3.8.0" = "У вас недостаточно средств для оплаты комиссии сети в размере %@.\nДоступный баланс для оплаты комиссии после операции: %@"; "deeplink.error.invalid.chainId.message" = "Сеть не найдена"; "deeplink.error.no.governance.type.message" = "Не указан тип управления"; diff --git a/novawallet/tr.lproj/Localizable.strings b/novawallet/tr.lproj/Localizable.strings index 9c1ebcee54..8dc5aa55a2 100644 --- a/novawallet/tr.lproj/Localizable.strings +++ b/novawallet/tr.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "bir sonraki era için bekleniyor (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Destek & Geribildirim"; -"settings.email" = "Email"; +"settings.email" = "Email yoluyla destek alın"; "wallet.manage.assets.title" = "Varlıkları Yönet"; "staking.reward.payouts.title_v2_2_0" = "Ödenmemiş ödüller"; "staking.setup.restake.title_v2_2_0" = "Ödülleri Yeniden Stake Et"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Takas"; "common.action.repeat.operation" = "İşlemi tekrarla"; "swaps.error.rate.was.updated.message" = "Eski oran: %@.\nYeni oran: %@"; -"settings.wiki" = "Kullanıcı Kılavuzu"; +"settings.wiki" = "Wiki & Yardım Merkezi"; "common.not.enough.fee.message_v3.8.0" = "Ağ ücretini ödemek için yeterli bakiyeniz yok. İşlem sonrası ücreti ödemek için kullanılabilir bakiye: %@"; "deeplink.error.invalid.chainId.message" = "Zincir bulunamadı"; "deeplink.error.no.governance.type.message" = "Yönetim türü belirtilmemiş"; diff --git a/novawallet/vi.lproj/Localizable.strings b/novawallet/vi.lproj/Localizable.strings index 5e92683a03..23f54816e3 100644 --- a/novawallet/vi.lproj/Localizable.strings +++ b/novawallet/vi.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "đang chờ đợi kỷ nguyên tiếp theo (%@)"; "settings.youtube" = "Youtube"; "settings.support" = "Hỗ trợ & Phản hồi"; -"settings.email" = "Email"; +"settings.email" = "Nhận hỗ trợ qua Email"; "wallet.manage.assets.title" = "Quản lý tài sản"; "staking.reward.payouts.title_v2_2_0" = "Phần thưởng chưa trả"; "staking.setup.restake.title_v2_2_0" = "Restake phần thưởng"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "Đổi"; "common.action.repeat.operation" = "Lặp lại thao tác"; "swaps.error.rate.was.updated.message" = "Tỷ lệ cũ: %@.\nTỷ lệ mới: %@"; -"settings.wiki" = "Wiki"; +"settings.wiki" = "Wiki & Trung tâm trợ giúp"; "common.not.enough.fee.message_v3.8.0" = "Bạn không có đủ số dư để trả phí mạng là %@.\nSố dư khả dụng để trả phí sau khi thực hiện: %@"; "deeplink.error.invalid.chainId.message" = "Không tìm thấy chuỗi"; "deeplink.error.no.governance.type.message" = "Loại quản lý không được chỉ định"; diff --git a/novawallet/zh-Hans.lproj/Localizable.strings b/novawallet/zh-Hans.lproj/Localizable.strings index 3d87a0c1f9..76eb6c8efd 100644 --- a/novawallet/zh-Hans.lproj/Localizable.strings +++ b/novawallet/zh-Hans.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.waiting.next.era.format" = "等待下一个时代(%@)"; "settings.youtube" = "Youtube"; "settings.support" = "支持和反馈"; -"settings.email" = "电子邮件"; +"settings.email" = "通过电子邮件获得支持"; "wallet.manage.assets.title" = "资产管理"; "staking.reward.payouts.title_v2_2_0" = "未支付奖励"; "staking.setup.restake.title_v2_2_0" = "增加质押奖励"; @@ -1419,7 +1419,7 @@ "common.swap.title" = "交换"; "common.action.repeat.operation" = "重复操作"; "swaps.error.rate.was.updated.message" = "旧汇率:%@。\n新汇率:%@"; -"settings.wiki" = "用户指南"; +"settings.wiki" = "维基百科和帮助中心"; "common.not.enough.fee.message_v3.8.0" = "您的余额不足以支付%@的网络费用。\n操作后可用于支付费用的余额:%@"; "deeplink.error.invalid.chainId.message" = "未找到链"; "deeplink.error.no.governance.type.message" = "未指定治理类型"; From a71b888f64e84aac5cc243b355f8ec0809bcc564 Mon Sep 17 00:00:00 2001 From: leohar Date: Mon, 14 Oct 2024 15:58:44 +0100 Subject: [PATCH 17/19] fix wrong strings --- novawallet/fr.lproj/Localizable.strings | 18 +++++++++--------- novawallet/ko.lproj/Localizable.strings | 16 ++++++++-------- novawallet/pl.lproj/Localizable.strings | 2 +- novawallet/ru.lproj/Localizable.strings | 2 +- novawallet/vi.lproj/Localizable.strings | 6 +++--- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/novawallet/fr.lproj/Localizable.strings b/novawallet/fr.lproj/Localizable.strings index 8994ffb5ac..0905ee2f49 100644 --- a/novawallet/fr.lproj/Localizable.strings +++ b/novawallet/fr.lproj/Localizable.strings @@ -908,9 +908,9 @@ "ledger.instructions.title" = "Connecter Ledger Nano X"; "ledger.instructions.link" = "Guide officiel de connexion Bluetooth de Ledger"; "ledger.instructions.step1" = "Assurez-vous que %@ sur votre appareil Ledger à l'aide de l'application Ledger Live"; -"ledger.instructions.step1.highlighted" = "L'application réseau est installée"; +"ledger.instructions.step1.highlighted" = "L'application réseau est installée"; "ledger.instructions.step2" = "%@ sur votre appareil Ledger"; -"ledger.instructions.step2.highlighted" = "Ouvrez l'application réseau"; +"ledger.instructions.step2.highlighted" = "Ouvrez l'application réseau"; "ledger.instructions.step3" = "Autorisez Nova Wallet à %@"; "ledger.instructions.step3.highlighted" = "accès Bluetooth"; "hardware.wallet.import.description" = "Utiliser Polkadot Vault, Ledger ou Parity Signer"; @@ -1323,12 +1323,12 @@ "common.recommended" = "Recommandé"; "staking.setup.amount.direct.type.subtitle" = "Sélectionné : %li (max %li)"; "staking.is.not.available.title" = "%@ est actuellement indisponible"; -"staking.locked.pool.violation.error" = "Vous avez des tokens verrouillés sur votre solde en raison de %@. Pour continuer, vous devez entrer moins de %@ ou plus de %@. Pour stake un autre montant, vous devez supprimer vos verrous %@."; -"staking.locked.pool.violation.title" = "Vous ne pouvez pas stake le montant spécifié"; +"staking.locked.pool.violation.error" = "Vous avez des tokens verrouillés sur votre solde en raison de %@. Pour continuer, vous devez entrer moins de %@ ou plus de %@. Pour stake un autre montant, vous devez supprimer vos verrous %@."; +"staking.locked.pool.violation.title" = "Vous ne pouvez pas stake le montant spécifié"; "staking.pool.has.no.apy.message" = "La piscine que vous avez sélectionnée est inactive en raison de l'absence de validateurs sélectionnés ou de son stake étant inférieur au minimum.\nÊtes-vous sûr de vouloir continuer avec la piscine sélectionnée?"; "staking.pool.ed.error.message" = "Votre solde disponible est de %@, vous devez laisser %@ comme solde minimal et payer les frais de réseau de %@. Vous pouvez staker pas plus de %@."; "staking.maximum.action" = "Stake max"; -"staking.pool.rewards.bond.more.pool.unbonding.error.message" = "Vous retirez tous vos tokens et ne pouvez pas en stake davantage."; +"staking.pool.rewards.bond.more.pool.unbonding.error.message" = "Vous retirez tous vos tokens et ne pouvez pas en stake davantage."; "staking.pool.rewards.bond.more.pool.unbonding.error.title" = "Impossible de stake plus"; "staking.start.already.have.any.staking" = "Vous avez déjà un staking dans %@"; "staking.pool.network.info" = "Infos sur le Stake en pool"; @@ -1369,7 +1369,7 @@ "common.alert.external.link.disclaimer.title" = "Continuer dans le navigateur?"; "common.alert.external.link.disclaimer.message" = "Pour poursuivre l'achat, vous serez redirigé de l'application Nova Wallet vers %@"; "polkadot.staking.promotion.title" = "Boostez vos DOT 🚀"; -"polkadot.staking.promotion.message" = "Vous avez récupéré vos DOT des crowdloans ? Commencez à staker vos DOT aujourd'hui pour obtenir les récompenses maximales !"; +"polkadot.staking.promotion.message" = "Vous avez récupéré vos DOT des crowdloans ? Commencez à staker vos DOT aujourd'hui pour obtenir les récompenses maximales !"; "swaps.violating.consumers.message" = "Vous devez conserver au moins %@ après avoir payé %@ de frais de réseau car vous détenez des tokens insuffisants"; "common.receive.not.sufficient.native.asset.error" = "Vous devez conserver au moins %@ pour recevoir des tokens %@"; "swaps.rate.description" = "Taux de change entre deux crypto-monnaies différentes. Il représente combien de crypto-monnaie vous pouvez obtenir en échange d'un certain montant d'une autre crypto-monnaie."; @@ -1573,7 +1573,7 @@ "common.backup.manual" = "Sauvegarder manuellement"; "cloud.backup.broken.title" = "Sauvegarde trouvée mais vide ou corrompue"; "cloud.backup.broken.message" = "Un problème a été identifié avec votre sauvegarde. Vous avez la possibilité de supprimer la sauvegarde actuelle et d'en créer une nouvelle. %@ avant de continuer."; -"cloud.backup.broken.highlighted" = "Assurez-vous d'avoir sauvegardé les phrases secrètes pour tous les portefeuilles"; +"cloud.backup.broken.highlighted" = "Assurez-vous d'avoir sauvegardé les phrases secrètes pour tous les portefeuilles"; "common.recover.wallets" = "Récupérer les portefeuilles"; "cloud.backup.existing.bottom.sheet.title" = "Sauvegarde Cloud existante trouvée"; "cloud.backup.existing.bottom.sheet.recover" = "Voulez-vous récupérer vos portefeuilles ?"; @@ -1588,7 +1588,7 @@ "cloud.backup.import.subtitle" = "Veuillez entrer le mot de passe que vous avez créé lors du processus de sauvegarde"; "common.got.it" = "Compris"; "cloud.backup.create.bottom.sheet.title" = "Souvenez-vous du mot de passe de sauvegarde"; -"cloud.backup.create.bottom.sheet.password" = "Ce mot de passe ne peut pas être récupéré."; +"cloud.backup.create.bottom.sheet.password" = "Ce mot de passe ne peut pas être récupéré."; "common.backup.password" = "Mot de passe de sauvegarde"; "cloud.backup.create.hint.letters" = "Lettres"; "cloud.backup.create.hint.password.match" = "Les mots de passe correspondent"; @@ -1744,7 +1744,7 @@ "gov.vote.setup.details.swipe.gov" = "Le vote sera configuré pour les futurs votes dans SwipeGov"; "voting.list.widget.title" = "Confirmer les votes"; "voting.list.widget.title.empty" = "Aucun vote"; -"vote.card.requested" = "Demandé :"; +"vote.card.requested" = "Demandé :"; "swipe.gov.empty.view.text" = "Vous avez déjà voté pour tous les référendums disponibles ou il n'y a pas de référendums pour voter en ce moment. Revenez plus tard."; "common.swipe.gov" = "SwipeGov"; "common.counted.referenda" = "%li référendums"; diff --git a/novawallet/ko.lproj/Localizable.strings b/novawallet/ko.lproj/Localizable.strings index beb5525bc2..b2f727f395 100644 --- a/novawallet/ko.lproj/Localizable.strings +++ b/novawallet/ko.lproj/Localizable.strings @@ -350,7 +350,7 @@ "crowdloan.cap.reached.title" = "크라우드론 상한 초과"; "crowdloan.too.small.contribution.message" = "기여할 수 있는 최소 금액은 %@입니다."; "crowdloan.too.small.contribution.title" = "기여 금액이 너무 작습니다"; -"crowdloan.learn" = "크라우드론 %@ 배우기"; +"crowdloan.learn" = "크라우드론 %@ 배우기"; "common.time.left" = "남은 시간"; "crowdloan.placeholder" = "활성 크라우드론은 여기에 표시됩니다"; "crowdloan.contribute.title" = "크라우드론에 기여"; @@ -619,7 +619,7 @@ "staking.network.info.title" = "스테이킹 정보"; "staking.network.info.staking.period.value" = "무제한"; "staking.network.info.staking.period.title" = "Stake 기간"; -"crowdloan.learn_v2_2_0" = "%@의 크라우드론 웹사이트"; +"crowdloan.learn_v2_2_0" = "%@의 크라우드론 웹사이트"; "common.learn.more_v2_2_0" = "더 알아보기"; "staking.reward.widget.title" = "보상"; "staking.waiting.next.era.format" = "다음 시대를 기다리는 중 (%@)"; @@ -908,11 +908,11 @@ "ledger.instructions.title" = "Ledger Nano X 연결"; "ledger.instructions.link" = "Ledger의 공식 Bluetooth 연결 가이드"; "ledger.instructions.step1" = "Ledger Live 앱을 사용하여 Ledger 기기에 %@ 있는지 확인하십시오"; -"ledger.instructions.step1.highlighted" = "네트워크 앱이 설치되었습니다"; +"ledger.instructions.step1.highlighted" = "네트워크 앱이 설치되었습니다"; "ledger.instructions.step2" = "Ledger 기기에서 %@"; -"ledger.instructions.step2.highlighted" = "네트워크 앱을 여세요"; +"ledger.instructions.step2.highlighted" = "네트워크 앱을 여세요"; "ledger.instructions.step3" = "Nova Wallet에 %@을(를) 허용하십시오"; -"ledger.instructions.step3.highlighted" = "Bluetooth 접근"; +"ledger.instructions.step3.highlighted" = "Bluetooth 접근"; "hardware.wallet.import.description" = "Polkadot Vault, Parity Signer 또는 Ledger 사용"; "ledger.sign.transaction.details" = "거래를 승인하려면 %@에서 두 버튼을 모두 누르세요"; "common.transaction.expired" = "거래가 만료되었습니다"; @@ -1609,7 +1609,7 @@ "cloud.backup.not.available.title" = "iCloud 인증 실패"; "cloud.backup.not.available.message" = "iCloud 계정에 로그인했는지 확인하십시오"; "cloud.backup.not.enough.storage.message" = "사용할 수 있는 iCloud 저장 공간이 충분한지 확인하십시오. 설정에서 저장 공간을 관리할 수 있습니다."; -"ledger.instructions.step4.highlighted" = "계정을 선택"; +"ledger.instructions.step4.highlighted" = "계정을 선택"; "cloud.backup.create.title" = "백업 비밀번호 생성"; "cloud.backup.create.hint.min.char" = "최소 %@ 문자"; "cloud.backup.create.hint.numbers" = "숫자"; @@ -1631,7 +1631,7 @@ "backup.attention.aggree.button.title" = "검토 및 수락하여 계속"; "mnemonic.card.cover.message.message" = "아무도 화면을 볼 수 없도록 하고 스크린샷을 찍지 마세요"; "mnemonic.card.revealed.header.message" = "아무에게도 %@ 하지 마세요"; -"mnemonic.card.revealed.header.message.highlighted" = "공유하지 마세요"; +"mnemonic.card.revealed.header.message.highlighted" = "공유하지 마세요"; "chain.accounts.list.default.subtitle" = "+%li 다른"; "cloud.backup.review.subtitle" = "변경 사항을 진행하기 전에, 수정되고 제거된 지갑을 위해 %@ 하십시오!"; "cloud.backup.review.ensure.passphrase" = "암호 구문을 저장했는지 확인하십시오"; @@ -1727,7 +1727,7 @@ "gov.voters.abstain" = "기권 투표"; "gov.vote.conviction.alert.title" = "확신도 업데이트"; "gov.vote.conviction.alert.message" = "기권 투표는 0.1x 확신도로만 가능합니다. 0.1x 확신도로 투표하시겠습니까?"; -"gov.vote.conviction.hint.title" = "기권 시 확신도가 0.1x로 설정됩니다"; +"gov.vote.conviction.hint.title" = "기권 시 확신도가 0.1x로 설정됩니다"; "cloud.backup.password.confirm.title" = "백업 비밀번호 확인"; "cloud.backup.password.confirm.details" = "이전 화면에서 생성한 비밀번호를 입력하세요"; "swipe.gov.empty.view.voted.text" = "모든 가능한 국민투표에 이미 투표했습니다"; diff --git a/novawallet/pl.lproj/Localizable.strings b/novawallet/pl.lproj/Localizable.strings index e67bde3be8..9327a99557 100644 --- a/novawallet/pl.lproj/Localizable.strings +++ b/novawallet/pl.lproj/Localizable.strings @@ -1573,7 +1573,7 @@ "common.backup.manual" = "Utwórz ręczną kopię zapasową"; "cloud.backup.broken.title" = "Znaleziono kopię zapasową, ale jest pusta lub uszkodzona"; "cloud.backup.broken.message" = "Wykryto problem z twoją kopią zapasową. Masz możliwość usunięcia bieżącej kopii zapasowej i utworzenia nowej. %@ przed kontynuowaniem."; -"cloud.backup.broken.highlighted" = "Upewnij się, że zapisałeś Passphrases dla wszystkich portfeli"; +"cloud.backup.broken.highlighted" = "Upewnij się, że zapisałeś Passphrases dla wszystkich portfeli"; "common.recover.wallets" = "Odzyskaj portfele"; "cloud.backup.existing.bottom.sheet.title" = "Znaleziono istniejącą kopię zapasową w chmurze"; "cloud.backup.existing.bottom.sheet.recover" = "Czy chcesz odzyskać swoje portfele?"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 48932feafc..f435cff577 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1756,4 +1756,4 @@ "swipe.gov.referenda.excluded.alert.message" = "Некоторые референдумы больше недоступны для голосования или у вас может быть недостаточно токенов для голосования. Доступно для голосования: %@."; "gov.referendum.completed.title.with.index" = "Референдум #%li завершен"; "gov.referendum.completed.message.with.index" = "Референдум №%li завершен, голосование окончено"; -"common.use.max.due.fee.message" = "Вы можете использовать до %@, так как вам нужно заплатить %@ комиссию сети."; +"common.use.max.due.fee.message" = "Вы можете использовать до %@, так как вам нужно заплатить %@ комиссию сети."; \ No newline at end of file diff --git a/novawallet/vi.lproj/Localizable.strings b/novawallet/vi.lproj/Localizable.strings index 23f54816e3..43e7da194e 100644 --- a/novawallet/vi.lproj/Localizable.strings +++ b/novawallet/vi.lproj/Localizable.strings @@ -908,9 +908,9 @@ "ledger.instructions.title" = "Kết nối Ledger Nano X"; "ledger.instructions.link" = "Hướng dẫn kết nối Bluetooth chính thức của Ledger"; "ledger.instructions.step1" = "Đảm bảo %@ vào thiết bị Ledger của bạn bằng ứng dụng Ledger Live"; -"ledger.instructions.step1.highlighted" = "Ứng dụng mạng đã được cài đặt"; +"ledger.instructions.step1.highlighted" = "Ứng dụng mạng đã được cài đặt"; "ledger.instructions.step2" = "%@ trên thiết bị Ledger của bạn"; -"ledger.instructions.step2.highlighted" = "Mở ứng dụng mạng"; +"ledger.instructions.step2.highlighted" = "Mở ứng dụng mạng"; "ledger.instructions.step3" = "Cho phép Nova Wallet %@"; "ledger.instructions.step3.highlighted" = "truy cập Bluetooth"; "hardware.wallet.import.description" = "Polkadot Vault, Parity Signer hoặc Ledger"; @@ -1727,7 +1727,7 @@ "gov.voters.abstain" = "Bỏ phiếu Abstain"; "gov.vote.conviction.alert.title" = "Cập nhật độ xác tín"; "gov.vote.conviction.alert.message" = "Bỏ phiếu Abstain chỉ có thể thực hiện với độ xác tín 0.1x. Bỏ phiếu với độ xác tín 0.1x?"; -"gov.vote.conviction.hint.title" = "Độ xác tín sẽ được đặt là 0.1x khi bỏ phiếu Abstain"; +"gov.vote.conviction.hint.title" = "Độ xác tín sẽ được đặt là 0.1x khi bỏ phiếu Abstain"; "cloud.backup.password.confirm.title" = "Xác nhận mật khẩu sao lưu"; "cloud.backup.password.confirm.details" = "Vui lòng nhập mật khẩu bạn đã tạo ở màn hình trước"; "swipe.gov.empty.view.voted.text" = "Bạn đã bỏ phiếu cho tất cả các cuộc trưng cầu khả dụng"; From 2971fb6fe323e16313569e49315de65bf04fe6e1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Oct 2024 17:54:46 +0200 Subject: [PATCH 18/19] add backup validation --- .../CloudBackupServiceFactory.swift | 1 + .../Model/CloudBackupSecretsExporting.swift | 12 ++++- .../Model/CloudBackupValidating.swift | 54 +++++++++++++++++-- .../CloudBackup/CloudBackupSyncTests.swift | 54 +++++++++++++++++++ .../MockCloudBackupOperationFactory.swift | 2 +- .../MockCloudBackupServiceFactory.swift | 1 + 6 files changed, 116 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/Services/CloudBackup/CloudBackupServiceFactory.swift b/novawallet/Common/Services/CloudBackup/CloudBackupServiceFactory.swift index c74f0c35b6..8a5cb345ae 100644 --- a/novawallet/Common/Services/CloudBackup/CloudBackupServiceFactory.swift +++ b/novawallet/Common/Services/CloudBackup/CloudBackupServiceFactory.swift @@ -56,6 +56,7 @@ extension ICloudBackupServiceFactory: CloudBackupServiceFactoryProtocol { CloudBackupSecretsExporter( walletConverter: CloudBackupFileModelConverter(), cryptoManager: createCryptoManager(), + validator: ICloudBackupValidator(), keychain: keychain ) } diff --git a/novawallet/Common/Services/CloudBackup/Model/CloudBackupSecretsExporting.swift b/novawallet/Common/Services/CloudBackup/Model/CloudBackupSecretsExporting.swift index 2b41d7ebf4..290aa32c04 100644 --- a/novawallet/Common/Services/CloudBackup/Model/CloudBackupSecretsExporting.swift +++ b/novawallet/Common/Services/CloudBackup/Model/CloudBackupSecretsExporting.swift @@ -15,20 +15,24 @@ enum CloudBackupSecretsExporterError: Error { case unsupportedWallet(MetaAccountModelType) case invalidSecret(UInt8) case brokenSecrets(MetaAccountModel.Id) + case validationFailed } final class CloudBackupSecretsExporter { let walletConverter: CloudBackupFileModelConverting let cryptoManager: CloudBackupCryptoManagerProtocol let keychain: KeystoreProtocol + let validator: CloudBackupValidating init( walletConverter: CloudBackupFileModelConverting, cryptoManager: CloudBackupCryptoManagerProtocol, + validator: CloudBackupValidating, keychain: KeystoreProtocol ) { self.walletConverter = walletConverter self.cryptoManager = cryptoManager + self.validator = validator self.keychain = keychain } @@ -333,12 +337,16 @@ extension CloudBackupSecretsExporter: CloudBackupSecretsExporting { try createPrivateInfo(from: wallet) } + let publicData = CloudBackup.PublicData(modifiedAt: modifiedAt, wallets: publicWalletsData) let privateInfo = CloudBackup.DecryptedFileModel.PrivateData(wallets: Set(privateInfoList)) + + guard validator.validate(publicData: publicData, matches: privateInfo) else { + throw CloudBackupSecretsExporterError.validationFailed + } + let encodedPrivateInfo = try JSONEncoder().encode(privateInfo) let encryptedInfo = try cryptoManager.encrypt(data: encodedPrivateInfo, password: password) - let publicData = CloudBackup.PublicData(modifiedAt: modifiedAt, wallets: publicWalletsData) - return .init(publicData: publicData, privateData: encryptedInfo.toHex()) } } diff --git a/novawallet/Common/Services/CloudBackup/Model/CloudBackupValidating.swift b/novawallet/Common/Services/CloudBackup/Model/CloudBackupValidating.swift index a130321073..8a87cd08be 100644 --- a/novawallet/Common/Services/CloudBackup/Model/CloudBackupValidating.swift +++ b/novawallet/Common/Services/CloudBackup/Model/CloudBackupValidating.swift @@ -7,14 +7,58 @@ protocol CloudBackupValidating { ) -> Bool } -final class ICloudBackupValidator {} +final class ICloudBackupValidator { + private func validateSecrets( + privateInfo: CloudBackup.DecryptedFileModel.WalletPrivateInfo + ) -> Bool { + privateInfo.substrate?.keypair != nil || + privateInfo.ethereum?.keypair != nil || + privateInfo.chainAccounts.contains { $0.keypair != nil } + } + + private func validateLedger(privateInfo: CloudBackup.DecryptedFileModel.WalletPrivateInfo) -> Bool { + privateInfo.chainAccounts.contains { $0.derivationPath != nil } + } + + private func validateGenericLedger(privateInfo: CloudBackup.DecryptedFileModel.WalletPrivateInfo) -> Bool { + privateInfo.substrate?.derivationPath != nil + } +} extension ICloudBackupValidator: CloudBackupValidating { func validate( - publicData _: CloudBackup.PublicData, - matches _: CloudBackup.DecryptedFileModel.PrivateData + publicData: CloudBackup.PublicData, + matches: CloudBackup.DecryptedFileModel.PrivateData ) -> Bool { - // TODO: Implement validation - true + let privateDataDict = matches.wallets.reduce( + into: [MetaAccountModel.Id: CloudBackup.DecryptedFileModel.WalletPrivateInfo]() + ) { + $0[$1.walletId] = $1 + } + + return publicData.wallets.allSatisfy { wallet in + switch wallet.type { + case .secrets: + guard let privateInfo = privateDataDict[wallet.walletId] else { + return false + } + + return validateSecrets(privateInfo: privateInfo) + case .ledger: + guard let privateInfo = privateDataDict[wallet.walletId] else { + return false + } + + return validateLedger(privateInfo: privateInfo) + case .genericLedger: + guard let privateInfo = privateDataDict[wallet.walletId] else { + return false + } + + return validateGenericLedger(privateInfo: privateInfo) + case .watchOnly, .paritySigner, .polkadotVault: + return true + } + } } } diff --git a/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift b/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift index bcbd695813..09228b8323 100644 --- a/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift +++ b/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift @@ -94,6 +94,45 @@ final class CloudBackupSyncTests: XCTestCase { XCTAssertTrue(try setupResult.syncMetadataManager.hasPassword()) } + func testCantApplyChangesIfSecretWalletHasNoSecrets() throws { + let logger = Logger.shared + let setupResult = setupSyncSevice( + configuringLocal: { params in + try? AccountCreationHelper.createMetaAccountFromMnemonic( + cryptoType: .sr25519, + keychain: params.keystore, + settings: params.walletSettings + ) + + try? KeystoreValidationHelper.clearKeystore( + for: params.walletSettings.value, + keystore: params.keystore + ) + }, + configuringBackup: { params in + params.syncMetadataManager.isBackupEnabled = true + try? params.syncMetadataManager.savePassword(Self.defaultPassword) + params.syncMetadataManager.saveLastSyncTimestamp(nil) + } + ) + + let syncChanges: CloudBackupSyncResult.Changes? = syncAndWait(service: setupResult.syncService) { result in + switch result { + case let .changes(changes): + return changes + default: + logger.debug("Skipped: \(result)") + return nil + } + } + + XCTAssertNotNil(syncChanges) + + let issue = applyChangesAndDetectIssue(for: setupResult.syncService) + + XCTAssertEqual(issue, .internalFailure) + } + func testDetectLocalChanges() throws { try performSyncTest( configuringLocal: { params in @@ -723,6 +762,21 @@ final class CloudBackupSyncTests: XCTestCase { } } + private func applyChangesAndDetectIssue( + for syncService: CloudBackupSyncServiceProtocol + ) -> CloudBackupSyncResult.Issue? { + syncService.applyChanges(notifyingIn: .global(), closure: {_ in }) + + return syncAndWait(service: syncService) { result in + switch result { + case let .issue(issue): + return issue + default: + return nil + } + } + } + private func setupSyncSevice( configuringLocal: LocalWalletsSetupClosure, configuringBackup: BackupSetupClosure diff --git a/novawalletTests/Common/Services/CloudBackup/MockCloudBackupOperationFactory.swift b/novawalletTests/Common/Services/CloudBackup/MockCloudBackupOperationFactory.swift index 37fad4b841..85bab85277 100644 --- a/novawalletTests/Common/Services/CloudBackup/MockCloudBackupOperationFactory.swift +++ b/novawalletTests/Common/Services/CloudBackup/MockCloudBackupOperationFactory.swift @@ -23,7 +23,7 @@ extension MockCloudBackupOperationFactory: CloudBackupOperationFactoryProtocol { dataClosure: @escaping () throws -> Data ) -> BaseOperation { ClosureOperation { - self.data = try? dataClosure() + self.data = try dataClosure() } } diff --git a/novawalletTests/Common/Services/CloudBackup/MockCloudBackupServiceFactory.swift b/novawalletTests/Common/Services/CloudBackup/MockCloudBackupServiceFactory.swift index c89162d369..abe686af83 100644 --- a/novawalletTests/Common/Services/CloudBackup/MockCloudBackupServiceFactory.swift +++ b/novawalletTests/Common/Services/CloudBackup/MockCloudBackupServiceFactory.swift @@ -52,6 +52,7 @@ extension MockCloudBackupServiceFactory: CloudBackupServiceFactoryProtocol { CloudBackupSecretsExporter( walletConverter: CloudBackupFileModelConverter(), cryptoManager: createCryptoManager(), + validator: ICloudBackupValidator(), keychain: keychain ) } From d7c0953a738fcb86a36b8fdbdecb822d5333ba60 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Oct 2024 04:41:52 +0200 Subject: [PATCH 19/19] add more tests --- .../CloudBackup/CloudBackupSyncTests.swift | 109 +++++++++++++----- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift b/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift index 09228b8323..d47c0e6a2d 100644 --- a/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift +++ b/novawalletTests/Common/Services/CloudBackup/CloudBackupSyncTests.swift @@ -94,43 +94,62 @@ final class CloudBackupSyncTests: XCTestCase { XCTAssertTrue(try setupResult.syncMetadataManager.hasPassword()) } - func testCantApplyChangesIfSecretWalletHasNoSecrets() throws { - let logger = Logger.shared - let setupResult = setupSyncSevice( - configuringLocal: { params in - try? AccountCreationHelper.createMetaAccountFromMnemonic( - cryptoType: .sr25519, - keychain: params.keystore, - settings: params.walletSettings - ) - - try? KeystoreValidationHelper.clearKeystore( - for: params.walletSettings.value, - keystore: params.keystore - ) - }, - configuringBackup: { params in - params.syncMetadataManager.isBackupEnabled = true - try? params.syncMetadataManager.savePassword(Self.defaultPassword) - params.syncMetadataManager.saveLastSyncTimestamp(nil) - } - ) + func testPreventApplyChangesIfSecretWalletHasNoSecrets() throws { + let optIssue = performDetectIssueTest { params in + // create a wallet with secrets + try? AccountCreationHelper.createMetaAccountFromMnemonic( + cryptoType: .sr25519, + keychain: params.keystore, + settings: params.walletSettings + ) + + // and then remove secrets before syncing + try? KeystoreValidationHelper.clearKeystore( + for: params.walletSettings.value, + keystore: params.keystore + ) + } - let syncChanges: CloudBackupSyncResult.Changes? = syncAndWait(service: setupResult.syncService) { result in - switch result { - case let .changes(changes): - return changes - default: - logger.debug("Skipped: \(result)") - return nil + XCTAssertEqual(optIssue, .internalFailure) + } + + func testPreventApplyChangesIfLegacyLedgerWalletHasNoDPath() throws { + let optIssue = performDetectIssueTest { params in + guard let ledgerApp = SupportedLedgerApp.substrate().first else { + return } + + try? AccountCreationHelper.createSubstrateLedgerAccount( + from: ledgerApp, + keychain: params.keystore, + settings: params.walletSettings + ) + + // and then remove secrets before syncing + try? KeystoreValidationHelper.clearKeystore( + for: params.walletSettings.value, + keystore: params.keystore + ) } - XCTAssertNotNil(syncChanges) - - let issue = applyChangesAndDetectIssue(for: setupResult.syncService) + XCTAssertEqual(optIssue, .internalFailure) + } + + func testPreventApplyChangesIfGenericLedgerWalletHasNoDPath() throws { + let optIssue = performDetectIssueTest { params in + try? AccountCreationHelper.createSubstrateGenericLedgerWallet( + keychain: params.keystore, + settings: params.walletSettings + ) + + // and then remove secrets before syncing + try? KeystoreValidationHelper.clearKeystore( + for: params.walletSettings.value, + keystore: params.keystore + ) + } - XCTAssertEqual(issue, .internalFailure) + XCTAssertEqual(optIssue, .internalFailure) } func testDetectLocalChanges() throws { @@ -666,6 +685,32 @@ final class CloudBackupSyncTests: XCTestCase { ) } + private func performDetectIssueTest( + configuringLocal: LocalWalletsSetupClosure + ) -> CloudBackupSyncResult.Issue? { + let setupResult = setupSyncSevice( + configuringLocal: configuringLocal, + configuringBackup: { params in + params.syncMetadataManager.isBackupEnabled = true + try? params.syncMetadataManager.savePassword(Self.defaultPassword) + params.syncMetadataManager.saveLastSyncTimestamp(nil) + } + ) + + let syncChanges: CloudBackupSyncResult.Changes? = syncAndWait(service: setupResult.syncService) { result in + switch result { + case let .changes(changes): + return changes + default: + return nil + } + } + + XCTAssertNotNil(syncChanges) + + return applyChangesAndDetectIssue(for: setupResult.syncService) + } + private func performSyncTest( configuringLocal: LocalWalletsSetupClosure, changingAfterBackup: LocalWalletsChangeClosure,