Skip to content

Commit

Permalink
Fix brave/brave-ios#8529: Update logic to support unknown tokens from…
Browse files Browse the repository at this point in the history
… non-mainnet transactions (brave/brave-ios#8530)

Update logic for fetching unknown tokens to support non-mainnet ethereum transactions. Add unknown token fetch to Activity tab.
  • Loading branch information
StephenHeaps authored Dec 8, 2023
1 parent 6b7b800 commit 10ac535
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 85 deletions.
26 changes: 11 additions & 15 deletions Sources/BraveWallet/Crypto/Stores/AccountActivityStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class AccountActivityStore: ObservableObject, WalletObserverStore {
private let assetManager: WalletUserAssetManagerType
/// Cache for storing `BlockchainToken`s that are not in user assets or our token registry.
/// This could occur with a dapp creating a transaction.
private var tokenInfoCache: [String: BraveWallet.BlockchainToken] = [:]
private var tokenInfoCache: [BraveWallet.BlockchainToken] = []

private var keyringServiceObserver: KeyringServiceObserver?
private var rpcServiceObserver: JsonRpcServiceObserver?
Expand Down Expand Up @@ -278,20 +278,16 @@ class AccountActivityStore: ObservableObject, WalletObserverStore {
if account.coin == .sol {
solEstimatedTxFees = await solTxManagerProxy.estimatedTxFees(for: transactions)
}
let unknownTokenContractAddresses = transactions
.flatMap { $0.tokenContractAddresses }
.filter { contractAddress in
!userAssets.contains(where: { $0.contractAddress.caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !allTokens.contains(where: { $0.contractAddress.caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !tokenInfoCache.keys.contains(where: { $0.caseInsensitiveCompare(contractAddress) == .orderedSame })
}
var allTokens = allTokens
if !unknownTokenContractAddresses.isEmpty {
let unknownTokens = await assetRatioService.fetchTokens(for: unknownTokenContractAddresses)
for unknownToken in unknownTokens {
tokenInfoCache[unknownToken.contractAddress] = unknownToken
let ethTransactions = transactions.filter { $0.coin == .eth }
if !ethTransactions.isEmpty {
// Gather known information about the transaction(s) tokens
let unknownTokenInfo = ethTransactions.unknownTokenContractAddressChainIdPairs(
knownTokens: userAssets + allTokens + tokenInfoCache
)
if !unknownTokenInfo.isEmpty {
let unknownTokens: [BraveWallet.BlockchainToken] = await rpcService.fetchEthTokens(for: unknownTokenInfo)
tokenInfoCache.append(contentsOf: unknownTokens)
}
allTokens.append(contentsOf: unknownTokens)
}
return transactions
.compactMap { transaction in
Expand All @@ -303,7 +299,7 @@ class AccountActivityStore: ObservableObject, WalletObserverStore {
network: network,
accountInfos: accountInfos,
userAssets: userAssets,
allTokens: allTokens,
allTokens: allTokens + tokenInfoCache,
assetRatios: assetRatios,
nftMetadata: [:],
solEstimatedTxFee: solEstimatedTxFees[transaction.id],
Expand Down
33 changes: 31 additions & 2 deletions Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
/// A list of tokens that are supported with the current selected network for all supported
/// on-ramp providers.
private var allBuyTokensAllOptions: [BraveWallet.OnRampProvider: [BraveWallet.BlockchainToken]] = [:]
/// Cache for storing `BlockchainToken`s that are not in user assets or our token registry.
/// This could occur with a dapp creating a transaction.
private var tokenInfoCache: [BraveWallet.BlockchainToken] = []
private var keyringServiceObserver: KeyringServiceObserver?
private var txServiceObserver: TxServiceObserver?
private var walletServiceObserver: WalletServiceObserver?
Expand Down Expand Up @@ -340,8 +343,19 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
})
}
var solEstimatedTxFees: [String: UInt64] = [:]
if token.coin == .sol {
switch token.coin {
case .eth:
let ethTransactions = allTransactions.filter { $0.coin == .eth }
if !ethTransactions.isEmpty { // we can only fetch unknown Ethereum tokens
let unknownTokenInfo = ethTransactions.unknownTokenContractAddressChainIdPairs(
knownTokens: userAssets + allTokens + tokenInfoCache
)
updateUnknownTokens(for: unknownTokenInfo)
}
case .sol:
solEstimatedTxFees = await solTxManagerProxy.estimatedTxFees(for: allTransactions)
default:
break
}
return allTransactions
.filter { tx in
Expand Down Expand Up @@ -378,7 +392,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
network: network,
accountInfos: accounts,
userAssets: userAssets,
allTokens: allTokens,
allTokens: allTokens + tokenInfoCache,
assetRatios: assetRatios,
nftMetadata: [:],
solEstimatedTxFee: solEstimatedTxFees[transaction.id],
Expand Down Expand Up @@ -420,6 +434,21 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
return false
}
}

private func updateUnknownTokens(
for contractAddressesChainIdPairs: [ContractAddressChainIdPair]
) {
guard !contractAddressesChainIdPairs.isEmpty else { return }
Task { @MainActor in
// Gather known information about the transaction(s) tokens
let unknownTokens: [BraveWallet.BlockchainToken] = await rpcService.fetchEthTokens(
for: contractAddressesChainIdPairs
)
guard !unknownTokens.isEmpty else { return }
tokenInfoCache.append(contentsOf: unknownTokens)
update()
}
}
}

extension AssetDetailStore: BraveWalletKeyringServiceObserver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
}
return
}
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: coin) + tokenInfoCache.map(\.value)
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: coin) + tokenInfoCache
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let solEstimatedTxFee: UInt64? = solEstimatedTxFeeCache[transaction.id]

Expand Down Expand Up @@ -393,7 +393,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
private var gasTokenBalanceCache: [String: Double] = [:]
/// Cache for storing `BlockchainToken`s that are not in user assets or our token registry.
/// This could occur with a dapp creating a transaction.
private var tokenInfoCache: [String: BraveWallet.BlockchainToken] = [:]
private var tokenInfoCache: [BraveWallet.BlockchainToken] = []
/// Cache for storing the estimated transaction fee for each Solana transaction. The key is the transaction id.
private var solEstimatedTxFeeCache: [String: UInt64] = [:]

Expand Down Expand Up @@ -444,27 +444,19 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
@MainActor private func fetchUnknownTokens(
for transactions: [BraveWallet.TransactionInfo]
) async {
// `AssetRatioService` can only fetch tokens from Ethereum Mainnet
let mainnetTransactions = transactions.filter { $0.chainId == BraveWallet.MainnetChainId }
guard !mainnetTransactions.isEmpty else { return }
let ethTransactions = transactions.filter { $0.coin == .eth }
guard !ethTransactions.isEmpty else { return } // we can only fetch unknown Ethereum tokens
let coin: BraveWallet.CoinType = .eth
let allNetworks = await rpcService.allNetworks(coin)
guard let network = allNetworks.first(where: { $0.chainId == BraveWallet.MainnetChainId }) else {
return
}
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: network.coin)
let unknownTokenContractAddresses = mainnetTransactions.flatMap(\.tokenContractAddresses)
.filter { contractAddress in
!userAssets.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !allTokens.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !tokenInfoCache.keys.contains(where: { $0.caseInsensitiveCompare(contractAddress) == .orderedSame })
}
guard !unknownTokenContractAddresses.isEmpty else { return }
let unknownTokens = await assetRatioService.fetchTokens(for: unknownTokenContractAddresses)
for unknownToken in unknownTokens {
tokenInfoCache[unknownToken.contractAddress] = unknownToken
}
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingUserDeleted: true).flatMap(\.tokens)
let allTokens = await blockchainRegistry.allTokens(in: allNetworks).flatMap(\.tokens)
// Gather known information about the transaction(s) tokens
let unknownTokenInfo = ethTransactions.unknownTokenContractAddressChainIdPairs(
knownTokens: userAssets + allTokens + tokenInfoCache
)
guard !unknownTokenInfo.isEmpty else { return } // Only if we have unknown tokens
let unknownTokens: [BraveWallet.BlockchainToken] = await rpcService.fetchEthTokens(for: unknownTokenInfo)
tokenInfoCache.append(contentsOf: unknownTokens)
updateTransaction(with: activeTransaction, shouldFetchCurrentAllowance: false, shouldFetchGasTokenBalance: false)
}

Expand Down
37 changes: 12 additions & 25 deletions Sources/BraveWallet/Crypto/Stores/TransactionDetailsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
private let assetManager: WalletUserAssetManagerType
/// Cache for storing `BlockchainToken`s that are not in user assets or our token registry.
/// This could occur with a dapp creating a transaction.
private var tokenInfoCache: [String: BraveWallet.BlockchainToken] = [:]
private var tokenInfoCache: [BraveWallet.BlockchainToken] = []
private var nftMetadataCache: [String: NFTMetadata] = [:]

var isObserving: Bool {
Expand Down Expand Up @@ -111,20 +111,17 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
return
}
self.network = network
var allTokens: [BraveWallet.BlockchainToken] = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) + tokenInfoCache.map(\.value)
let userAssets: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let unknownTokenContractAddresses = transaction.tokenContractAddresses
.filter { contractAddress in
!userAssets.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !allTokens.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !tokenInfoCache.keys.contains(where: { $0.caseInsensitiveCompare(contractAddress) == .orderedSame })
var allTokens: [BraveWallet.BlockchainToken] = await blockchainRegistry.allTokens(network.chainId, coin: network.coin)
let userAssets: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap(\.tokens)
if coin == .eth {
// Gather known information about the transaction(s) tokens
let unknownTokenInfo = [transaction].unknownTokenContractAddressChainIdPairs(
knownTokens: userAssets + allTokens + tokenInfoCache
)
if !unknownTokenInfo.isEmpty {
let unknownTokens: [BraveWallet.BlockchainToken] = await rpcService.fetchEthTokens(for: unknownTokenInfo)
tokenInfoCache.append(contentsOf: unknownTokens)
}
if !unknownTokenContractAddresses.isEmpty {
let unknownTokens = await assetRatioService.fetchTokens(for: unknownTokenContractAddresses)
for unknownToken in unknownTokens {
tokenInfoCache[unknownToken.contractAddress] = unknownToken
}
allTokens.append(contentsOf: unknownTokens)
}

let priceResult = await assetRatioService.priceWithIndividualRetry(
Expand All @@ -144,7 +141,7 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
network: network,
accountInfos: allAccounts,
userAssets: userAssets,
allTokens: allTokens,
allTokens: allTokens + tokenInfoCache,
assetRatios: assetRatios,
nftMetadata: nftMetadataCache,
solEstimatedTxFee: solEstimatedTxFee,
Expand Down Expand Up @@ -191,14 +188,4 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
self.parsedTransaction = parsedTransaction
}
}

@MainActor private func fetchTokenInfo(for contractAddress: String) async -> BraveWallet.BlockchainToken? {
if let cachedToken = tokenInfoCache[contractAddress] {
return cachedToken
}
let tokenInfo = await assetRatioService.tokenInfo(contractAddress)
guard let tokenInfo = tokenInfo else { return nil }
self.tokenInfoCache[contractAddress] = tokenInfo
return tokenInfo
}
}
34 changes: 31 additions & 3 deletions Sources/BraveWallet/Crypto/Stores/TransactionsActivityStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
private var assetPricesCache: [String: Double] = [:]
/// Cache of metadata for NFTs. The key is the token's `id`.
private var metadataCache: [String: NFTMetadata] = [:]
/// Cache for storing `BlockchainToken`s that are not in user assets or our token registry.
/// This could occur with a dapp creating a transaction.
private var tokenInfoCache: [BraveWallet.BlockchainToken] = []

private let keyringService: BraveWalletKeyringService
private let rpcService: BraveWalletJsonRpcService
Expand Down Expand Up @@ -147,10 +150,20 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
let allTransactions = await txService.allTransactions(
networksForCoin: networksForCoin, for: allAccountInfos
).filter { $0.txStatus != .rejected }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworksAllCoins, includingUserDeleted: true).flatMap(\.tokens)
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(
networks: allNetworksAllCoins,
includingUserDeleted: true
).flatMap(\.tokens)
let allTokens = await blockchainRegistry.allTokens(
in: allNetworksAllCoins
).flatMap(\.tokens)
let ethTransactions = allTransactions.filter { $0.coin == .eth }
if !ethTransactions.isEmpty { // we can only fetch unknown Ethereum tokens
let unknownTokenInfo = ethTransactions.unknownTokenContractAddressChainIdPairs(
knownTokens: userAssets + allTokens + tokenInfoCache
)
updateUnknownTokens(for: unknownTokenInfo)
}
guard !Task.isCancelled else { return }
// display transactions prior to network request to fetch
// estimated solana tx fees & asset prices
Expand All @@ -159,7 +172,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
networksForCoin: networksForCoin,
accountInfos: allAccountInfos,
userAssets: userAssets,
allTokens: allTokens,
allTokens: allTokens + tokenInfoCache,
assetRatios: assetPricesCache,
nftMetadata: metadataCache,
solEstimatedTxFees: solEstimatedTxFeesCache
Expand Down Expand Up @@ -254,7 +267,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
network: network,
accountInfos: accountInfos,
userAssets: userAssets,
allTokens: allTokens,
allTokens: allTokens + tokenInfoCache,
assetRatios: assetRatios,
nftMetadata: nftMetadata,
solEstimatedTxFee: solEstimatedTxFees[transaction.id],
Expand Down Expand Up @@ -287,6 +300,21 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
}
}

private func updateUnknownTokens(
for contractAddressesChainIdPairs: [ContractAddressChainIdPair]
) {
guard !contractAddressesChainIdPairs.isEmpty else { return }
Task { @MainActor in
// Gather known information about the transaction(s) tokens
let unknownTokens: [BraveWallet.BlockchainToken] = await rpcService.fetchEthTokens(
for: contractAddressesChainIdPairs
)
guard !unknownTokens.isEmpty else { return }
tokenInfoCache.append(contentsOf: unknownTokens)
update()
}
}

private var transactionDetailsStore: TransactionDetailsStore?
func transactionDetailsStore(
for transaction: BraveWallet.TransactionInfo
Expand Down
16 changes: 0 additions & 16 deletions Sources/BraveWallet/Extensions/AssetRatioServiceExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,4 @@ extension BraveWalletAssetRatioService {
let prices = Dictionary(uniqueKeysWithValues: priceResult.assetPrices.map { ($0.fromAsset, $0.price) })
return prices
}

/// Fetches the BlockchainToken for the given contract addresses. The token for a given contract
/// address is not guaranteed to be found, and will not be provided in the result if not found.
@MainActor func fetchTokens(
for contractAddresses: [String]
) async -> [BraveWallet.BlockchainToken] {
await withTaskGroup(of: [BraveWallet.BlockchainToken?].self) { @MainActor group in
for contractAddress in contractAddresses {
group.addTask { @MainActor in
let token = await self.tokenInfo(contractAddress)
return [token]
}
}
return await group.reduce([BraveWallet.BlockchainToken?](), { $0 + $1 })
}.compactMap { $0 }
}
}
Loading

0 comments on commit 10ac535

Please sign in to comment.