diff --git a/BraveWallet/Crypto/Stores/AccountActivityStore.swift b/BraveWallet/Crypto/Stores/AccountActivityStore.swift index 278de4b7b4c..6178faac0d2 100644 --- a/BraveWallet/Crypto/Stores/AccountActivityStore.swift +++ b/BraveWallet/Crypto/Stores/AccountActivityStore.swift @@ -26,6 +26,7 @@ class AccountActivityStore: ObservableObject { private let assetRatioService: BraveWalletAssetRatioService private let txService: BraveWalletTxService private let blockchainRegistry: BraveWalletBlockchainRegistry + private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy init( account: BraveWallet.AccountInfo, @@ -34,7 +35,8 @@ class AccountActivityStore: ObservableObject { rpcService: BraveWalletJsonRpcService, assetRatioService: BraveWalletAssetRatioService, txService: BraveWalletTxService, - blockchainRegistry: BraveWalletBlockchainRegistry + blockchainRegistry: BraveWalletBlockchainRegistry, + solTxManagerProxy: BraveWalletSolanaTxManagerProxy ) { self.account = account self.keyringService = keyringService @@ -43,6 +45,7 @@ class AccountActivityStore: ObservableObject { self.assetRatioService = assetRatioService self.txService = txService self.blockchainRegistry = blockchainRegistry + self.solTxManagerProxy = solTxManagerProxy self.keyringService.add(self) self.rpcService.add(self) @@ -124,7 +127,12 @@ class AccountActivityStore: ObservableObject { allTokens: [BraveWallet.BlockchainToken], assetRatios: [String: Double] ) async -> [TransactionSummary] { - await txService.allTransactionInfo(.eth, from: account.address) + let transactions = await txService.allTransactionInfo(account.coin, from: account.address) + var solEstimatedTxFees: [String: UInt64] = [:] + if account.coin == .sol { + solEstimatedTxFees = await solTxManagerProxy.estimatedTxFees(for: transactions.map(\.id)) + } + return transactions .sorted(by: { $0.createdTime > $1.createdTime }) .map { transaction in TransactionParser.transactionSummary( @@ -134,6 +142,7 @@ class AccountActivityStore: ObservableObject { visibleTokens: userVisibleTokens, allTokens: allTokens, assetRatios: assetRatios, + solEstimatedTxFee: solEstimatedTxFees[transaction.id], currencyFormatter: currencyFormatter ) } diff --git a/BraveWallet/Crypto/Stores/AssetDetailStore.swift b/BraveWallet/Crypto/Stores/AssetDetailStore.swift index b374ce919ca..e5c5198646f 100644 --- a/BraveWallet/Crypto/Stores/AssetDetailStore.swift +++ b/BraveWallet/Crypto/Stores/AssetDetailStore.swift @@ -55,6 +55,7 @@ class AssetDetailStore: ObservableObject { private let rpcService: BraveWalletJsonRpcService private let txService: BraveWalletTxService private let blockchainRegistry: BraveWalletBlockchainRegistry + private let solTxManagerProxy: BraveWalletSolanaTxManagerProxy let token: BraveWallet.BlockchainToken @@ -65,6 +66,7 @@ class AssetDetailStore: ObservableObject { walletService: BraveWalletBraveWalletService, txService: BraveWalletTxService, blockchainRegistry: BraveWalletBlockchainRegistry, + solTxManagerProxy: BraveWalletSolanaTxManagerProxy, token: BraveWallet.BlockchainToken ) { self.assetRatioService = assetRatioService @@ -73,6 +75,7 @@ class AssetDetailStore: ObservableObject { self.walletService = walletService self.txService = txService self.blockchainRegistry = blockchainRegistry + self.solTxManagerProxy = solTxManagerProxy self.token = token self.keyringService.add(self) @@ -164,25 +167,43 @@ class AssetDetailStore: ObservableObject { keyring: BraveWallet.KeyringInfo, assetRatios: [String: Double] ) async -> [TransactionSummary] { - let network = await rpcService.network(.eth) + let coin = token.coin + let network = await rpcService.network(coin) + let userVisibleAssets = await walletService.userAssets(network.chainId, coin: coin) + let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: coin) let allTransactions = await withTaskGroup(of: [BraveWallet.TransactionInfo].self) { group -> [BraveWallet.TransactionInfo] in for account in keyring.accountInfos { group.addTask { - await self.txService.allTransactionInfo(.eth, from: account.address) + await self.txService.allTransactionInfo(coin, from: account.address) } } return await group.reduce([BraveWallet.TransactionInfo](), { partialResult, prior in return partialResult + prior }) } + var solEstimatedTxFees: [String: UInt64] = [:] + if token.coin == .sol { + solEstimatedTxFees = await solTxManagerProxy.estimatedTxFees(for: allTransactions.map(\.id)) + } return allTransactions .filter { tx in switch tx.txType { case .erc20Approve, .erc20Transfer: - let toAddress = tx.txDataUnion.ethTxData1559?.baseData.to - return toAddress == self.token.contractAddress + guard let tokenContractAddress = tx.txDataUnion.ethTxData1559?.baseData.to else { + return false + } + return tokenContractAddress.caseInsensitiveCompare(self.token.contractAddress) == .orderedSame case .ethSend, .ethSwap, .other, .erc721TransferFrom, .erc721SafeTransferFrom: return network.symbol.caseInsensitiveCompare(self.token.symbol) == .orderedSame + case .solanaSystemTransfer: + return network.symbol.caseInsensitiveCompare(self.token.symbol) == .orderedSame + case .solanaSplTokenTransfer, .solanaSplTokenTransferWithAssociatedTokenAccountCreation: + guard let tokenContractAddress = tx.txDataUnion.solanaTxData?.splTokenMintAddress else { + return false + } + return tokenContractAddress.caseInsensitiveCompare(self.token.contractAddress) == .orderedSame + case .erc1155SafeTransferFrom, .solanaDappSignTransaction, .solanaDappSignAndSendTransaction: + return false @unknown default: return false } @@ -193,9 +214,10 @@ class AssetDetailStore: ObservableObject { from: transaction, network: network, accountInfos: keyring.accountInfos, - visibleTokens: [token], - allTokens: [], + visibleTokens: userVisibleAssets, + allTokens: allTokens, assetRatios: assetRatios, + solEstimatedTxFee: solEstimatedTxFees[transaction.id], currencyFormatter: self.currencyFormatter ) } diff --git a/BraveWallet/Crypto/Stores/CryptoStore.swift b/BraveWallet/Crypto/Stores/CryptoStore.swift index 624c21dcd7f..2157357479b 100644 --- a/BraveWallet/Crypto/Stores/CryptoStore.swift +++ b/BraveWallet/Crypto/Stores/CryptoStore.swift @@ -172,6 +172,7 @@ public class CryptoStore: ObservableObject { walletService: walletService, txService: txService, blockchainRegistry: blockchainRegistry, + solTxManagerProxy: solTxManagerProxy, token: token ) assetDetailStore = store @@ -196,7 +197,8 @@ public class CryptoStore: ObservableObject { rpcService: rpcService, assetRatioService: assetRatioService, txService: txService, - blockchainRegistry: blockchainRegistry + blockchainRegistry: blockchainRegistry, + solTxManagerProxy: solTxManagerProxy ) accountActivityStore = store return store diff --git a/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift b/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift index 7e3411462cb..96589556aec 100644 --- a/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift +++ b/BraveWallet/Crypto/Transactions/TransactionParser+TransactionSummary.swift @@ -44,6 +44,7 @@ extension TransactionParser { visibleTokens: [BraveWallet.BlockchainToken], allTokens: [BraveWallet.BlockchainToken], assetRatios: [String: Double], + solEstimatedTxFee: UInt64?, currencyFormatter: NumberFormatter ) -> TransactionSummary { guard let parsedTransaction = parseTransaction( @@ -53,6 +54,7 @@ extension TransactionParser { visibleTokens: visibleTokens, allTokens: allTokens, assetRatios: assetRatios, + solEstimatedTxFee: solEstimatedTxFee, currencyFormatter: currencyFormatter, decimalFormatStyle: .balance // use 4 digit precision for summary ) else { @@ -72,17 +74,17 @@ extension TransactionParser { } switch parsedTransaction.details { case let .ethSend(details): - let title = String.localizedStringWithFormat(Strings.Wallet.transactionSendTitle, details.fromAmount, details.fromTokenSymbol, details.fromFiat ?? "") + let title = String.localizedStringWithFormat(Strings.Wallet.transactionSendTitle, details.fromAmount, details.fromToken.symbol, details.fromFiat ?? "") return .init( txInfo: transaction, namedFromAddress: parsedTransaction.namedFromAddress, namedToAddress: parsedTransaction.namedToAddress, title: title, gasFee: details.gasFee, - networkSymbol: details.fromTokenSymbol + networkSymbol: parsedTransaction.networkSymbol ) case let .erc20Transfer(details): - let title = String.localizedStringWithFormat(Strings.Wallet.transactionSendTitle, details.fromAmount, details.fromTokenSymbol, details.fromFiat ?? "") + let title = String.localizedStringWithFormat(Strings.Wallet.transactionSendTitle, details.fromAmount, details.fromToken.symbol, details.fromFiat ?? "") return .init( txInfo: transaction, namedFromAddress: parsedTransaction.namedFromAddress, @@ -135,6 +137,16 @@ extension TransactionParser { gasFee: nil, networkSymbol: parsedTransaction.networkSymbol ) + case let .solSystemTransfer(details), let .solSplTokenTransfer(details): + let title = String.localizedStringWithFormat(Strings.Wallet.transactionSendTitle, details.fromAmount, details.fromToken.symbol, details.fromFiat ?? "") + return .init( + txInfo: transaction, + namedFromAddress: parsedTransaction.namedFromAddress, + namedToAddress: parsedTransaction.namedToAddress, + title: title, + gasFee: details.gasFee, + networkSymbol: parsedTransaction.networkSymbol + ) } } } diff --git a/BraveWallet/Crypto/Transactions/TransactionParser.swift b/BraveWallet/Crypto/Transactions/TransactionParser.swift index c6157562890..0bdf0db221e 100644 --- a/BraveWallet/Crypto/Transactions/TransactionParser.swift +++ b/BraveWallet/Crypto/Transactions/TransactionParser.swift @@ -12,18 +12,37 @@ enum TransactionParser { from transaction: BraveWallet.TransactionInfo, network: BraveWallet.NetworkInfo, assetRatios: [String: Double], + solEstimatedTxFee: UInt64? = nil, currencyFormatter: NumberFormatter ) -> GasFee? { - let isEIP1559Transaction = transaction.isEIP1559Transaction - let limit = transaction.ethTxGasLimit - let formatter = WeiFormatter(decimalFormatStyle: .gasFee(limit: limit.removingHexPrefix, radix: .hex)) - let hexFee = isEIP1559Transaction ? (transaction.txDataUnion.ethTxData1559?.maxFeePerGas ?? "") : transaction.ethTxGasPrice - if let value = formatter.decimalString(for: hexFee.removingHexPrefix, radix: .hex, decimals: Int(network.decimals)) { - if let doubleValue = Double(value), let assetRatio = assetRatios[network.symbol.lowercased()] { - return .init(fee: value, fiat: currencyFormatter.string(from: NSNumber(value: doubleValue * assetRatio)) ?? "$0.00") - } else { - return .init(fee: value, fiat: "$0.00") + switch network.coin { + case .eth: + let isEIP1559Transaction = transaction.isEIP1559Transaction + let limit = transaction.ethTxGasLimit + let formatter = WeiFormatter(decimalFormatStyle: .gasFee(limit: limit.removingHexPrefix, radix: .hex)) + let hexFee = isEIP1559Transaction ? (transaction.txDataUnion.ethTxData1559?.maxFeePerGas ?? "") : transaction.ethTxGasPrice + if let value = formatter.decimalString(for: hexFee.removingHexPrefix, radix: .hex, decimals: Int(network.decimals)) { + if let doubleValue = Double(value), let assetRatio = assetRatios[network.symbol.lowercased()] { + return .init(fee: value, fiat: currencyFormatter.string(from: NSNumber(value: doubleValue * assetRatio)) ?? "$0.00") + } else { + return .init(fee: value, fiat: "$0.00") + } + } + case .sol: + guard let solEstimatedTxFee = solEstimatedTxFee else { return nil } + let gasFee = "\(solEstimatedTxFee)" + let formatter = WeiFormatter(decimalFormatStyle: .decimals(precision: Int(network.decimals))) + if let value = formatter.decimalString(for: gasFee, radix: .decimal, decimals: Int(network.decimals))?.trimmingTrailingZeros { + if let doubleValue = Double(value), let assetRatio = assetRatios[network.symbol.lowercased()] { + return .init(fee: value, fiat: currencyFormatter.string(from: NSNumber(value: doubleValue * assetRatio)) ?? "$0.00") + } else { + return .init(fee: value, fiat: "$0.00") + } } + case .fil: + break + @unknown default: + break } return nil } @@ -47,13 +66,13 @@ enum TransactionParser { visibleTokens: [BraveWallet.BlockchainToken], allTokens: [BraveWallet.BlockchainToken], assetRatios: [String: Double], + solEstimatedTxFee: UInt64?, currencyFormatter: NumberFormatter, decimalFormatStyle: WeiFormatter.DecimalFormatStyle? = nil ) -> ParsedTransaction? { let formatter = WeiFormatter(decimalFormatStyle: decimalFormatStyle ?? .decimals(precision: Int(network.decimals))) switch transaction.txType { case .ethSend, .other: - let fromTokenSymbol = network.symbol let fromValue = transaction.ethTxValue let fromValueFormatted = formatter.decimalString(for: fromValue.removingHexPrefix, radix: .hex, decimals: Int(network.decimals))?.trimmingTrailingZeros ?? "" let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[network.symbol.lowercased(), default: 0] * (Double(fromValueFormatted) ?? 0))) ?? "$0.00" @@ -75,7 +94,7 @@ enum TransactionParser { networkSymbol: network.symbol, details: .ethSend( .init( - fromTokenSymbol: fromTokenSymbol, + fromToken: network.nativeToken, fromValue: fromValue, fromAmount: fromValueFormatted, fromFiat: fromFiat, @@ -92,11 +111,11 @@ enum TransactionParser { guard let toAddress = transaction.txArgs[safe: 0], let fromValue = transaction.txArgs[safe: 1], let tokenContractAddress = transaction.txDataUnion.ethTxData1559?.baseData.to, - let token = token(for: tokenContractAddress, network: network, visibleTokens: visibleTokens, allTokens: allTokens) else { + let fromToken = token(for: tokenContractAddress, network: network, visibleTokens: visibleTokens, allTokens: allTokens) else { return nil } - let fromAmount = formatter.decimalString(for: fromValue.removingHexPrefix, radix: .hex, decimals: Int(token.decimals))?.trimmingTrailingZeros ?? "" - let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[token.symbol.lowercased(), default: 0] * (Double(fromAmount) ?? 0))) ?? "$0.00" + let fromAmount = formatter.decimalString(for: fromValue.removingHexPrefix, radix: .hex, decimals: Int(fromToken.decimals))?.trimmingTrailingZeros ?? "" + let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[fromToken.symbol.lowercased(), default: 0] * (Double(fromAmount) ?? 0))) ?? "$0.00" /* fromAddress="0x882F5a2c1C429e6592D801486566D0753BC1dD04" toAddress="0x7c24aed73d82c9d98a1b86bc2c8d2452c40419f8" @@ -115,7 +134,7 @@ enum TransactionParser { networkSymbol: network.symbol, details: .erc20Transfer( .init( - fromTokenSymbol: token.symbol, + fromToken: fromToken, fromValue: fromValue, fromAmount: fromAmount, fromFiat: fromFiat, @@ -148,19 +167,24 @@ enum TransactionParser { let formattedSellAmount = formatter.decimalString(for: sellAmountValue.removingHexPrefix, radix: .hex, decimals: fromTokenDecimals)?.trimmingTrailingZeros ?? "" let formattedMinBuyAmount = formatter.decimalString(for: minBuyAmountValue.removingHexPrefix, radix: .hex, decimals: toTokenDecimals)?.trimmingTrailingZeros ?? "" + + let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[fromToken?.symbol.lowercased() ?? "", default: 0] * (Double(formattedSellAmount) ?? 0))) ?? "$0.00" + let minBuyAmountFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[toToken?.symbol.lowercased() ?? "", default: 0] * (Double(formattedMinBuyAmount) ?? 0))) ?? "$0.00" /* Example: USDC -> DAI Sell Amount: 1.5 - fillPath="0x07865c6e87b9f70255377e024ace6630c1eaa37fad6d458402f60fd3bd25163575031acdce07538d" + fillPath = "0x07865c6e87b9f70255377e024ace6630c1eaa37fad6d458402f60fd3bd25163575031acdce07538d" fromTokenAddress = "0x07865c6e87b9f70255377e024ace6630c1eaa37f" fromToken.symbol = "USDC" - sellAmountValue="0x16e360" - formattedSellAmount="1.5" + sellAmountValue = "0x16e360" + formattedSellAmount = "1.5" + fromFiat = "$187.37" toTokenAddress = "0xad6d458402f60fd3bd25163575031acdce07538d" toToken.symbol = "DAI" - minBuyAmountValue="0x1bd02ca9a7c244e" - formattedMinBuyAmount="0.125259433834718286" + minBuyAmountValue = "0x1bd02ca9a7c244e" + formattedMinBuyAmount = "0.125259433834718286" + minBuyAmountFiat = "$6.67" */ return .init( transaction: transaction, @@ -174,9 +198,11 @@ enum TransactionParser { fromToken: fromToken, fromValue: sellAmountValue, fromAmount: formattedSellAmount, + fromFiat: fromFiat, toToken: toToken, minBuyValue: minBuyAmountValue, minBuyAmount: formattedMinBuyAmount, + minBuyAmountFiat: minBuyAmountFiat, gasFee: gasFee( from: transaction, network: network, @@ -258,11 +284,88 @@ enum TransactionParser { ) ) case .solanaSystemTransfer: - return nil - case .solanaSplTokenTransfer: - return nil - case .solanaSplTokenTransferWithAssociatedTokenAccountCreation: - return nil + guard let lamports = transaction.txDataUnion.solanaTxData?.lamports, + let toAddress = transaction.txDataUnion.solanaTxData?.toWalletAddress else { + return nil + } + let fromValue = "\(lamports)" + let fromValueFormatted = formatter.decimalString(for: fromValue, radix: .decimal, decimals: Int(network.decimals))?.trimmingTrailingZeros ?? "" + let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[network.symbol.lowercased(), default: 0] * (Double(fromValueFormatted) ?? 0))) ?? "$0.00" + /* Example: + Send 0.1234 SOL + + fromAddress="0x882F5a2c1C429e6592D801486566D0753BC1dD04" + toAddress="0x4FC29eDF46859A67c5Bfa894C77a4E3C69353202" + fromTokenSymbol="SOL" + fromValue="0x1b667a56d488000" + fromValueFormatted="0.1234" + */ + return .init( + transaction: transaction, + namedFromAddress: NamedAddresses.name(for: transaction.fromAddress, accounts: accountInfos), + fromAddress: transaction.fromAddress, + namedToAddress: NamedAddresses.name(for: toAddress, accounts: accountInfos), + toAddress: toAddress, + networkSymbol: network.symbol, + details: .solSystemTransfer( + .init( + fromToken: network.nativeToken, + fromValue: fromValue, + fromAmount: fromValueFormatted, + fromFiat: fromFiat, + gasFee: gasFee( + from: transaction, + network: network, + assetRatios: assetRatios, + solEstimatedTxFee: solEstimatedTxFee, + currencyFormatter: currencyFormatter + ) + ) + ) + ) + case .solanaSplTokenTransfer, + .solanaSplTokenTransferWithAssociatedTokenAccountCreation: + guard let amount = transaction.txDataUnion.solanaTxData?.amount, + let toAddress = transaction.txDataUnion.solanaTxData?.toWalletAddress, + let splTokenMintAddress = transaction.txDataUnion.solanaTxData?.splTokenMintAddress, + let fromToken = token(for: splTokenMintAddress, network: network, visibleTokens: visibleTokens, allTokens: allTokens) else { + return nil + } + let fromValue = "\(amount)" + let fromValueFormatted = formatter.decimalString(for: fromValue, radix: .decimal, decimals: Int(fromToken.decimals))?.trimmingTrailingZeros ?? "" + let fromFiat = currencyFormatter.string(from: NSNumber(value: assetRatios[fromToken.symbol.lowercased(), default: 0] * (Double(fromValueFormatted) ?? 0))) ?? "$0.00" + /* Example: + Send 0.1234 SMB + + fromAddress="0x882F5a2c1C429e6592D801486566D0753BC1dD04" + toAddress="0x4FC29eDF46859A67c5Bfa894C77a4E3C69353202" + fromTokenSymbol="SMB" + fromValue="0x1b667a56d488000" + fromValueFormatted="0.1234" + */ + return .init( + transaction: transaction, + namedFromAddress: NamedAddresses.name(for: transaction.fromAddress, accounts: accountInfos), + fromAddress: transaction.fromAddress, + namedToAddress: NamedAddresses.name(for: toAddress, accounts: accountInfos), + toAddress: toAddress, + networkSymbol: network.symbol, + details: .solSplTokenTransfer( + .init( + fromToken: fromToken, + fromValue: fromValue, + fromAmount: fromValueFormatted, + fromFiat: fromFiat, + gasFee: gasFee( + from: transaction, + network: network, + assetRatios: assetRatios, + solEstimatedTxFee: solEstimatedTxFee, + currencyFormatter: currencyFormatter + ) + ) + ) + ) case .erc1155SafeTransferFrom: return nil case .solanaDappSignAndSendTransaction: @@ -284,11 +387,13 @@ struct GasFee: Equatable { struct ParsedTransaction: Equatable { enum Details: Equatable { - case ethSend(EthSendDetails) - case erc20Transfer(EthSendDetails) + case ethSend(SendDetails) + case erc20Transfer(SendDetails) case ethSwap(EthSwapDetails) case ethErc20Approve(EthErc20ApproveDetails) case erc721Transfer(Eth721TransferDetails) + case solSystemTransfer(SendDetails) + case solSplTokenTransfer(SendDetails) } /// The transaction @@ -309,6 +414,23 @@ struct ParsedTransaction: Equatable { /// Details of the transaction let details: Details + + /// Gas fee for the transaction if available + var gasFee: GasFee? { + switch details { + case let .ethSend(details), + let .erc20Transfer(details), + let .solSystemTransfer(details), + let .solSplTokenTransfer(details): + return details.gasFee + case let .ethSwap(details): + return details.gasFee + case let .ethErc20Approve(details): + return details.gasFee + case .erc721Transfer: + return nil + } + } } struct EthErc20ApproveDetails: Equatable { @@ -324,9 +446,9 @@ struct EthErc20ApproveDetails: Equatable { let gasFee: GasFee? } -struct EthSendDetails: Equatable { - /// From token symbol - let fromTokenSymbol: String +struct SendDetails: Equatable { + /// Token being swapped from + let fromToken: BraveWallet.BlockchainToken /// From value prior to formatting let fromValue: String /// From amount formatted @@ -345,6 +467,8 @@ struct EthSwapDetails: Equatable { let fromValue: String /// From amount formatted let fromAmount: String + /// The amount formatted as currency + let fromFiat: String? /// Token being swapped to let toToken: BraveWallet.BlockchainToken? @@ -352,6 +476,8 @@ struct EthSwapDetails: Equatable { let minBuyValue: String /// Min. buy amount formatted let minBuyAmount: String + /// The amount formatted as currency + let minBuyAmountFiat: String? /// Gas fee for the transaction let gasFee: GasFee? @@ -379,6 +505,7 @@ extension BraveWallet.TransactionInfo { visibleTokens: [BraveWallet.BlockchainToken], allTokens: [BraveWallet.BlockchainToken], assetRatios: [String: Double], + solEstimatedTxFee: UInt64? = nil, currencyFormatter: NumberFormatter, decimalFormatStyle: WeiFormatter.DecimalFormatStyle? = nil ) -> ParsedTransaction? { @@ -389,6 +516,7 @@ extension BraveWallet.TransactionInfo { visibleTokens: visibleTokens, allTokens: allTokens, assetRatios: assetRatios, + solEstimatedTxFee: solEstimatedTxFee, currencyFormatter: currencyFormatter, decimalFormatStyle: decimalFormatStyle ) diff --git a/BraveWallet/Extensions/SolanaTxManagerProxyExtensions.swift b/BraveWallet/Extensions/SolanaTxManagerProxyExtensions.swift new file mode 100644 index 00000000000..f2ec0fa734c --- /dev/null +++ b/BraveWallet/Extensions/SolanaTxManagerProxyExtensions.swift @@ -0,0 +1,39 @@ +// Copyright 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import BraveCore + +extension BraveWalletSolanaTxManagerProxy { + + /// Fetches the estimatedTxFee for an array of transaction meta ids. + func estimatedTxFees( + for transactionMetaIds: [String], + completion: @escaping ([String: UInt64]) -> Void + ) { + var estimatedTxFees: [String: UInt64] = [:] + let dispatchGroup = DispatchGroup() + transactionMetaIds.forEach { txMetaId in + dispatchGroup.enter() + estimatedTxFee(txMetaId) { fee, _, _ in + defer { dispatchGroup.leave() } + estimatedTxFees[txMetaId] = fee + } + } + dispatchGroup.notify(queue: .main) { + completion(estimatedTxFees) + } + } + + /// Fetches the estimatedTxFee for an array of transaction meta ids. + @MainActor func estimatedTxFees( + for transactionMetaIds: [String] + ) async -> [String: UInt64] { + await withCheckedContinuation { continuation in + estimatedTxFees(for: transactionMetaIds) { fees in + continuation.resume(returning: fees) + } + } + } +} diff --git a/BraveWallet/Preview Content/MockContent.swift b/BraveWallet/Preview Content/MockContent.swift index aba07d975f8..01bc4814f78 100644 --- a/BraveWallet/Preview Content/MockContent.swift +++ b/BraveWallet/Preview Content/MockContent.swift @@ -55,7 +55,7 @@ extension BraveWallet.BlockchainToken { ) static let mockSolToken: BraveWallet.BlockchainToken = .init( - contractAddress: "0x1111111111222222222233333333334444444444", + contractAddress: "", name: "Solana", logo: "", isErc20: false, @@ -68,15 +68,15 @@ extension BraveWallet.BlockchainToken { chainId: "", coin: .sol ) - - static let mockSPLToken: BraveWallet.BlockchainToken = .init( - contractAddress: "0x1111111111222222222233333333334444444445", - name: "Non-SOL", + + static let mockSpdToken: BraveWallet.BlockchainToken = .init( + contractAddress: "0x1111111111222222222233333333334444444444", + name: "", logo: "", isErc20: false, isErc721: false, - symbol: "NONSOL", - decimals: 9, + symbol: "SPD", + decimals: 6, visible: false, tokenId: "", coingeckoId: "", @@ -212,6 +212,7 @@ extension TransactionSummary { visibleTokens: [.previewToken, .previewDaiToken], allTokens: [], assetRatios: [BraveWallet.BlockchainToken.previewToken.symbol.lowercased(): 1], + solEstimatedTxFee: nil, currencyFormatter: .usdCurrencyFormatter ) } diff --git a/BraveWallet/Preview Content/MockStores.swift b/BraveWallet/Preview Content/MockStores.swift index bd17cf8070b..ebabbe3607f 100644 --- a/BraveWallet/Preview Content/MockStores.swift +++ b/BraveWallet/Preview Content/MockStores.swift @@ -105,6 +105,7 @@ extension AssetDetailStore { walletService: MockBraveWalletService(), txService: MockTxService(), blockchainRegistry: MockBlockchainRegistry(), + solTxManagerProxy: BraveWallet.TestSolanaTxManagerProxy.previewProxy, token: .previewToken ) } @@ -146,7 +147,8 @@ extension AccountActivityStore { rpcService: MockJsonRpcService(), assetRatioService: MockAssetRatioService(), txService: MockTxService(), - blockchainRegistry: MockBlockchainRegistry() + blockchainRegistry: MockBlockchainRegistry(), + solTxManagerProxy: BraveWallet.TestSolanaTxManagerProxy.previewProxy ) } } diff --git a/Tests/BraveWalletTests/SendTokenStoreTests.swift b/Tests/BraveWalletTests/SendTokenStoreTests.swift index 79a3b686aa1..eb8db31d59a 100644 --- a/Tests/BraveWalletTests/SendTokenStoreTests.swift +++ b/Tests/BraveWalletTests/SendTokenStoreTests.swift @@ -341,7 +341,7 @@ class SendTokenStoreTests: XCTestCase { blockchainRegistry: MockBlockchainRegistry(), ethTxManagerProxy: MockEthTxManagerProxy(), solTxManagerProxy: solTxManagerProxy, - prefilledToken: .mockSPLToken + prefilledToken: .mockSpdToken ) let ex = expectation(description: "send-sol-transaction") diff --git a/Tests/BraveWalletTests/TransactionParserTests.swift b/Tests/BraveWalletTests/TransactionParserTests.swift index 50713bbc141..0519d29394f 100644 --- a/Tests/BraveWalletTests/TransactionParserTests.swift +++ b/Tests/BraveWalletTests/TransactionParserTests.swift @@ -27,12 +27,19 @@ class TransactionParserTests: XCTestCase { private let currencyFormatter: NumberFormatter = .usdCurrencyFormatter private let accountInfos: [BraveWallet.AccountInfo] = [ - .init(address: "0x1234567890123456789012345678901234567890", name: "Account 1"), - .init(address: "0x0987654321098765432109876543210987654321", name: "Account 2") + .init(address: "0x1234567890123456789012345678901234567890", name: "Ethereum Account 1"), + .init(address: "0x0987654321098765432109876543210987654321", name: "Ethereum Account 2"), + .init(address: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", name: "Solana Account 1", coin: .sol), + .init(address: "0xeeeeeeeeeeffffffffff11111111112222222222", name: "Solana Account 2", coin: .sol) + ] + private let tokens: [BraveWallet.BlockchainToken] = [ + .previewToken, .previewDaiToken, .mockUSDCToken, .mockSolToken, .mockSpdToken ] - private let tokens: [BraveWallet.BlockchainToken] = [.previewToken, .previewDaiToken, .mockUSDCToken] let assetRatios: [String: Double] = ["eth": 1, - "dai": 2] + "dai": 2, + "usdc": 3, + "sol": 20, + "spd": 15] func testEthSendTransaction() { let network: BraveWallet.NetworkInfo = .mockMainnet @@ -76,14 +83,14 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", - namedToAddress: "Account 2", + namedToAddress: "Ethereum Account 2", toAddress: "0x0987654321098765432109876543210987654321", networkSymbol: "ETH", details: .ethSend( .init( - fromTokenSymbol: "ETH", + fromToken: network.nativeToken, fromValue: "0x1b667a56d488000", fromAmount: "0.1234", fromFiat: "$0.12", @@ -102,12 +109,26 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse ethSend transaction") return } - XCTAssertEqual(expectedParsedTransaction, parsedTransaction) + XCTAssertEqual(expectedParsedTransaction.fromAddress, parsedTransaction.fromAddress) + XCTAssertEqual(expectedParsedTransaction.namedFromAddress, parsedTransaction.namedFromAddress) + XCTAssertEqual(expectedParsedTransaction.toAddress, parsedTransaction.toAddress) + XCTAssertEqual(expectedParsedTransaction.networkSymbol, parsedTransaction.networkSymbol) + guard case let .ethSend(expectedDetails) = expectedParsedTransaction.details, + case let .ethSend(parsedDetails) = parsedTransaction.details else { + XCTFail("Incorrectly parsed ethSend transaction") + return + } + // `fromToken` to fail equatability check because `network.nativeToken` will because is a computed property + XCTAssertEqual(expectedDetails.fromValue, parsedDetails.fromValue) + XCTAssertEqual(expectedDetails.fromAmount, parsedDetails.fromAmount) + XCTAssertEqual(expectedDetails.fromFiat, parsedDetails.fromFiat) + XCTAssertEqual(expectedDetails.gasFee, parsedDetails.gasFee) } func testEthErc20TransferTransaction() { @@ -152,14 +173,14 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", - namedToAddress: "Account 2", + namedToAddress: "Ethereum Account 2", toAddress: "0x0987654321098765432109876543210987654321", networkSymbol: "ETH", details: .erc20Transfer( .init( - fromTokenSymbol: "DAI", + fromToken: .previewDaiToken, fromValue: "0x5ff20a91f724000", fromAmount: "0.4321", fromFiat: "$0.86", @@ -178,6 +199,7 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse erc20Transfer transaction") @@ -232,7 +254,7 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", namedToAddress: "0x Exchange Proxy", toAddress: "0xDef1C0ded9bec7F1a1670819833240f027b25EfF", @@ -242,9 +264,11 @@ class TransactionParserTests: XCTestCase { fromToken: .previewToken, fromValue: "0x1b6951ef585a000", fromAmount: "0.12345", + fromFiat: "$0.12", toToken: .previewDaiToken, minBuyValue: "0x5c6f2d76e910358b", minBuyAmount: "6.660592362643797387", + minBuyAmountFiat: "$13.32", gasFee: .init( fee: "0.000466", fiat: "$0.00" @@ -260,6 +284,7 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse ethSwap transaction") @@ -314,7 +339,7 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", namedToAddress: "0x Exchange Proxy", toAddress: "0xDef1C0ded9bec7F1a1670819833240f027b25EfF", @@ -324,9 +349,11 @@ class TransactionParserTests: XCTestCase { fromToken: .mockUSDCToken, fromValue: "0x16e360", fromAmount: "1.5", + fromFiat: "$4.50", toToken: .previewDaiToken, minBuyValue: "0x1bd02ca9a7c244e", minBuyAmount: "0.125259433834718286", + minBuyAmountFiat: "$0.25", gasFee: .init( fee: "0.000466", fiat: "$0.00" @@ -342,6 +369,7 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse ethSwap transaction") @@ -392,7 +420,7 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", namedToAddress: BraveWallet.BlockchainToken.previewDaiToken.contractAddress.truncatedAddress, toAddress: BraveWallet.BlockchainToken.previewDaiToken.contractAddress, @@ -418,6 +446,7 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse erc20Approve transaction") @@ -468,7 +497,7 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", namedToAddress: BraveWallet.BlockchainToken.previewDaiToken.contractAddress.truncatedAddress, toAddress: BraveWallet.BlockchainToken.previewDaiToken.contractAddress, @@ -494,6 +523,7 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse erc20Approve transaction") @@ -548,9 +578,9 @@ class TransactionParserTests: XCTestCase { let expectedParsedTransaction = ParsedTransaction( transaction: transaction, - namedFromAddress: "Account 1", + namedFromAddress: "Ethereum Account 1", fromAddress: "0x1234567890123456789012345678901234567890", - namedToAddress: "Account 2", + namedToAddress: "Ethereum Account 2", toAddress: "0x0987654321098765432109876543210987654321", networkSymbol: "ETH", details: .erc721Transfer( @@ -571,6 +601,7 @@ class TransactionParserTests: XCTestCase { visibleTokens: tokens, allTokens: tokens, assetRatios: assetRatios, + solEstimatedTxFee: nil, currencyFormatter: currencyFormatter ) else { XCTFail("Failed to parse erc721TransferFrom transaction") @@ -578,4 +609,159 @@ class TransactionParserTests: XCTestCase { } XCTAssertEqual(expectedParsedTransaction, parsedTransaction) } + + func testSolanaSystemTransfer() { + let network: BraveWallet.NetworkInfo = .mockSolana + + let transactionData: BraveWallet.SolanaTxData = .init( + recentBlockhash: "", + lastValidBlockHeight: 0, + feePayer: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + toWalletAddress: "0xeeeeeeeeeeffffffffff11111111112222222222", + splTokenMintAddress: "", + lamports: 100000000, + amount: 0, + txType: .solanaSystemTransfer, + instructions: [ + .init( + programId: "", + accountMetas: [.init(pubkey: "", isSigner: false, isWritable: false)], + data: []) + ], + send: .init(maxRetries: .init(maxRetries: 1), preflightCommitment: nil, skipPreflight: nil), + signTransactionParam: nil + ) + let transaction = BraveWallet.TransactionInfo( + id: "7", + fromAddress: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + txHash: "0xaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff1234", + txDataUnion: .init(solanaTxData: transactionData), + txStatus: .confirmed, + txType: .solanaSystemTransfer, + txParams: [], + txArgs: [ + ], + createdTime: Date(), + submittedTime: Date(), + confirmedTime: Date(), + originInfo: nil + ) + let expectedParsedTransaction = ParsedTransaction( + transaction: transaction, + namedFromAddress: "Solana Account 1", + fromAddress: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + namedToAddress: "Solana Account 2", + toAddress: "0xeeeeeeeeeeffffffffff11111111112222222222", + networkSymbol: "SOL", + details: .solSystemTransfer( + .init( + fromToken: .mockSolToken, + fromValue: "100000000", + fromAmount: "0.1", + fromFiat: "$2.00", + gasFee: .init(fee: "0.00123", fiat: "$0.02") + ) + ) + ) + + guard let parsedTransaction = TransactionParser.parseTransaction( + transaction: transaction, + network: network, + accountInfos: accountInfos, + visibleTokens: tokens, + allTokens: tokens, + assetRatios: assetRatios, + solEstimatedTxFee: 1230000, + currencyFormatter: currencyFormatter + ) else { + XCTFail("Failed to parse solanaSystemTransfer transaction") + return + } + + XCTAssertEqual(expectedParsedTransaction.fromAddress, parsedTransaction.fromAddress) + XCTAssertEqual(expectedParsedTransaction.namedFromAddress, parsedTransaction.namedFromAddress) + XCTAssertEqual(expectedParsedTransaction.toAddress, parsedTransaction.toAddress) + XCTAssertEqual(expectedParsedTransaction.networkSymbol, parsedTransaction.networkSymbol) + guard case let .solSystemTransfer(expectedDetails) = expectedParsedTransaction.details, + case let .solSystemTransfer(parsedDetails) = parsedTransaction.details else { + XCTFail("Incorrectly parsed solanaSystemTransfer transaction") + return + } + // `fromToken` to fail equatability check because `network.nativeToken` will because is a computed property + XCTAssertEqual(expectedDetails.fromValue, parsedDetails.fromValue) + XCTAssertEqual(expectedDetails.fromAmount, parsedDetails.fromAmount) + XCTAssertEqual(expectedDetails.fromFiat, parsedDetails.fromFiat) + XCTAssertEqual(expectedDetails.gasFee, parsedDetails.gasFee) + } + + func testSolanaSplTokenTransfer() { + let network: BraveWallet.NetworkInfo = .mockSolana + + let transactionData: BraveWallet.SolanaTxData = .init( + recentBlockhash: "", + lastValidBlockHeight: 0, + feePayer: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + toWalletAddress: "0xeeeeeeeeeeffffffffff11111111112222222222", + splTokenMintAddress: BraveWallet.BlockchainToken.mockSpdToken.contractAddress, + lamports: 0, + amount: 43210000, + txType: .solanaSplTokenTransfer, + instructions: [ + .init( + programId: "", + accountMetas: [.init(pubkey: "", isSigner: false, isWritable: false)], + data: []) + ], + send: .init(maxRetries: .init(maxRetries: 1), preflightCommitment: nil, skipPreflight: nil), + signTransactionParam: nil + ) + let transaction = BraveWallet.TransactionInfo( + id: "7", + fromAddress: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + txHash: "0xaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff1234", + txDataUnion: .init(solanaTxData: transactionData), + txStatus: .confirmed, + txType: .solanaSplTokenTransfer, + txParams: [], + txArgs: [ + ], + createdTime: Date(), + submittedTime: Date(), + confirmedTime: Date(), + originInfo: nil + ) + let expectedParsedTransaction = ParsedTransaction( + transaction: transaction, + namedFromAddress: "Solana Account 1", + fromAddress: "0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + namedToAddress: "Solana Account 2", + toAddress: "0xeeeeeeeeeeffffffffff11111111112222222222", + networkSymbol: "SOL", + details: .solSplTokenTransfer( + .init( + fromToken: .mockSpdToken, + fromValue: "43210000", + fromAmount: "43.21", + fromFiat: "$648.15", + gasFee: .init(fee: "0.0123", fiat: "$0.25") + ) + ) + ) + + guard let parsedTransaction = TransactionParser.parseTransaction( + transaction: transaction, + network: network, + accountInfos: accountInfos, + visibleTokens: tokens, + allTokens: tokens, + assetRatios: assetRatios, + solEstimatedTxFee: 12300000, + currencyFormatter: currencyFormatter + ) else { + XCTFail("Failed to parse solanaSplTokenTransfer transaction") + return + } + + XCTAssertEqual(expectedParsedTransaction, parsedTransaction) + } }