From 30f24bc9d83ada9b5ae6a2499ad89952b9d6ead6 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Thu, 13 Jun 2024 17:19:37 +0600 Subject: [PATCH 1/2] Change TonKit for new api-kit --- .../project.pbxproj | 31 ++- .../Core/Adapters/TonAdapter.swift | 236 ++++++++++-------- .../Core/Address/TonAddressParserItem.swift | 6 +- .../UnstoppableWallet/Core/Protocols.swift | 2 +- .../Core/Providers/AppConfig.swift | 4 + .../UnstoppableWallet/Extensions/Kmm.swift | 234 ++++++++--------- .../UnstoppableWallet/Info.plist | 2 + .../Models/AccountType.swift | 2 +- .../Ton/TonIncomingTransactionRecord.swift | 22 +- .../Ton/TonOutgoingTransactionRecord.swift | 25 +- .../Ton/TonTransactionRecord.swift | 18 +- .../Send/Platforms/Ton/SendTonService.swift | 96 ++++--- .../TransactionInfoViewItemFactory.swift | 6 +- .../TransactionsViewItemFactory.swift | 4 +- .../Modules/Watch/WatchViewController.swift | 5 + 15 files changed, 392 insertions(+), 301 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 0ddbcc00f9..82f21b8d40 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -1912,6 +1912,8 @@ 6BCD531D2A16203F00993F20 /* CloudBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD531B2A16203F00993F20 /* CloudBackupManager.swift */; }; 6BDA29AB29D6F37C003847ED /* ECashKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AA29D6F37C003847ED /* ECashKit */; }; 6BDA29AD29D6F384003847ED /* ECashKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AC29D6F384003847ED /* ECashKit */; }; + 6BDDB59C2C1816B500DE9D56 /* TonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDDB59B2C1816B500DE9D56 /* TonKit */; }; + 6BDDB5A12C1AC73D00DE9D56 /* TonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDDB5A02C1AC73D00DE9D56 /* TonKit */; }; 6BE8A07B2ADE2F8D0012DE7F /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE8A07A2ADE2F8D0012DE7F /* Currency.swift */; }; 6BE8A07C2ADE2F8D0012DE7F /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE8A07A2ADE2F8D0012DE7F /* Currency.swift */; }; 6BE8A07E2ADE2F950012DE7F /* CurrencyValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE8A07D2ADE2F950012DE7F /* CurrencyValue.swift */; }; @@ -2989,9 +2991,6 @@ D3BA259F2ADFAF23002B13EA /* HsToolKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3BA25982ADFAF23002B13EA /* HsToolKit */; }; D3BA25A02ADFAF23002B13EA /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3948F092ADA887300FAE566 /* Intents.framework */; }; D3BA25A72ADFB042002B13EA /* IntentExtension Prod.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D3BA25A52ADFAF23002B13EA /* IntentExtension Prod.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - D3BC25812B0B5E1E0092F682 /* TonKitKmm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D3BC25832B0B5E460092F682 /* TonKitKmm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; }; - D3BC25842B0B5E460092F682 /* TonKitKmm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D3BF1E5C274BC29D00229A00 /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = D3BF1E5B274BC29D00229A00 /* Down */; }; D3BF1E5E274CAF1C00229A00 /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = D3BF1E5D274CAF1C00229A00 /* Down */; }; D3BF1E63274CBBCE00229A00 /* DeepDiff in Frameworks */ = {isa = PBXBuildFile; productRef = D3BF1E62274CBBCE00229A00 /* DeepDiff */; }; @@ -3153,7 +3152,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D3BC25812B0B5E1E0092F682 /* TonKitKmm.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -3164,7 +3162,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D3BC25842B0B5E460092F682 /* TonKitKmm.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -4654,7 +4651,6 @@ D3B73E2F2BDFC5580067429D /* PriceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceRow.swift; sourceTree = ""; }; D3BA25912ADFAD7C002B13EA /* WidgetExtension Prod.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "WidgetExtension Prod.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D3BA25A52ADFAF23002B13EA /* IntentExtension Prod.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "IntentExtension Prod.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = TonKitKmm.xcframework; sourceTree = ""; }; D3C5986D2C1315AF00789D69 /* AttributedStringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringView.swift; sourceTree = ""; }; D3C598702C16CDB700789D69 /* CoinAnalyticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsView.swift; sourceTree = ""; }; D3C598732C16CDC400789D69 /* CoinAnalyticsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsViewModel.swift; sourceTree = ""; }; @@ -4716,6 +4712,7 @@ D3AF5A9329FFD87500C1399E /* RxSwift in Frameworks */, D3604E5728F02B280066C366 /* NftKit in Frameworks */, D3C187E0290FD00E00FE1900 /* ThemeKit in Frameworks */, + 6BDDB59C2C1816B500DE9D56 /* TonKit in Frameworks */, D0DA7413272A6F180072BE86 /* UnicodeURL in Frameworks */, D38405B8218317DF007D50AD /* LocalAuthentication.framework in Frameworks */, D3BF1E5E274CAF1C00229A00 /* Down in Frameworks */, @@ -4758,13 +4755,13 @@ D3604E6928F02DF30066C366 /* BitcoinCashKit in Frameworks */, D3993DC228F42992008720FB /* UnstoppableDomainsResolution in Frameworks */, D3604E8528F03CDC0066C366 /* BinanceChainKit in Frameworks */, - D3BC25832B0B5E460092F682 /* TonKitKmm.xcframework in Frameworks */, D3C187BA2907CFAB00FE1900 /* Checkpoints in Frameworks */, 6BDA29AD29D6F384003847ED /* ECashKit in Frameworks */, D3604E4D28F02AB40066C366 /* NftKit in Frameworks */, D3C187CF290FCF2D00FE1900 /* ThemeKit in Frameworks */, D0DA740F272A6EFC0072BE86 /* UnicodeURL in Frameworks */, D38406A721831B3D007D50AD /* LocalAuthentication.framework in Frameworks */, + 6BDDB5A12C1AC73D00DE9D56 /* TonKit in Frameworks */, D3BF1E5C274BC29D00229A00 /* Down in Frameworks */, D3604E4A28F02A8C0066C366 /* Eip20Kit in Frameworks */, D3604E4328F02A020066C366 /* EvmKit in Frameworks */, @@ -7390,7 +7387,6 @@ 95AD93BB0A2D5F5C4A435252 /* Frameworks */ = { isa = PBXGroup; children = ( - D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */, 50AB06AD2158E2EE00A01E4A /* LocalAuthentication.framework */, D3373D9420BEC7B30082BC4A /* AVFoundation.framework */, D3948EF12ADA846400FAE566 /* WidgetKit.framework */, @@ -8857,6 +8853,7 @@ 6B55E33C2AF26D7A00616B60 /* Starscream */, D08C93B02B91E3B400A7D1D5 /* Hodler */, 6BBCE4A22BDA419200ABBD55 /* Web3Wallet */, + 6BDDB59B2C1816B500DE9D56 /* TonKit */, ); productName = Wallet; productReference = D38405CE218317DF007D50AD /* Unstoppable D.app */; @@ -8919,6 +8916,7 @@ 6B55E33A2AF26D6400616B60 /* Starscream */, D08C93AE2B91E39E00A7D1D5 /* Hodler */, 6BBCE4A42BDA419B00ABBD55 /* Web3Wallet */, + 6BDDB5A02C1AC73D00DE9D56 /* TonKit */, ); productName = Wallet; productReference = D38406BE21831B3D007D50AD /* Unstoppable.app */; @@ -9089,6 +9087,7 @@ 6B55E3392AF26D6400616B60 /* XCRemoteSwiftPackageReference "Starscream" */, D08C93AD2B91E37E00A7D1D5 /* XCRemoteSwiftPackageReference "Hodler" */, 6BF66DD82BA1A73300963242 /* XCRemoteSwiftPackageReference "ObjectMapper" */, + 6BDDB59F2C1AC73200DE9D56 /* XCRemoteSwiftPackageReference "TonKit" */, ); productRefGroup = D3285F4320BD158E00644076 /* Products */; projectDirPath = ""; @@ -12866,6 +12865,14 @@ version = 2.0.5; }; }; + 6BDDB59F2C1AC73200DE9D56 /* XCRemoteSwiftPackageReference "TonKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/horizontalsystems/TonKit.Swift"; + requirement = { + kind = exactVersion; + version = 0.1.0; + }; + }; 6BF66DD82BA1A73300963242 /* XCRemoteSwiftPackageReference "ObjectMapper" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/tristanhimmelman/ObjectMapper/"; @@ -13197,6 +13204,14 @@ package = 6BDA29A929D6EA9B003847ED /* XCRemoteSwiftPackageReference "ECashKit.Swift" */; productName = ECashKit; }; + 6BDDB59B2C1816B500DE9D56 /* TonKit */ = { + isa = XCSwiftPackageProductDependency; + productName = TonKit; + }; + 6BDDB5A02C1AC73D00DE9D56 /* TonKit */ = { + isa = XCSwiftPackageProductDependency; + productName = TonKit; + }; D023D2622A249E59004F65B0 /* TronKit */ = { isa = XCSwiftPackageProductDependency; package = D023D2612A249E59004F65B0 /* XCRemoteSwiftPackageReference "TronKit.Swift" */; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift index 3a31d58517..4b4d6ca179 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift @@ -4,13 +4,17 @@ import HdWalletKit import HsToolKit import MarketKit import RxSwift -import TonKitKmm +import TonKit +import TweetNacl +import TonSwift +import BigInt class TonAdapter { private static let coinRate: Decimal = 1_000_000_000 + static let bounceableDefault = false - private let tonKit: TonKit - private let ownAddress: String + private let tonKit: TonKit.Kit + private let ownAddress: TonSwift.Address private let transactionSource: TransactionSource private let baseToken: Token private let reachabilityManager = App.shared.reachabilityManager @@ -19,10 +23,10 @@ class TonAdapter { private var adapterStarted = false private var kitStarted = false - private let balanceStateSubject = PublishSubject() - private(set) var balanceState: AdapterState { + private let adapterStateSubject = PublishSubject() + private(set) var adapterState: AdapterState { didSet { - balanceStateSubject.onNext(balanceState) + adapterStateSubject.onNext(adapterState) } } @@ -33,13 +37,6 @@ class TonAdapter { } } - private let transactionsStateSubject = PublishSubject() - private(set) var transactionsState: AdapterState { - didSet { - transactionsStateSubject.onNext(transactionsState) - } - } - private let transactionRecordsSubject = PublishSubject<[TonTransactionRecord]>() init(wallet: Wallet, baseToken: Token) throws { @@ -51,51 +48,53 @@ class TonAdapter { guard let seed = wallet.account.type.mnemonicSeed else { throw AdapterError.unsupportedAccount } - + let hdWallet = HDWallet(seed: seed, coinType: 607, xPrivKey: 0, curve: .ed25519) let privateKey = try hdWallet.privateKey(account: 0) + let privateRaw = Data(privateKey.raw.bytes) + let pair = try TweetNacl.NaclSign.KeyPair.keyPair(fromSeed: privateRaw) + let keyPair = KeyPair(publicKey: .init(data: pair.publicKey), + privateKey: .init(data: pair.secretKey)) + + tonKit = try Kit.instance( + type: .full(keyPair), + network: .mainNet, + walletId: wallet.account.id, + apiKey: nil, + minLogLevel: .debug + ) - tonKit = TonKitFactory(driverFactory: DriverFactory(), connectionManager: ConnectionManager()).create(seed: privateKey.raw.toKotlinByteArray(), walletId: wallet.account.id) case let .tonAddress(address): - tonKit = TonKitFactory(driverFactory: DriverFactory(), connectionManager: ConnectionManager()).createWatch(address: address, walletId: wallet.account.id) + let tonAddress = try TonSwift.Address.parse(address) + tonKit = try Kit.instance( + type: .watch(tonAddress), + network: .mainNet, + walletId: wallet.account.id, + apiKey: nil, + minLogLevel: .debug + ) default: throw AdapterError.unsupportedAccount } - ownAddress = tonKit.receiveAddress + ownAddress = tonKit.address - balanceState = Self.adapterState(kitSyncState: tonKit.balanceSyncState) + + adapterState = Self.adapterState(kitSyncState: tonKit.syncState) balanceData = BalanceData(available: Self.amount(kitAmount: tonKit.balance)) - transactionsState = Self.adapterState(kitSyncState: tonKit.transactionsSyncState) - collect(tonKit.balanceSyncStatePublisher) - .completeOnFailure() + tonKit.syncStatePublisher .sink { [weak self] syncState in - self?.balanceState = Self.adapterState(kitSyncState: syncState) + self?.adapterState = Self.adapterState(kitSyncState: syncState) } .store(in: &cancellables) - collect(tonKit.balancePublisher) - .completeOnFailure() + tonKit.tonBalancePublisher .sink { [weak self] balance in self?.balanceData = BalanceData(available: Self.amount(kitAmount: balance)) } .store(in: &cancellables) - collect(tonKit.transactionsSyncStatePublisher) - .completeOnFailure() - .sink { [weak self] syncState in - self?.transactionsState = Self.adapterState(kitSyncState: syncState) - } - .store(in: &cancellables) - - collect(tonKit.doNewTransactionsPublisher) - .completeOnFailure() - .sink { [weak self] tonTransactions in - self?.handle(tonTransactions: tonTransactions) - } - .store(in: &cancellables) - reachabilityManager.$isReachable .sink { [weak self] isReachable in self?.handle(isReachable: isReachable) @@ -115,72 +114,71 @@ class TonAdapter { } } - private func handle(tonTransactions: [TonTransactionWithTransfers]) { + private func handle(tonTransactions: [TonKit.FullTransaction]) { let transactionRecords = tonTransactions.map { transactionRecord(tonTransaction: $0) } transactionRecordsSubject.onNext(transactionRecords) } - private static func adapterState(kitSyncState: AnyObject) -> AdapterState { + private static func adapterState(kitSyncState: TonKit.SyncState) -> AdapterState { switch kitSyncState { - case is TonKitKmm.SyncState.Syncing: return .syncing(progress: nil, lastBlockDate: nil) - case is TonKitKmm.SyncState.Synced: return .synced - case let notSyncedState as TonKitKmm.SyncState.NotSynced: return .notSynced(error: notSyncedState.error) - default: return .notSynced(error: AppError.unknownError) + case .syncing: return .syncing(progress: nil, lastBlockDate: nil) + case .synced: return .synced + case let .notSynced(error): return .notSynced(error: error) } } static func amount(kitAmount: String) -> Decimal { - guard let decimal = Decimal(string: kitAmount) else { - return 0 - } + Decimal(string: kitAmount).map { amount(kitAmount: $0) } ?? 0 + } + + static func amount(kitAmount: BigUInt) -> Decimal { + amount(kitAmount: kitAmount.toDecimal(decimals: 0) ?? 0) + } - return decimal / coinRate + static func amount(kitAmount: Decimal) -> Decimal { + return kitAmount / coinRate } - private func transactionRecord(tonTransaction tx: TonTransactionWithTransfers) -> TonTransactionRecord { - switch tx.type { - case TransactionType.incoming: + private func transactionRecord(tonTransaction tx: TonKit.FullTransaction) -> TonTransactionRecord { + switch tx.decoration { + case is TonKit.IncomingDecoration: return TonIncomingTransactionRecord( - source: transactionSource, - transaction: tx, + source: .init(blockchainType: .ton, meta: nil), + event: tx.event, feeToken: baseToken, token: baseToken ) - case TransactionType.outgoing: + + case let decoration as TonKit.OutgoingDecoration: return TonOutgoingTransactionRecord( - source: transactionSource, - transaction: tx, + source: .init(blockchainType: .ton, meta: nil), + event: tx.event, feeToken: baseToken, - token: baseToken + token: baseToken, + sentToSelf: decoration.sentToSelf ) + default: - return TonTransactionRecord( - source: transactionSource, - transaction: tx, - feeToken: baseToken - ) + return TonTransactionRecord( + source: .init(blockchainType: .ton, meta: nil), + event: tx.event, + feeToken: baseToken + ) } } + + private func tagQuery(token: MarketKit.Token?, filter: TransactionTypeFilter, address: String?) -> TransactionTagQuery { + var type: TransactionTag.TagType? - private func transactionsSingle(from: TransactionRecord?, type: TransactionType?, address: String?, limit: Int) -> Single<[TransactionRecord]> { - let single: Single<[TonTransactionWithTransfers]> = Single.create { [tonKit] observer in - let task = Task { [tonKit] in - do { - let tonTransactions = try await tonKit.transactions(fromTransactionHash: from?.transactionHash, type: type, address: address, limit: Int64(limit)) - observer(.success(tonTransactions)) - } catch { - observer(.error(error)) - } - } - - return Disposables.create { - task.cancel() - } + switch filter { + case .all: () + case .incoming: type = .incoming + case .outgoing: type = .outgoing + case .swap: type = .swap + case .approve: type = .approve } - return single.map { [weak self] tonTransactions -> [TransactionRecord] in - tonTransactions.compactMap { self?.transactionRecord(tonTransaction: $0) } - } + return TransactionTagQuery(type: type, protocol: .native, jettonAddress: nil, address: address) } private func startKit() { @@ -217,7 +215,9 @@ extension TonAdapter: IAdapter { } } - func refresh() {} + func refresh() { + tonKit.refresh() + } var statusInfo: [(String, Any)] { [] @@ -230,27 +230,31 @@ extension TonAdapter: IAdapter { extension TonAdapter: IBalanceAdapter { var balanceStateUpdatedObservable: Observable { - balanceStateSubject.asObservable() + adapterStateSubject } var balanceDataUpdatedObservable: Observable { balanceDataSubject.asObservable() } + + var balanceState: AdapterState { + adapterState + } } extension TonAdapter: IDepositAdapter { var receiveAddress: DepositAddress { - DepositAddress(tonKit.receiveAddress) + DepositAddress(tonKit.receiveAddress.toString(bounceable: TonAdapter.bounceableDefault)) } } extension TonAdapter: ITransactionsAdapter { var syncing: Bool { - transactionsState.syncing + adapterState.syncing } var syncingObservable: Observable { - transactionsStateSubject.map { _ in () } + adapterStateSubject.map { _ in () } } var lastBlockInfo: LastBlockInfo? { @@ -273,27 +277,42 @@ extension TonAdapter: ITransactionsAdapter { "https://tonscan.org/tx/\(transactionHash)" } - func transactionsObservable(token _: Token?, filter: TransactionTypeFilter, address _: String?) -> Observable<[TransactionRecord]> { - transactionRecordsSubject - .map { transactionRecords in - transactionRecords.compactMap { transaction -> TransactionRecord? in - switch (transaction, filter) { - case (_, .all): return transaction - case (is TonIncomingTransactionRecord, .incoming): return transaction - case (is TonOutgoingTransactionRecord, .outgoing): return transaction - default: return nil - } - } + func transactionsObservable(token: Token?, filter: TransactionTypeFilter, address: String?) -> Observable<[TransactionRecord]> { + let address = address.flatMap { try? FriendlyAddress(string: $0) }?.address.toRaw() + + return tonKit.transactionsPublisher(tagQueries: [tagQuery(token: token, filter: filter, address: address)]).asObservable() + .map { [weak self] in + $0.compactMap { self?.transactionRecord(tonTransaction: $0) } } - .filter { !$0.isEmpty } } func transactionsSingle(from: TransactionRecord?, token _: Token?, filter: TransactionTypeFilter, address: String?, limit: Int) -> Single<[TransactionRecord]> { - switch filter { - case .all: return transactionsSingle(from: from, type: nil, address: address, limit: limit) - case .incoming: return transactionsSingle(from: from, type: TransactionType.incoming, address: address, limit: limit) - case .outgoing: return transactionsSingle(from: from, type: TransactionType.outgoing, address: address, limit: limit) - default: return Single.just([]) + Single.create { [weak self] observer in + guard let self else { + observer(.error(AppError.unknownError)) + return Disposables.create() + } + + Task { [weak self] in + let address = address.flatMap { try? FriendlyAddress(string: $0) }?.address.toRaw() + + let beforeLt = (from as? TonTransactionRecord).map { $0.lt } + var tagQueries = [TransactionTagQuery]() + switch filter { + case .all: () + case .incoming: tagQueries.append(.init(type: .incoming, address: address)) + case .outgoing: tagQueries.append(.init(type: .outgoing, address: address)) + default: observer(.success([])) + } + + let txs = (self?.tonKit + .transactions(tagQueries: tagQueries, beforeLt: beforeLt, limit: limit) + .compactMap { self?.transactionRecord(tonTransaction: $0) }) ?? [] + + observer(.success(txs)) + } + + return Disposables.create() } } @@ -308,16 +327,19 @@ extension TonAdapter: ISendTonAdapter { } func validate(address: String) throws { - try TonKit.companion.validate(address: address) + _ = try FriendlyAddress(string: address) } - func estimateFee() async throws -> Decimal { - let kitAmount = try await tonKit.estimateFee() + func estimateFee(recipient: String, amount: Decimal, memo: String?) async throws -> Decimal { + let amount = (amount * Self.coinRate).rounded(decimal: 0) + + let kitAmount = try await tonKit.estimateFee(recipient: recipient, amount: amount, comment: memo) return Self.amount(kitAmount: kitAmount) } - + func send(recipient: String, amount: Decimal, memo: String?) async throws { - let rawAmount = amount * Self.coinRate - try await tonKit.send(recipient: recipient, amount: rawAmount.rounded(decimal: 0).description, memo: memo) + let amount = (amount * Self.coinRate).rounded(decimal: 0) + + try await tonKit.send(recipient: recipient, amount: amount, comment: memo) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/TonAddressParserItem.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/TonAddressParserItem.swift index a94e753d9c..4c57bf355b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Address/TonAddressParserItem.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/TonAddressParserItem.swift @@ -1,13 +1,13 @@ import MarketKit import RxSwift -import TonKitKmm +import TonKit class TonAddressParserItem: IAddressParserItem { var blockchainType: MarketKit.BlockchainType = .ton func handle(address: String) -> Single
{ do { - try TonKit.companion.validate(address: address) + try TonKit.Kit.validate(address: address) return Single.just(Address(raw: address, blockchainType: blockchainType)) } catch { return Single.error(error) @@ -16,7 +16,7 @@ class TonAddressParserItem: IAddressParserItem { func isValid(address: String) -> Single { do { - try TonKit.companion.validate(address: address) + try TonKit.Kit.validate(address: address) return Single.just(true) } catch { return Single.just(false) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift b/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift index f924fa382c..801f8d65e2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift @@ -103,7 +103,7 @@ protocol ISendTronAdapter { protocol ISendTonAdapter { var availableBalance: Decimal { get } func validate(address: String) throws - func estimateFee() async throws -> Decimal + func estimateFee(recipient: String, amount: Decimal, memo: String?) async throws -> Decimal func send(recipient: String, amount: Decimal, memo: String?) async throws } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift index c56967ecd6..e740df533e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift @@ -137,6 +137,10 @@ enum AppConfig { Bundle.main.object(forInfoDictionaryKey: "DefaultPassphrase") as? String ?? "" } + static var defaultWatchAddress: String? { + Bundle.main.object(forInfoDictionaryKey: "DefaultWatchAddress") as? String + } + static var sharedCloudContainer: String? { Bundle.main.object(forInfoDictionaryKey: "SharedCloudContainerId") as? String } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift index a6b5273ca4..a8e76defc5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift @@ -1,117 +1,117 @@ -import Combine -import TonKitKmm - -typealias OnEach = (Output) -> Void -typealias OnCompletion = (Failure?) -> Void - -typealias OnCollect = (@escaping OnEach, @escaping OnCompletion) -> TonKitKmm.Cancellable - -/** - Creates a `Publisher` that collects output from a flow wrapper function emitting values from an underlying - instance of `Flow`. - */ -func collect(_ onCollect: @escaping OnCollect) -> Publishers.Flow { - Publishers.Flow(onCollect: onCollect) -} - -class SharedCancellableSubscription: Subscription { - private var isCancelled: Bool = false - - var cancellable: TonKitKmm.Cancellable? { - didSet { - if isCancelled { - cancellable?.cancel() - } - } - } - - func request(_: Subscribers.Demand) { - // Not supported - } - - func cancel() { - isCancelled = true - cancellable?.cancel() - } -} - -extension Publishers { - class Flow: Publisher { - private let onCollect: OnCollect - - init(onCollect: @escaping OnCollect) { - self.onCollect = onCollect - } - - func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { - let subscription = SharedCancellableSubscription() - subscriber.receive(subscription: subscription) - - let cancellable = onCollect({ input in _ = subscriber.receive(input) }) { failure in - if let failure { - subscriber.receive(completion: .failure(failure)) - } else { - subscriber.receive(completion: .finished) - } - } - - subscription.cancellable = cancellable - } - } -} - -extension KotlinThrowable: Error {} - -enum PublisherFailures { - /** - The action to invoke when a failure is dropped as the result of a `Publisher` returned by - `Publisher.completeOnFailure()`. - */ - static var willCompleteOnFailure: (Error, Callsite) -> Void = { error, callsite in - // if error.isKotlinCancellation { - // return - // } - - print("[ERROR] A publisher failed and was completed due to a call to `completeOnFailure()` \(callsite): \(error)") - } -} - -struct Callsite: CustomStringConvertible { - let file: String - let fileID: String - let filePath: String - let line: Int - let column: Int - let function: String - let dsoHandle: UnsafeRawPointer - - var description: String { - "in \(function) at \(filePath)#\(line):\(column)" - } -} - -extension Publisher { - /** - Ignores errors in the upstream publisher, emitting an empty sequence instead, and otherwise republishes all received input. - You can hook into these failures by assigning a function to `PublisherHooks.willCompleteOnFailure`. - */ - func completeOnFailure(file: String = #file, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column, function: String = #function, dsoHandle: UnsafeRawPointer = #dsohandle) -> Publishers.Catch> { - `catch` { error in - let callsite = Callsite(file: file, fileID: fileID, filePath: filePath, line: line, column: column, function: function, dsoHandle: dsoHandle) - PublisherFailures.willCompleteOnFailure(error, callsite) - return Empty(completeImmediately: true) - } - } -} - -extension Data { - func toKotlinByteArray() -> KotlinByteArray { - let swiftByteArray = [UInt8](self) - return swiftByteArray - .map(Int8.init(bitPattern:)) - .enumerated() - .reduce(into: KotlinByteArray(size: Int32(swiftByteArray.count))) { result, row in - result.set(index: Int32(row.offset), value: row.element) - } - } -} +//import Combine +//import TonKitKmm +// +//typealias OnEach = (Output) -> Void +//typealias OnCompletion = (Failure?) -> Void +// +//typealias OnCollect = (@escaping OnEach, @escaping OnCompletion) -> TonKitKmm.Cancellable +// +///** +// Creates a `Publisher` that collects output from a flow wrapper function emitting values from an underlying +// instance of `Flow`. +// */ +//func collect(_ onCollect: @escaping OnCollect) -> Publishers.Flow { +// Publishers.Flow(onCollect: onCollect) +//} +// +//class SharedCancellableSubscription: Subscription { +// private var isCancelled: Bool = false +// +// var cancellable: TonKitKmm.Cancellable? { +// didSet { +// if isCancelled { +// cancellable?.cancel() +// } +// } +// } +// +// func request(_: Subscribers.Demand) { +// // Not supported +// } +// +// func cancel() { +// isCancelled = true +// cancellable?.cancel() +// } +//} +// +//extension Publishers { +// class Flow: Publisher { +// private let onCollect: OnCollect +// +// init(onCollect: @escaping OnCollect) { +// self.onCollect = onCollect +// } +// +// func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { +// let subscription = SharedCancellableSubscription() +// subscriber.receive(subscription: subscription) +// +// let cancellable = onCollect({ input in _ = subscriber.receive(input) }) { failure in +// if let failure { +// subscriber.receive(completion: .failure(failure)) +// } else { +// subscriber.receive(completion: .finished) +// } +// } +// +// subscription.cancellable = cancellable +// } +// } +//} +// +//extension KotlinThrowable: Error {} +// +//enum PublisherFailures { +// /** +// The action to invoke when a failure is dropped as the result of a `Publisher` returned by +// `Publisher.completeOnFailure()`. +// */ +// static var willCompleteOnFailure: (Error, Callsite) -> Void = { error, callsite in +// // if error.isKotlinCancellation { +// // return +// // } +// +// print("[ERROR] A publisher failed and was completed due to a call to `completeOnFailure()` \(callsite): \(error)") +// } +//} +// +//struct Callsite: CustomStringConvertible { +// let file: String +// let fileID: String +// let filePath: String +// let line: Int +// let column: Int +// let function: String +// let dsoHandle: UnsafeRawPointer +// +// var description: String { +// "in \(function) at \(filePath)#\(line):\(column)" +// } +//} +// +//extension Publisher { +// /** +// Ignores errors in the upstream publisher, emitting an empty sequence instead, and otherwise republishes all received input. +// You can hook into these failures by assigning a function to `PublisherHooks.willCompleteOnFailure`. +// */ +// func completeOnFailure(file: String = #file, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column, function: String = #function, dsoHandle: UnsafeRawPointer = #dsohandle) -> Publishers.Catch> { +// `catch` { error in +// let callsite = Callsite(file: file, fileID: fileID, filePath: filePath, line: line, column: column, function: function, dsoHandle: dsoHandle) +// PublisherFailures.willCompleteOnFailure(error, callsite) +// return Empty(completeImmediately: true) +// } +// } +//} +// +//extension Data { +// func toKotlinByteArray() -> KotlinByteArray { +// let swiftByteArray = [UInt8](self) +// return swiftByteArray +// .map(Int8.init(bitPattern:)) +// .enumerated() +// .reduce(into: KotlinByteArray(size: Int32(swiftByteArray.count))) { result, row in +// result.set(index: Int32(row.offset), value: row.element) +// } +// } +//} diff --git a/UnstoppableWallet/UnstoppableWallet/Info.plist b/UnstoppableWallet/UnstoppableWallet/Info.plist index 5f30b213a3..22c16e7ebf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Info.plist +++ b/UnstoppableWallet/UnstoppableWallet/Info.plist @@ -49,6 +49,8 @@ ${default_passphrase} DefaultWords ${default_words} + DefaultWatchAddress + ${default_watch_address} DefiYieldApiKey $(defiyield_api_key) DonateEnabled diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index 82451b008f..c1bf0be88d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -319,7 +319,7 @@ extension AccountType { case .evmAddress: return (try? EvmKit.Address(hex: string)).map { AccountType.evmAddress(address: $0) } case .tronAddress: - let hexData = Data(hex: string) + let hexData = string.hs.hexData ?? Data() let address: TronKit.Address? if !hexData.isEmpty { // android convention address diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift index a53f69cd54..fedfc11b38 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift @@ -1,19 +1,25 @@ import Foundation import MarketKit -import TonKitKmm +import TonKit +import TonSwift +import BigInt class TonIncomingTransactionRecord: TonTransactionRecord { let transfer: Transfer? - init(source: TransactionSource, transaction: TonTransactionWithTransfers, feeToken: Token, token: Token) { - transfer = transaction.transfers.first.map { transfer in - Transfer( - address: transfer.src.getNonBounceable(), - value: .coinValue(token: token, value: TonAdapter.amount(kitAmount: transfer.amount)) - ) + init(source: TransactionSource, event: AccountEvent, feeToken: Token, token: Token) { + transfer = event + .actions + .compactMap { $0 as? TonTransfer } + .first + .map { transfer in + Transfer( + address: transfer.recipient.address.toString(bounceable: TonAdapter.bounceableDefault), + value: .coinValue(token: token, value: TonAdapter.amount(kitAmount: Decimal(transfer.amount))) + ) } - super.init(source: source, transaction: transaction, feeToken: feeToken) + super.init(source: source, event: event, feeToken: feeToken) } override var mainValue: TransactionValue? { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonOutgoingTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonOutgoingTransactionRecord.swift index 0287e7cfe3..04b2f99918 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonOutgoingTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonOutgoingTransactionRecord.swift @@ -1,29 +1,36 @@ import Foundation import MarketKit -import TonKitKmm +import TonKit class TonOutgoingTransactionRecord: TonTransactionRecord { let transfers: [Transfer] let totalValue: TransactionValue + let sentToSelf: Bool - init(source: TransactionSource, transaction: TonTransactionWithTransfers, feeToken: Token, token: Token) { + init(source: TransactionSource, event: AccountEvent, feeToken: Token, token: Token, sentToSelf: Bool) { var totalAmount: Decimal = 0 - transfers = transaction.transfers.map { transfer in - let tonValue = TonAdapter.amount(kitAmount: transfer.amount) - let value = Decimal(sign: .minus, exponent: tonValue.exponent, significand: tonValue.significand) - - totalAmount += value + transfers = event.actions.compactMap { transfer in + guard let transfer = transfer as? TonTransfer else { + return nil + } + let tonValue = TonAdapter.amount(kitAmount: Decimal(transfer.amount)) + var value: Decimal = 0 + if !tonValue.isZero { + value = Decimal(sign: .minus, exponent: tonValue.exponent, significand: tonValue.significand) + totalAmount += value + } return Transfer( - address: transfer.dest.getNonBounceable(), + address: transfer.recipient.address.toString(bounceable: TonAdapter.bounceableDefault), value: .coinValue(token: token, value: value) ) } totalValue = .coinValue(token: token, value: totalAmount) + self.sentToSelf = sentToSelf - super.init(source: source, transaction: transaction, feeToken: feeToken) + super.init(source: source, event: event, feeToken: feeToken) } override var mainValue: TransactionValue? { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift index 209bf7e457..4ab9956aae 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift @@ -1,23 +1,25 @@ import Foundation import MarketKit -import TonKitKmm +import TonKit class TonTransactionRecord: TransactionRecord { let fee: TransactionValue? + let lt: Int64 let memo: String? - init(source: TransactionSource, transaction: TonTransactionWithTransfers, feeToken: Token) { - fee = transaction.fee.map { .coinValue(token: feeToken, value: TonAdapter.amount(kitAmount: $0)) } - memo = transaction.memo - + init(source: TransactionSource, event: AccountEvent, feeToken: Token) { + fee = .coinValue(token: feeToken, value: TonAdapter.amount(kitAmount: Decimal(event.fee))) + lt = event.lt + memo = event.actions.compactMap { ($0 as? TonTransfer)?.comment }.first + super.init( source: source, - uid: transaction.hash, - transactionHash: transaction.hash, + uid: event.eventId, + transactionHash: event.eventId, transactionIndex: 0, blockHeight: nil, confirmationsThreshold: nil, - date: Date(timeIntervalSince1970: TimeInterval(transaction.timestamp)), + date: Date(timeIntervalSince1970: TimeInterval(event.timestamp)), failed: false ) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift index 00c590dfbb..34123045f6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift @@ -31,6 +31,7 @@ class SendTonService { didSet { if !feeState.equalTo(oldValue) { feeStateRelay.accept(feeState) + syncState() } } } @@ -66,16 +67,20 @@ class SendTonService { subscribe(MainScheduler.instance, disposeBag, reachabilityManager.reachabilityObservable) { [weak self] isReachable in if isReachable { - self?.syncState() + self?.updateFee() } } - subscribe(scheduler, disposeBag, amountService.amountObservable) { [weak self] _ in self?.syncState() } - subscribe(scheduler, disposeBag, amountCautionService.amountCautionObservable) { [weak self] _ in self?.syncState() } - subscribe(scheduler, disposeBag, addressService.stateObservable) { [weak self] _ in self?.syncState() } + subscribe(scheduler, disposeBag, amountService.amountObservable) { [weak self] _ in self?.updateFee() } + subscribe(scheduler, disposeBag, amountCautionService.amountCautionObservable) { [weak self] _ in self?.updateFee() } + subscribe(scheduler, disposeBag, addressService.stateObservable) { [weak self] _ in self?.updateFee() } loadFee() } + + private func updateFee() { + loadFee() + } private func syncState() { guard amountCautionService.amountCaution == nil, !amountService.amount.isZero else { @@ -101,18 +106,44 @@ class SendTonService { state = .ready } + private func params() throws -> (Address, Decimal, String?) { + let address: Address + switch addressService.state { + case let .success(sendAddress): address = sendAddress + case let .fetchError(error): throw error + default: throw AppError.addressInvalid + } + + let amount = amountService.amount + + guard !amount.isZero else { + throw SendTransactionError.wrongAmount + } + + let memo = memoService.memo + return (address, amount, memo) + } + private func loadFee() { - Task { [weak self, adapter] in - do { - let fee = try await adapter.estimateFee() - self?.feeState = .completed(fee) - self?.availableBalance = .completed(max(0, adapter.availableBalance - fee)) - } catch { - self?.feeState = .failed(error) - self?.availableBalance = .completed(adapter.availableBalance) + do { + let data = try params() + feeState = .loading + + Task { [weak self, adapter] in + do { + let fee = try await adapter.estimateFee(recipient: data.0.raw, amount: data.1, memo: data.2) + self?.feeState = .completed(fee) + self?.availableBalance = .completed(max(0, adapter.availableBalance - fee)) + } catch { + self?.feeState = .failed(error) + self?.availableBalance = .completed(adapter.availableBalance) + } } + .store(in: &tasks) + } catch { + feeState = .failed(error) + availableBalance = .completed(adapter.availableBalance) } - .store(in: &tasks) } } @@ -124,33 +155,24 @@ extension SendTonService: ISendBaseService { extension SendTonService: ISendService { func sendSingle(logger _: HsToolKit.Logger) -> Single { - let address: Address - switch addressService.state { - case let .success(sendAddress): address = sendAddress - case let .fetchError(error): return Single.error(error) - default: return Single.error(AppError.addressInvalid) - } - - let amount = amountService.amount - - guard !amount.isZero else { - return Single.error(SendTransactionError.wrongAmount) - } - - let memo = memoService.memo - return Single.create { [adapter] observer in - let task = Task { [adapter] in - do { - try await adapter.send(recipient: address.raw, amount: amount, memo: memo) - observer(.success(())) - } catch { - observer(.error(error)) + do { + let data = try params() + return Single.create { [adapter] observer in + let task = Task { [adapter] in + do { + try await adapter.send(recipient: data.0.raw, amount: data.1, memo: data.2) + observer(.success(())) + } catch { + observer(.error(error)) + } } - } - return Disposables.create { - task.cancel() + return Disposables.create { + task.cancel() + } } + } catch { + return Single.error(error) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift index fae7eb89ec..44bb876920 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift @@ -559,7 +559,11 @@ class TransactionInfoViewItemFactory { } case let record as TonOutgoingTransactionRecord: for transfer in record.transfers { - sections.append(.init(sendSection(source: record.source, transactionValue: transfer.value, to: transfer.address, rates: item.rates, balanceHidden: balanceHidden))) + sections.append(.init(sendSection(source: record.source, transactionValue: transfer.value, to: transfer.address, rates: item.rates, sentToSelf: record.sentToSelf, balanceHidden: balanceHidden))) + } + + if record.sentToSelf { + sections.append(.init([.sentToSelf])) } if let memo = record.memo, !memo.isEmpty { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift index b67448d626..fe910d9c38 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift @@ -423,7 +423,7 @@ class TransactionsViewItemFactory { subTitle = "transactions.multiple".localized } - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.totalValue), type: type(value: record.totalValue, .outgoing)) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.totalValue, signType: record.sentToSelf ? .never : .always), type: type(value: record.totalValue, condition: record.sentToSelf, .neutral, .outgoing)) } else { iconType = .localIcon(imageName: item.record.source.blockchainType.iconPlain32) subTitle = "" @@ -432,6 +432,8 @@ class TransactionsViewItemFactory { if let currencyValue = item.currencyValue { secondaryValue = BaseTransactionsViewModel.Value(text: currencyString(from: currencyValue), type: .secondary) } + + sentToSelf = record.sentToSelf case is TonTransactionRecord: iconType = .localIcon(imageName: item.record.source.blockchainType.iconPlain32) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchViewController.swift index eaf36a013a..9ae927ab00 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Watch/WatchViewController.swift @@ -69,6 +69,11 @@ class WatchViewController: KeyboardAwareViewController { nameCell.onChangeText = { [weak self] in self?.viewModel.onChange(name: $0 ?? "") } watchDataInputCell.set(placeholderText: "watch_address.watch_data.placeholder".localized) + if let defaultWatchAddress = AppConfig.defaultWatchAddress { + watchDataInputCell.set(text: defaultWatchAddress) + viewModel.onChange(text: defaultWatchAddress) + } + watchDataInputCell.onChangeHeight = { [weak self] in self?.reloadTable() } watchDataInputCell.onChangeText = { [weak self] in self?.viewModel.onChange(text: $0) } watchDataInputCell.onChangeTextViewCaret = { [weak self] in self?.syncContentOffsetIfRequired(textView: $0) } From 6b2738be383e3f4d405fb9b04d78ae174800295a Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Fri, 14 Jun 2024 15:34:06 +0600 Subject: [PATCH 2/2] Fix reSync logic (from background and change internet state) --- .../project.pbxproj | 8 +- .../Core/Adapters/TonAdapter.swift | 76 ++++++------ .../Core/Managers/AppManager.swift | 7 ++ .../UnstoppableWallet/Extensions/Kmm.swift | 117 ------------------ .../Ton/TonIncomingTransactionRecord.swift | 4 +- .../Ton/TonTransactionRecord.swift | 2 +- .../Send/Platforms/Ton/SendTonService.swift | 10 +- .../TransactionsViewItemFactory.swift | 2 +- 8 files changed, 56 insertions(+), 170 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 82f21b8d40..552d1edca1 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -420,7 +420,6 @@ 11B354B5E42290EE934C428E /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359697FC3E92D4111ED5D /* String.swift */; }; 11B354B8BD1C3C036F6DE16A /* LitecoinAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356861F703A5A5C6630B6 /* LitecoinAdapter.swift */; }; 11B354BC4D954CCDA2E75C68 /* AddEvmSyncSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29037572DDAAF9E16 /* AddEvmSyncSourceViewModel.swift */; }; - 11B354C1218C0776499FAA5E /* Kmm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D813B2B43683404CCD6 /* Kmm.swift */; }; 11B354CAD4BC4FAB3889838D /* EvmSyncSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D9C2409FD9060974F67 /* EvmSyncSourceManager.swift */; }; 11B354CF393A2EAFDABE1C47 /* WalletTokenListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */; }; 11B354D628AADF3AFD9123E1 /* SingleCoinPriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F5E57874D4517F67B7 /* SingleCoinPriceWidget.swift */; }; @@ -781,7 +780,6 @@ 11B358C4D4C466ACCEF0E4C7 /* MultiSwapMainField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0D43137223A01FC2DA /* MultiSwapMainField.swift */; }; 11B358C72B4E7F70331084AA /* SendEvmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35113CB935A0E54504C1C /* SendEvmViewController.swift */; }; 11B358D01760F90518DA612F /* SendHandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B02ADF5EC5CC83FB33 /* SendHandlerFactory.swift */; }; - 11B358D0D4AE015DC9FECF29 /* Kmm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D813B2B43683404CCD6 /* Kmm.swift */; }; 11B358D1687049E5DACEBC96 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352884D47E0B23DCF2C2C /* AppManager.swift */; }; 11B358D35D2270FD78C6EF82 /* AutoLockPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E41142BD3D2FF59BAE7 /* AutoLockPeriod.swift */; }; 11B358D519ACFE88A7823C7E /* ApiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3531363949F235A210921 /* ApiProvider.swift */; }; @@ -3777,7 +3775,6 @@ 11B35D6FC62F4797DEE1C419 /* StatStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatStorage.swift; sourceTree = ""; }; 11B35D747108CE6727D3103D /* HsToolKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HsToolKit.swift; sourceTree = ""; }; 11B35D805327837A9E81801C /* ManageAccountsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountsViewController.swift; sourceTree = ""; }; - 11B35D813B2B43683404CCD6 /* Kmm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kmm.swift; sourceTree = ""; }; 11B35D8AF9D337A98530548D /* Auditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Auditor.swift; sourceTree = ""; }; 11B35D8B730D82D948B27210 /* ISendHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ISendHandler.swift; sourceTree = ""; }; 11B35D96B8963CDC30DC5643 /* NftViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftViewModel.swift; sourceTree = ""; }; @@ -5169,7 +5166,6 @@ ABC9A9B35C58F6525F3B2D5C /* FullCoin.swift */, ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */, ABC9A3DFC1E03CB2E6C12F2C /* Encodable.swift */, - 11B35D813B2B43683404CCD6 /* Kmm.swift */, D00DAE442B626C2900F48E1D /* GasPrice.swift */, ABC9A448ABC30B93088DE978 /* Binding.swift */, 11B35ED0A8819AB7EA27D368 /* StatExtensions.swift */, @@ -10501,7 +10497,6 @@ 11B35ACE7B126DCF9F7F1A19 /* SearchBar.swift in Sources */, 11B3546CB3C043D22A5F7A88 /* TransactionsViewModel.swift in Sources */, 11B35071705455BC25C214D2 /* TonAdapter.swift in Sources */, - 11B358D0D4AE015DC9FECF29 /* Kmm.swift in Sources */, 11B3535D6A72ED1D564A0F7C /* TonOutgoingTransactionRecord.swift in Sources */, 11B35C60FE9B94994FCCB0CB /* TonTransactionRecord.swift in Sources */, 11B3589CF4D819A0430DE3D9 /* TonIncomingTransactionRecord.swift in Sources */, @@ -11957,7 +11952,6 @@ 11B358781EBEFCE7CED000F0 /* SearchBar.swift in Sources */, 11B358D91C9D8102C46B97ED /* TransactionsViewModel.swift in Sources */, 11B35D88633A14FD13E91702 /* TonAdapter.swift in Sources */, - 11B354C1218C0776499FAA5E /* Kmm.swift in Sources */, 11B3589C124F6BBDDBB144F4 /* TonOutgoingTransactionRecord.swift in Sources */, D389BC4F2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift in Sources */, 11B35EB4CAA93773DF09B479 /* TonTransactionRecord.swift in Sources */, @@ -12870,7 +12864,7 @@ repositoryURL = "https://github.com/horizontalsystems/TonKit.Swift"; requirement = { kind = exactVersion; - version = 0.1.0; + version = 0.2.0; }; }; 6BF66DD82BA1A73300963242 /* XCRemoteSwiftPackageReference "ObjectMapper" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift index 4b4d6ca179..3c0a95806e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift @@ -1,3 +1,4 @@ +import BigInt import Combine import Foundation import HdWalletKit @@ -5,9 +6,8 @@ import HsToolKit import MarketKit import RxSwift import TonKit -import TweetNacl import TonSwift -import BigInt +import TweetNacl class TonAdapter { private static let coinRate: Decimal = 1_000_000_000 @@ -18,11 +18,15 @@ class TonAdapter { private let transactionSource: TransactionSource private let baseToken: Token private let reachabilityManager = App.shared.reachabilityManager + private let appManager = App.shared.appManager + private var cancellables = Set() private var adapterStarted = false private var kitStarted = false + private let logger: Logger? + private let adapterStateSubject = PublishSubject() private(set) var adapterState: AdapterState { didSet { @@ -43,12 +47,15 @@ class TonAdapter { transactionSource = wallet.transactionSource self.baseToken = baseToken +// logger = Logger(minLogLevel: .debug) + logger = App.shared.logger.scoped(with: "TonKit") + switch wallet.account.type { case .mnemonic: guard let seed = wallet.account.type.mnemonicSeed else { throw AdapterError.unsupportedAccount } - + let hdWallet = HDWallet(seed: seed, coinType: 607, xPrivKey: 0, curve: .ed25519) let privateKey = try hdWallet.privateKey(account: 0) let privateRaw = Data(privateKey.raw.bytes) @@ -61,7 +68,7 @@ class TonAdapter { network: .mainNet, walletId: wallet.account.id, apiKey: nil, - minLogLevel: .debug + logger: logger ) case let .tonAddress(address): @@ -71,7 +78,7 @@ class TonAdapter { network: .mainNet, walletId: wallet.account.id, apiKey: nil, - minLogLevel: .debug + logger: logger ) default: throw AdapterError.unsupportedAccount @@ -79,7 +86,6 @@ class TonAdapter { ownAddress = tonKit.address - adapterState = Self.adapterState(kitSyncState: tonKit.syncState) balanceData = BalanceData(available: Self.amount(kitAmount: tonKit.balance)) @@ -95,23 +101,17 @@ class TonAdapter { } .store(in: &cancellables) - reachabilityManager.$isReachable - .sink { [weak self] isReachable in - self?.handle(isReachable: isReachable) + appManager.didEnterBackgroundPublisher + .sink { [weak self] in + self?.stop() } .store(in: &cancellables) - } - - private func handle(isReachable: Bool) { - guard adapterStarted else { - return - } - if isReachable, !kitStarted { - startKit() - } else if !isReachable, kitStarted { - stopKit() - } + appManager.willEnterForegroundPublisher + .sink { [weak self] in + self?.start() + } + .store(in: &cancellables) } private func handle(tonTransactions: [TonKit.FullTransaction]) { @@ -136,7 +136,7 @@ class TonAdapter { } static func amount(kitAmount: Decimal) -> Decimal { - return kitAmount / coinRate + kitAmount / coinRate } private func transactionRecord(tonTransaction tx: TonKit.FullTransaction) -> TonTransactionRecord { @@ -159,15 +159,15 @@ class TonAdapter { ) default: - return TonTransactionRecord( - source: .init(blockchainType: .ton, meta: nil), - event: tx.event, - feeToken: baseToken - ) + return TonTransactionRecord( + source: .init(blockchainType: .ton, meta: nil), + event: tx.event, + feeToken: baseToken + ) } } - - private func tagQuery(token: MarketKit.Token?, filter: TransactionTypeFilter, address: String?) -> TransactionTagQuery { + + private func tagQuery(token _: MarketKit.Token?, filter: TransactionTypeFilter, address: String?) -> TransactionTagQuery { var type: TransactionTag.TagType? switch filter { @@ -182,11 +182,13 @@ class TonAdapter { } private func startKit() { + logger?.log(level: .debug, message: "TonAdapter, start kit.") tonKit.start() kitStarted = true } private func stopKit() { + logger?.log(level: .debug, message: "TonAdapter, stop kit.") tonKit.stop() kitStarted = false } @@ -220,7 +222,7 @@ extension TonAdapter: IAdapter { } var statusInfo: [(String, Any)] { - [] + [] // tonKit.statusInfo() } var debugInfo: String { @@ -236,7 +238,7 @@ extension TonAdapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { balanceDataSubject.asObservable() } - + var balanceState: AdapterState { adapterState } @@ -295,8 +297,8 @@ extension TonAdapter: ITransactionsAdapter { Task { [weak self] in let address = address.flatMap { try? FriendlyAddress(string: $0) }?.address.toRaw() - - let beforeLt = (from as? TonTransactionRecord).map { $0.lt } + + let beforeLt = (from as? TonTransactionRecord).map(\.lt) var tagQueries = [TransactionTagQuery]() switch filter { case .all: () @@ -304,14 +306,14 @@ extension TonAdapter: ITransactionsAdapter { case .outgoing: tagQueries.append(.init(type: .outgoing, address: address)) default: observer(.success([])) } - + let txs = (self?.tonKit .transactions(tagQueries: tagQueries, beforeLt: beforeLt, limit: limit) .compactMap { self?.transactionRecord(tonTransaction: $0) }) ?? [] - + observer(.success(txs)) } - + return Disposables.create() } } @@ -332,11 +334,11 @@ extension TonAdapter: ISendTonAdapter { func estimateFee(recipient: String, amount: Decimal, memo: String?) async throws -> Decimal { let amount = (amount * Self.coinRate).rounded(decimal: 0) - + let kitAmount = try await tonKit.estimateFee(recipient: recipient, amount: amount, comment: memo) return Self.amount(kitAmount: kitAmount) } - + func send(recipient: String, amount: Decimal, memo: String?) async throws { let amount = (amount * Self.coinRate).rounded(decimal: 0) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift index cb9e956ce5..b34740f5b4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift @@ -26,6 +26,7 @@ class AppManager { private let didBecomeActiveSubject = PublishSubject() private let willEnterForegroundSubjectOld = PublishSubject() + private let didEnterBackgroundSubject = PassthroughSubject() private let willEnterForegroundSubject = PassthroughSubject() init(accountManager: AccountManager, walletManager: WalletManager, adapterManager: AdapterManager, lockManager: LockManager, @@ -97,6 +98,8 @@ extension AppManager { func didEnterBackground() { debugBackgroundLogger?.logEnterBackground() + didEnterBackgroundSubject.send() + lockManager.didEnterBackground() walletConnectSocketConnectionService.didEnterBackground() balanceHiddenManager.didEnterBackground() @@ -133,6 +136,10 @@ extension AppManager { } extension AppManager { + var didEnterBackgroundPublisher: AnyPublisher { + didEnterBackgroundSubject.eraseToAnyPublisher() + } + var willEnterForegroundPublisher: AnyPublisher { willEnterForegroundSubject.eraseToAnyPublisher() } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift deleted file mode 100644 index a8e76defc5..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/Kmm.swift +++ /dev/null @@ -1,117 +0,0 @@ -//import Combine -//import TonKitKmm -// -//typealias OnEach = (Output) -> Void -//typealias OnCompletion = (Failure?) -> Void -// -//typealias OnCollect = (@escaping OnEach, @escaping OnCompletion) -> TonKitKmm.Cancellable -// -///** -// Creates a `Publisher` that collects output from a flow wrapper function emitting values from an underlying -// instance of `Flow`. -// */ -//func collect(_ onCollect: @escaping OnCollect) -> Publishers.Flow { -// Publishers.Flow(onCollect: onCollect) -//} -// -//class SharedCancellableSubscription: Subscription { -// private var isCancelled: Bool = false -// -// var cancellable: TonKitKmm.Cancellable? { -// didSet { -// if isCancelled { -// cancellable?.cancel() -// } -// } -// } -// -// func request(_: Subscribers.Demand) { -// // Not supported -// } -// -// func cancel() { -// isCancelled = true -// cancellable?.cancel() -// } -//} -// -//extension Publishers { -// class Flow: Publisher { -// private let onCollect: OnCollect -// -// init(onCollect: @escaping OnCollect) { -// self.onCollect = onCollect -// } -// -// func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { -// let subscription = SharedCancellableSubscription() -// subscriber.receive(subscription: subscription) -// -// let cancellable = onCollect({ input in _ = subscriber.receive(input) }) { failure in -// if let failure { -// subscriber.receive(completion: .failure(failure)) -// } else { -// subscriber.receive(completion: .finished) -// } -// } -// -// subscription.cancellable = cancellable -// } -// } -//} -// -//extension KotlinThrowable: Error {} -// -//enum PublisherFailures { -// /** -// The action to invoke when a failure is dropped as the result of a `Publisher` returned by -// `Publisher.completeOnFailure()`. -// */ -// static var willCompleteOnFailure: (Error, Callsite) -> Void = { error, callsite in -// // if error.isKotlinCancellation { -// // return -// // } -// -// print("[ERROR] A publisher failed and was completed due to a call to `completeOnFailure()` \(callsite): \(error)") -// } -//} -// -//struct Callsite: CustomStringConvertible { -// let file: String -// let fileID: String -// let filePath: String -// let line: Int -// let column: Int -// let function: String -// let dsoHandle: UnsafeRawPointer -// -// var description: String { -// "in \(function) at \(filePath)#\(line):\(column)" -// } -//} -// -//extension Publisher { -// /** -// Ignores errors in the upstream publisher, emitting an empty sequence instead, and otherwise republishes all received input. -// You can hook into these failures by assigning a function to `PublisherHooks.willCompleteOnFailure`. -// */ -// func completeOnFailure(file: String = #file, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column, function: String = #function, dsoHandle: UnsafeRawPointer = #dsohandle) -> Publishers.Catch> { -// `catch` { error in -// let callsite = Callsite(file: file, fileID: fileID, filePath: filePath, line: line, column: column, function: function, dsoHandle: dsoHandle) -// PublisherFailures.willCompleteOnFailure(error, callsite) -// return Empty(completeImmediately: true) -// } -// } -//} -// -//extension Data { -// func toKotlinByteArray() -> KotlinByteArray { -// let swiftByteArray = [UInt8](self) -// return swiftByteArray -// .map(Int8.init(bitPattern:)) -// .enumerated() -// .reduce(into: KotlinByteArray(size: Int32(swiftByteArray.count))) { result, row in -// result.set(index: Int32(row.offset), value: row.element) -// } -// } -//} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift index fedfc11b38..a7e58ef1a7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonIncomingTransactionRecord.swift @@ -1,8 +1,8 @@ +import BigInt import Foundation import MarketKit import TonKit import TonSwift -import BigInt class TonIncomingTransactionRecord: TonTransactionRecord { let transfer: Transfer? @@ -17,7 +17,7 @@ class TonIncomingTransactionRecord: TonTransactionRecord { address: transfer.recipient.address.toString(bounceable: TonAdapter.bounceableDefault), value: .coinValue(token: token, value: TonAdapter.amount(kitAmount: Decimal(transfer.amount))) ) - } + } super.init(source: source, event: event, feeToken: feeToken) } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift index 4ab9956aae..d3966e1b2c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Ton/TonTransactionRecord.swift @@ -11,7 +11,7 @@ class TonTransactionRecord: TransactionRecord { fee = .coinValue(token: feeToken, value: TonAdapter.amount(kitAmount: Decimal(event.fee))) lt = event.lt memo = event.actions.compactMap { ($0 as? TonTransfer)?.comment }.first - + super.init( source: source, uid: event.eventId, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift index 34123045f6..a21d2ffdcc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift @@ -77,7 +77,7 @@ class SendTonService { loadFee() } - + private func updateFee() { loadFee() } @@ -113,17 +113,17 @@ class SendTonService { case let .fetchError(error): throw error default: throw AppError.addressInvalid } - + let amount = amountService.amount - + guard !amount.isZero else { throw SendTransactionError.wrongAmount } - + let memo = memoService.memo return (address, amount, memo) } - + private func loadFee() { do { let data = try params() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift index fe910d9c38..f317939ec6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift @@ -432,7 +432,7 @@ class TransactionsViewItemFactory { if let currencyValue = item.currencyValue { secondaryValue = BaseTransactionsViewModel.Value(text: currencyString(from: currencyValue), type: .secondary) } - + sentToSelf = record.sentToSelf case is TonTransactionRecord: