diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 552d1edca1..74d8fcc459 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2644,6 +2644,10 @@ D0A6902F2C04969300E59296 /* CautionDataSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A6902D2C04969300E59296 /* CautionDataSourceViewModel.swift */; }; D0A690342C05D01C00E59296 /* UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690332C05D01C00E59296 /* UIImageView.swift */; }; D0A690352C05D01C00E59296 /* UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690332C05D01C00E59296 /* UIImageView.swift */; }; + D0A690252BFCB51800E59296 /* TronPresendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690242BFCB51800E59296 /* TronPresendHandler.swift */; }; + D0A690262BFCB51800E59296 /* TronPresendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690242BFCB51800E59296 /* TronPresendHandler.swift */; }; + D0A690282BFF50D900E59296 /* TronSendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690272BFF50D900E59296 /* TronSendData.swift */; }; + D0A690292BFF50D900E59296 /* TronSendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690272BFF50D900E59296 /* TronSendData.swift */; }; D0A980A92B5E3C0900127AF4 /* StepChangeButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A980A82B5E3C0900127AF4 /* StepChangeButtonsView.swift */; }; D0A980AA2B5E3C0900127AF4 /* StepChangeButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A980A82B5E3C0900127AF4 /* StepChangeButtonsView.swift */; }; D0A980AF2B60E73F00127AF4 /* LegacyFeeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A980AE2B60E73F00127AF4 /* LegacyFeeSettingsView.swift */; }; @@ -4508,6 +4512,8 @@ D0A6902A2C00ACF600E59296 /* CautionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CautionDataSource.swift; sourceTree = ""; }; D0A6902D2C04969300E59296 /* CautionDataSourceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CautionDataSourceViewModel.swift; sourceTree = ""; }; D0A690332C05D01C00E59296 /* UIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageView.swift; sourceTree = ""; }; + D0A690242BFCB51800E59296 /* TronPresendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronPresendHandler.swift; sourceTree = ""; }; + D0A690272BFF50D900E59296 /* TronSendData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronSendData.swift; sourceTree = ""; }; D0A980A82B5E3C0900127AF4 /* StepChangeButtonsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepChangeButtonsView.swift; sourceTree = ""; }; D0A980AE2B60E73F00127AF4 /* LegacyFeeSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFeeSettingsView.swift; sourceTree = ""; }; D0C2260F2A66A3BC007101F7 /* PersonalSupportModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalSupportModule.swift; sourceTree = ""; }; @@ -8554,6 +8560,8 @@ D3A580872BE4DAA2003953F4 /* EvmSendData.swift */, D3A5808A2BE4DB11003953F4 /* WalletConnectSendHandler.swift */, D0E5E8542BE38AA2005080A4 /* TronSendHandler.swift */, + D0A690242BFCB51800E59296 /* TronPresendHandler.swift */, + D0A690272BFF50D900E59296 /* TronSendData.swift */, D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */, D3A580932BE8AA80003953F4 /* BitcoinSendSettingsView.swift */, D3A580962BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift */, @@ -9179,6 +9187,7 @@ D36DE0FD272FD92F000BC916 /* SwapSelectProviderService.swift in Sources */, D008CA67267C8E1800001E0A /* ApproveTransactionRecord.swift in Sources */, D09D768F2A2E06D6004311E6 /* SendTronConfirmationViewController.swift in Sources */, + D0A690262BFCB51800E59296 /* TronPresendHandler.swift in Sources */, D384057F218317DF007D50AD /* LaunchService.swift in Sources */, D3840581218317DF007D50AD /* App.swift in Sources */, D3840583218317DF007D50AD /* BlurManager.swift in Sources */, @@ -10567,6 +10576,7 @@ 11B35337D37FD03982571DF0 /* ThorChainMultiSwapProvider.swift in Sources */, ABC9AE635D02A4E6DA719787 /* TransactionContactSelectView.swift in Sources */, ABC9AD02AD6340C230AC4F9D /* TransactionContactSelectViewModel.swift in Sources */, + D0A690292BFF50D900E59296 /* TronSendData.swift in Sources */, ABC9AC0F2A4A48BB223005EA /* TransactionBlockchainSelectViewModel.swift in Sources */, ABC9A24A0A2F6C7C476D7F69 /* TransactionTokenSelectViewModel.swift in Sources */, 11B35EFEEB7F361BD5FCCC3D /* SendView.swift in Sources */, @@ -10632,6 +10642,7 @@ D04D98EA268055A2001A3135 /* TransactionRecord.swift in Sources */, D008CA66267C8E1800001E0A /* ApproveTransactionRecord.swift in Sources */, D384066E21831B3D007D50AD /* LaunchService.swift in Sources */, + D0A690252BFCB51800E59296 /* TronPresendHandler.swift in Sources */, D384067021831B3D007D50AD /* App.swift in Sources */, D384067221831B3D007D50AD /* BlurManager.swift in Sources */, D384067421831B3D007D50AD /* MainSettingsModule.swift in Sources */, @@ -12020,6 +12031,7 @@ ABC9AB3C239A182E9B93AFDA /* MultiSwapApproveViewModel.swift in Sources */, ABC9A62E858360A9964084D2 /* Binding.swift in Sources */, ABC9AAF24F259B71E59C247D /* ErrorViewController.swift in Sources */, + D0A690282BFF50D900E59296 /* TronSendData.swift in Sources */, 11B357696EB7DB1895BF9F23 /* ThorChainMultiSwapProvider.swift in Sources */, ABC9A5B9D81C00155732363C /* TransactionContactSelectView.swift in Sources */, ABC9A6C99739F0AC308F0574 /* TransactionContactSelectViewModel.swift in Sources */, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionAdapter.swift index 04b7df3956..065b58484b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionAdapter.swift @@ -78,7 +78,7 @@ extension TronTransactionsAdapter: ITransactionsAdapter { return nil } - return TokenQuery(blockchainType: tronKitWrapper.blockchainType, tokenType: tokenType) + return TokenQuery(blockchainType: .tron, tokenType: tokenType) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionConverter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionConverter.swift index 219d2c2cbd..05c4ea7a8c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionConverter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronTransactionConverter.swift @@ -36,7 +36,7 @@ class TronTransactionConverter { } private func eip20Value(tokenAddress: TronKit.Address, value: BigUInt, sign: FloatingPointSign, tokenInfo: TokenInfo?) -> TransactionValue { - let query = TokenQuery(blockchainType: tronKitWrapper.blockchainType, tokenType: .eip20(address: tokenAddress.base58)) + let query = TokenQuery(blockchainType: .tron, tokenType: .eip20(address: tokenAddress.base58)) if let token = try? coinManager.token(query: query) { let value = convertAmount(amount: value, decimals: token.decimals, sign: sign) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift index 00f97c2644..cdcf141188 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift @@ -53,7 +53,7 @@ class AdapterFactory { } private func tronAdapter(wallet: Wallet) -> IAdapter? { - guard let tronKitWrapper = try? tronKitManager.tronKitWrapper(account: wallet.account, blockchainType: .tron) else { + guard let tronKitWrapper = try? tronKitManager.tronKitWrapper(account: wallet.account) else { return nil } @@ -61,7 +61,7 @@ class AdapterFactory { } private func trc20Adapter(address: String, wallet: Wallet) -> IAdapter? { - guard let tronKitWrapper = try? tronKitManager.tronKitWrapper(account: wallet.account, blockchainType: .tron) else { + guard let tronKitWrapper = try? tronKitManager.tronKitWrapper(account: wallet.account) else { return nil } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/TronKitManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/TronKitManager.swift index ad7d6c22cf..e0648f3ba9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/TronKitManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/TronKitManager.swift @@ -20,7 +20,7 @@ class TronKitManager { self.testNetManager = testNetManager } - private func _tronKitWrapper(account: Account, blockchainType: BlockchainType) throws -> TronKitWrapper { + private func _tronKitWrapper(account: Account) throws -> TronKitWrapper { if let _tronKitWrapper, let currentAccount, currentAccount == account { return _tronKitWrapper } @@ -52,7 +52,7 @@ class TronKitManager { tronKit.start() - let wrapper = TronKitWrapper(blockchainType: blockchainType, tronKit: tronKit, signer: signer) + let wrapper = TronKitWrapper(tronKit: tronKit, signer: signer) _tronKitWrapper = wrapper currentAccount = account @@ -74,20 +74,18 @@ extension TronKitManager { } } - func tronKitWrapper(account: Account, blockchainType: BlockchainType) throws -> TronKitWrapper { + func tronKitWrapper(account: Account) throws -> TronKitWrapper { try queue.sync { - try _tronKitWrapper(account: account, blockchainType: blockchainType) + try _tronKitWrapper(account: account) } } } class TronKitWrapper { - let blockchainType: BlockchainType let tronKit: TronKit.Kit let signer: Signer? - init(blockchainType: BlockchainType, tronKit: TronKit.Kit, signer: Signer?) { - self.blockchainType = blockchainType + init(tronKit: TronKit.Kit, signer: Signer?) { self.tronKit = tronKit self.signer = signer } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsView.swift index a74a14b446..aa3ee3d1ba 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsView.swift @@ -648,7 +648,7 @@ struct CoinAnalyticsView: View { VStack(spacing: .margin12) { cardHeader(text: title, info: info) - let value = { + let value: String = { switch viewItem { case .preview: return Self.placeholderText case let .regular(viewItem): return viewItem.value @@ -698,7 +698,7 @@ struct CoinAnalyticsView: View { VStack(spacing: .margin12) { cardHeader(text: title) - let value = { + let value: String = { switch viewItem.value { case .preview: return Self.placeholderText case let .regular(value): return value diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift index 35c4e26d0d..5993978bd4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift @@ -1,6 +1,9 @@ +import TronKit + enum FeeData { case evm(evmFeeData: EvmFeeData) case bitcoin(bitcoinFeeData: BitcoinFeeData) + case tron(fees: [Fee]) var gasLimit: Int? { switch self { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift index 67b37a3792..c4847bd0b1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift @@ -2,6 +2,7 @@ import BitcoinCore import EvmKit import Foundation import MarketKit +import TronKit import ZcashLightClientKit enum SendData { @@ -9,6 +10,7 @@ enum SendData { case bitcoin(token: Token, params: SendParameters) case binance(token: Token, amount: Decimal, address: String, memo: String?) case zcash(amount: Decimal, recipient: Recipient, memo: String?) + case tron(token: Token, contract: Contract) case swap(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider) case walletConnect(request: WalletConnectRequest) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift index c75e0e76b1..8e648322b9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift @@ -6,6 +6,7 @@ import SwiftUI enum SendField { case amount(title: String, token: Token, coinValueType: CoinValueType, currencyValue: CurrencyValue?, type: AmountType) case value(title: String, description: ActionSheetView.InfoDescription?, coinValue: CoinValue?, currencyValue: CurrencyValue?, formatFull: Bool) + case doubleValue(title: String, description: ActionSheetView.InfoDescription?, value1: String, value2: String?) case levelValue(title: String, value: String, level: ValueLevel) case address(title: String, value: String, blockchainType: BlockchainType) case price(title: String, tokenA: Token, tokenB: Token, amountA: Decimal, amountB: Decimal) @@ -105,6 +106,31 @@ enum SendField { } .buttonStyle(SecondaryCircleButtonStyle(style: .default)) } + case let .doubleValue(title, description, value1, value2): + ListRow(padding: EdgeInsets(top: .margin12, leading: description == nil ? .margin16 : 0, bottom: .margin12, trailing: .margin16)) { + if let description { + Text(title) + .textSubhead2() + .modifier(Informed(description: description)) + } else { + Text(title) + .textSubhead2() + } + + Spacer() + + VStack(alignment: .trailing, spacing: 1) { + Text(value1) + .textSubhead1(color: .themeLeah) + .multilineTextAlignment(.trailing) + + if let value2 { + Text(value2) + .textSubhead1(color: .themeLeah) + .multilineTextAlignment(.trailing) + } + } + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift index 7489f45f6e..882263ea25 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift @@ -11,6 +11,8 @@ enum SendHandlerFactory { return BinanceSendHandler.instance(token: token, amount: amount, address: address, memo: memo) case let .zcash(amount, recipient, memo): return ZcashSendHandler.instance(amount: amount, recipient: recipient, memo: memo) + case let .tron(token, contract): + return TronSendHandler.instance(token: token, contract: contract) case let .swap(tokenIn, tokenOut, amountIn, provider): return MultiSwapSendHandler.instance(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, provider: provider) case let .walletConnect(request): @@ -37,6 +39,10 @@ enum SendHandlerFactory { return ZcashPreSendHandler(token: wallet.token, adapter: adapter) } + if let adapter = adapter as? ISendTronAdapter & IBalanceAdapter { + return TronPreSendHandler(token: wallet.token, adapter: adapter) + } + return nil } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift index 878bb93f21..1a957f0d94 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift @@ -55,7 +55,7 @@ class SendViewModel: ObservableObject { return false } - guard let service = transactionService, !service.cautions.contains(where: { $0.type == .error }) else { + if let service = transactionService, service.cautions.contains(where: { $0.type == .error }) { return false } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronPresendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronPresendHandler.swift new file mode 100644 index 0000000000..6365b12d80 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronPresendHandler.swift @@ -0,0 +1,71 @@ +import BigInt +import Combine +import Foundation +import MarketKit +import RxSwift +import TronKit + +class TronPreSendHandler { + private let token: Token + private let adapter: ISendTronAdapter & IBalanceAdapter + + private let stateSubject = PassthroughSubject() + private let balanceSubject = PassthroughSubject() + + private let disposeBag = DisposeBag() + + init(token: Token, adapter: ISendTronAdapter & IBalanceAdapter) { + self.token = token + self.adapter = adapter + + adapter.balanceStateUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] state in + self?.stateSubject.send(state) + } + .disposed(by: disposeBag) + + adapter.balanceDataUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] balanceData in + self?.balanceSubject.send(balanceData.available) + } + .disposed(by: disposeBag) + } +} + +extension TronPreSendHandler: IPreSendHandler { + var state: AdapterState { + adapter.balanceState + } + + var statePublisher: AnyPublisher { + stateSubject.eraseToAnyPublisher() + } + + var balance: Decimal { + adapter.balanceData.available + } + + var balancePublisher: AnyPublisher { + balanceSubject.eraseToAnyPublisher() + } + + func sendData(amount: Decimal, address: String, memo: String?) -> SendDataResult { + guard let amountBigUInt = BigUInt(amount.hs.roundedString(decimal: token.decimals)) else { + return .invalid(cautions: []) + } + + guard let tronAddress = try? TronKit.Address(address: address) else { + return .invalid(cautions: []) + } + + guard tronAddress != adapter.tronKitWrapper.tronKit.receiveAddress else { + return .invalid(cautions: [CautionNew(text: "send.address_error.own_address".localized, type: .error)]) + } + + let contract = adapter.contract(amount: amountBigUInt, address: tronAddress, memo: memo) + + return .valid(sendData: .tron(token: token, contract: contract)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendData.swift new file mode 100644 index 0000000000..89733f2f3d --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendData.swift @@ -0,0 +1,194 @@ +import BigInt +import Foundation +import MarketKit +import TronKit + +class TronSendData: ISendData { + let token: Token + let baseToken: Token + let decoration: TransactionDecoration? + let contract: Contract? + let rateCoins: [Coin] + let transactionError: Error? + let fees: [Fee]? + let totalFees: Int? + + init(token: Token, baseToken: Token, decoration: TransactionDecoration?, contract: Contract?, rateCoins: [Coin], transactionError: Error?, fees: [Fee]?, totalFees: Int?) { + self.token = token + self.baseToken = baseToken + self.decoration = decoration + self.contract = contract + self.rateCoins = rateCoins + self.transactionError = transactionError + self.fees = fees + self.totalFees = totalFees + } + + var feeData: FeeData? { + fees.map { .tron(fees: $0) } + } + + var canSend: Bool { + fees != nil && transactionError == nil + } + + var customSendButtonTitle: String? { + nil + } + + private func feeFields(currency: Currency, feeTokenRate: Decimal?) -> [SendField] { + var viewItems = [SendField]() + + if let totalFees { + let decimalAmount = Decimal(totalFees) / pow(10, baseToken.decimals) + let coinValue = CoinValue(kind: .token(token: baseToken), value: decimalAmount) + let currencyValue = feeTokenRate.map { CurrencyValue(currency: currency, value: decimalAmount * $0) } + + viewItems.append( + .value( + title: "fee_settings.network_fee".localized, + description: .init(title: "fee_settings.network_fee".localized, description: "fee_settings.network_fee.info".localized), + coinValue: coinValue, + currencyValue: currencyValue, + formatFull: true + ) + ) + } + + if let fees { + var bandwidth: String? + var energy: String? + + for fee in fees { + switch fee { + case let .accountActivation(amount): + let decimalAmount = Decimal(amount) / pow(10, baseToken.decimals) + let coinValue = CoinValue(kind: .token(token: baseToken), value: decimalAmount) + let currencyValue = feeTokenRate.map { CurrencyValue(currency: currency, value: decimalAmount * $0) } + + viewItems.append( + .value( + title: "tron.send.activation_fee".localized, + description: .init(title: "tron.send.activation_fee".localized, description: "tron.send.activation_fee.info".localized), + coinValue: coinValue, + currencyValue: currencyValue, + formatFull: true + ) + ) + + case let .bandwidth(points, _): + bandwidth = ValueFormatter.instance.formatShort(value: Decimal(points), decimalCount: 0) + + case let .energy(required, _): + energy = ValueFormatter.instance.formatShort(value: Decimal(required), decimalCount: 0) + } + } + + if bandwidth != nil || energy != nil { + viewItems.append( + .doubleValue( + title: "tron.send.resources_consumed".localized, + description: .init(title: "tron.send.resources_consumed".localized, description: "tron.send.resources_consumed.info".localized), + value1: bandwidth.flatMap { "\($0) \("tron.send.bandwidth".localized)" } ?? "", + value2: energy.flatMap { "\($0) \("tron.send.energy".localized)" } + ) + ) + } + } + + return viewItems + } + + private func decorationSections(currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + guard let decoration else { + return [] + } + + switch decoration { + case let decoration as NativeTransactionDecoration: + guard let transfer = decoration.contract as? TransferContract else { + return [] + } + + return sendFields( + to: transfer.toAddress, + value: Decimal(transfer.amount) / pow(10, token.decimals), + currency: currency, + rate: rates[token.coin.uid] + ) + + case let decoration as OutgoingEip20Decoration: + return sendFields( + to: decoration.to, + value: Decimal(bigUInt: decoration.value, decimals: token.decimals) ?? 0, + currency: currency, + rate: rates[token.coin.uid] + ) + + default: + return [] + } + } + + private func sendFields(to: TronKit.Address, value: Decimal, currency: Currency, rate: Decimal?) -> [[SendField]] { + let coinValue = CoinValue(kind: .token(token: token), value: Decimal(sign: .plus, exponent: value.exponent, significand: value.significand)) + + return [[ + .amount( + title: "send.confirmation.you_send".localized, + token: token, + coinValueType: coinValue.isMaxValue ? .infinity(kind: coinValue.kind) : .regular(coinValue: coinValue), + currencyValue: coinValue.isMaxValue ? nil : rate.map { CurrencyValue(currency: currency, value: $0 * value) }, + type: .neutral + ), + .address( + title: "send.confirmation.to".localized, + value: to.base58, + blockchainType: token.blockchainType + ), + ]] + } + + func caution(transactionError: Error, feeToken: Token) -> CautionNew { + let title: String + let text: String + + if let tronError = transactionError as? TronSendHandler.TransactionError { + switch tronError { + case let .insufficientBalance(balance): + let coinValue = CoinValue(kind: .token(token: feeToken), value: balance.toDecimal(decimals: feeToken.decimals) ?? 0) + let balanceString = ValueFormatter.instance.formatShort(coinValue: coinValue) + + title = "fee_settings.errors.insufficient_balance".localized + text = "fee_settings.errors.insufficient_balance.info".localized(balanceString ?? "") + + case .zeroAmount: + title = "alert.error".localized + text = "fee_settings.errors.zero_amount.info".localized + } + } else { + title = "Error" + text = transactionError.convertedError.smartDescription + } + + return CautionNew(title: title, text: text, type: .error) + } + + func cautions(baseToken: Token) -> [CautionNew] { + var cautions = [CautionNew]() + + if let transactionError { + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) + } + + return cautions + } + + func sections(baseToken _: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var sections = decorationSections(currency: currency, rates: rates) + + sections.append(feeFields(currency: currency, feeTokenRate: rates[baseToken.coin.uid])) + + return sections + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift index 8b13789179..703eb83d27 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift @@ -1 +1,144 @@ +import BigInt +import Foundation +import MarketKit +import TronKit +class TronSendHandler { + let baseToken: Token + private let token: Token + private let contract: Contract + private let tronKitWrapper: TronKitWrapper + private let decorator = EvmDecorator() + + init(baseToken: Token, token: Token, contract: Contract, tronKitWrapper: TronKitWrapper) { + self.baseToken = baseToken + self.token = token + self.contract = contract + self.tronKitWrapper = tronKitWrapper + } + + private func calculateTotalFees(fees: [Fee]) -> Int { + var totalFees = 0 + for fee in fees { + switch fee { + case let .bandwidth(points, price): + totalFees += points * price + case let .energy(required, price): + totalFees += required * price + case let .accountActivation(amount): + totalFees += amount + } + } + + return totalFees + } +} + +extension TronSendHandler: ISendHandler { + var expirationDuration: Int? { + 10 + } + + func sendData(transactionSettings _: TransactionSettings?) async throws -> ISendData { + var totalFees: Int? + var fees: [Fee]? + var transactionError: Error? + var contract = contract + + let tronKit = tronKitWrapper.tronKit + let trxBalance = tronKit.trxBalance + + do { + let _fees = try await tronKit.estimateFee(contract: contract) + let _totalFees = calculateTotalFees(fees: _fees) + + var totalAmount = 0 + if let transfer = contract as? TransferContract { + var sentAmount = transfer.amount + if trxBalance == transfer.amount { + // If the maximum amount is being sent, then we subtract fees from sent amount + sentAmount = sentAmount - _totalFees + + guard sentAmount > 0 else { + throw TransactionError.zeroAmount + } + + contract = tronKit.transferContract(toAddress: transfer.toAddress, value: sentAmount) + } + totalAmount += sentAmount + } + + totalAmount += _totalFees + fees = _fees + totalFees = _totalFees + + if trxBalance < totalAmount { + throw TransactionError.insufficientBalance(balance: trxBalance) + } + } catch { + transactionError = error + } + + return TronSendData( + token: token, + baseToken: baseToken, + decoration: tronKit.decorate(contract: contract), + contract: contract, + rateCoins: Array(Set([baseToken.coin, token.coin])), + transactionError: transactionError, + fees: fees, + totalFees: totalFees + ) + } + + func send(data: ISendData) async throws { + guard let data = data as? TronSendData else { + throw SendError.invalidData + } + + guard let contract = data.contract else { + throw SendError.noContract + } + + guard let totalFees = data.totalFees else { + throw SendError.noFees + } + + _ = try await tronKitWrapper.send( + contract: contract, + feeLimit: totalFees + ) + } +} + +extension TronSendHandler { + enum SendError: Error { + case invalidData + case noFees + case noContract + } + + enum TransactionError: Error { + case insufficientBalance(balance: BigUInt) + case zeroAmount + } +} + +extension TronSendHandler { + static func instance(token: Token, contract: Contract) -> TronSendHandler? { + guard let baseToken = try? App.shared.coinManager.token(query: .init(blockchainType: .tron, tokenType: .native)) else { + return nil + } + + guard let adapter = App.shared.adapterManager.adapter(for: token) as? ISendTronAdapter else { + return nil + } + + return TronSendHandler( + baseToken: baseToken, + token: token, + contract: contract, + tronKitWrapper: adapter.tronKitWrapper + ) + } +}