Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #7295: Show pending transactions from all networks (#7424)
Browse files Browse the repository at this point in the history
* Initial support for displaying & confirming pending transactions from all networks

* Add unit test to verify network for TransactionConfirmation. Sort transactions in confirmation by createdDate for deterministic order.

* Fetch transactions from known networks only until #7429
  • Loading branch information
StephenHeaps authored May 10, 2023
1 parent b48ac2c commit bb503f1
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 59 deletions.
8 changes: 4 additions & 4 deletions Sources/BraveWallet/Crypto/Stores/CryptoStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -375,12 +375,12 @@ public class CryptoStore: ObservableObject {
@MainActor
func fetchPendingTransactions() async -> [BraveWallet.TransactionInfo] {
let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes)
var selectedChainIdForCoinTypes: [BraveWallet.CoinType: [String]] = [:]
var allChainIdsForCoin: [BraveWallet.CoinType: [String]] = [:]
for coin in WalletConstants.supportedCoinTypes {
let selectedNetwork = await rpcService.network(coin, origin: nil)
selectedChainIdForCoinTypes[coin] = [selectedNetwork.chainId]
let allNetworks = await rpcService.allNetworks(coin)
allChainIdsForCoin[coin] = allNetworks.map(\.chainId)
}
return await txService.pendingTransactions(chainIdsForCoin: selectedChainIdForCoinTypes, for: allKeyrings)
return await txService.pendingTransactions(chainIdsForCoin: allChainIdsForCoin, for: allKeyrings)
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public class TransactionConfirmationStore: ObservableObject {
}
}
}
/// A cache of networks for the supported coin types.
private var allNetworks: [BraveWallet.NetworkInfo] = []

private let assetRatioService: BraveWalletAssetRatioService
private let rpcService: BraveWalletJsonRpcService
Expand Down Expand Up @@ -189,14 +191,15 @@ public class TransactionConfirmationStore: ObservableObject {
}

@MainActor func prepare() async {
allNetworks = await rpcService.allNetworksForSupportedCoins()
allTxs = await fetchAllTransactions()
if !unapprovedTxs.contains(where: { $0.id == activeTransactionId }) {
self.activeTransactionId = unapprovedTxs.first?.id ?? ""
}
let coinsForTransactions: Set<BraveWallet.CoinType> = .init(unapprovedTxs.map(\.coin))
for coin in coinsForTransactions {
let network = await rpcService.network(coin, origin: nil)
let userVisibleTokens = await walletService.userAssets(network.chainId, coin: coin)
let transactionNetworks: [BraveWallet.NetworkInfo] = Set(allTxs.map(\.chainId))
.compactMap { chainId in allNetworks.first(where: { $0.chainId == chainId }) }
for network in transactionNetworks {
let userVisibleTokens = await walletService.userAssets(network.chainId, coin: network.coin)
await fetchAssetRatios(for: userVisibleTokens)
}
await fetchUnknownTokens(for: unapprovedTxs)
Expand All @@ -213,7 +216,15 @@ public class TransactionConfirmationStore: ObservableObject {

let coin = transaction.coin
let keyring = await keyringService.keyringInfo(coin.keyringId)
let network = await rpcService.network(coin, origin: nil)
if !allNetworks.contains(where: { $0.chainId == transaction.chainId }) {
allNetworks = await rpcService.allNetworksForSupportedCoins()
}
guard let network = allNetworks.first(where: { $0.chainId == transaction.chainId }) else {
// Transactions should be removed if their network is removed
// https://github.com/brave/brave-browser/issues/30234
assertionFailure("The NetworkInfo for the transaction's chainId (\(transaction.chainId)) is unavailable")
return
}
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: coin) + tokenInfoCache.map(\.value)
let userVisibleTokens = await walletService.userAssets(network.chainId, coin: coin)
let solEstimatedTxFee: UInt64? = solEstimatedTxFeeCache[transaction.id]
Expand Down Expand Up @@ -326,11 +337,17 @@ public class TransactionConfirmationStore: ObservableObject {
@MainActor private func fetchUnknownTokens(
for transactions: [BraveWallet.TransactionInfo]
) async {
let coin = await walletService.selectedCoin()
let network = await rpcService.network(coin, origin: nil)
// `AssetRatioService` can only fetch tokens from Ethereum Mainnet
let mainnetTransactions = transactions.filter { $0.chainId == BraveWallet.MainnetChainId }
guard !mainnetTransactions.isEmpty else { return }
let coin: BraveWallet.CoinType = .eth
let allNetworks = await rpcService.allNetworks(coin)
guard let network = allNetworks.first(where: { $0.chainId == BraveWallet.MainnetChainId }) else {
return
}
let userVisibleTokens = await walletService.userAssets(network.chainId, coin: network.coin)
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: network.coin)
let unknownTokenContractAddresses = transactions.flatMap(\.tokenContractAddresses)
let unknownTokenContractAddresses = mainnetTransactions.flatMap(\.tokenContractAddresses)
.filter { contractAddress in
!userVisibleTokens.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
&& !allTokens.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
Expand Down Expand Up @@ -533,12 +550,13 @@ public class TransactionConfirmationStore: ObservableObject {

@MainActor private func fetchAllTransactions() async -> [BraveWallet.TransactionInfo] {
let allKeyrings = await keyringService.keyrings(for: WalletConstants.supportedCoinTypes)
var selectedChainIdForCoinTypes: [BraveWallet.CoinType: [String]] = [:]
var allChainIdsForCoin: [BraveWallet.CoinType: [String]] = [:]
for coin in WalletConstants.supportedCoinTypes {
let selectedNetwork = await rpcService.network(coin, origin: nil)
selectedChainIdForCoinTypes[coin] = [selectedNetwork.chainId]
let allNetworks = await rpcService.allNetworks(coin)
allChainIdsForCoin[coin] = allNetworks.map(\.chainId)
}
return await txService.pendingTransactions(chainIdsForCoin: selectedChainIdForCoinTypes, for: allKeyrings)
return await txService.pendingTransactions(chainIdsForCoin: allChainIdsForCoin, for: allKeyrings)
.sorted(by: { $0.createdTime < $1.createdTime })
}

func confirm(transaction: BraveWallet.TransactionInfo, completion: @escaping (_ error: String?) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ class TransactionDetailsStore: ObservableObject {
func update() {
Task { @MainActor in
let coin = transaction.coin
let network = await rpcService.network(coin, origin: nil)
let networksForCoin = await rpcService.allNetworks(coin)
guard let network = networksForCoin.first(where: { $0.chainId == transaction.chainId }) else {
// Transactions should be removed if their network is removed
// https://github.com/brave/brave-browser/issues/30234
assertionFailure("The NetworkInfo for the transaction's chainId (\(transaction.chainId)) is unavailable")
return
}
self.network = network
let keyring = await keyringService.keyringInfo(coin.keyringId)
var allTokens: [BraveWallet.BlockchainToken] = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) + tokenInfoCache.map(\.value)
Expand Down
30 changes: 0 additions & 30 deletions Sources/BraveWallet/Extensions/WalletTxServiceExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,6 @@ import BraveCore

extension BraveWalletTxService {

// Fetches all pending transactions for all given keyrings. This will return transactions from all networks.
func pendingTransactions(
for keyrings: [BraveWallet.KeyringInfo]
) async -> [BraveWallet.TransactionInfo] {
await allTransactions(for: keyrings).filter { $0.txStatus == .unapproved }
}

// Fetches all transactions for all given keyrings. This will return transactions from all networks.
func allTransactions(
for keyrings: [BraveWallet.KeyringInfo]
) async -> [BraveWallet.TransactionInfo] {
return await withTaskGroup(
of: [BraveWallet.TransactionInfo].self,
body: { @MainActor group in
for keyring in keyrings {
for info in keyring.accountInfos {
group.addTask { @MainActor in
await self.allTransactionInfo(info.coin, chainId: nil, from: info.address)
}
}
}
var allTx: [BraveWallet.TransactionInfo] = []
for await transactions in group {
allTx.append(contentsOf: transactions)
}
return allTx
}
)
}

// Fetches all pending transactions for all given keyrings
func pendingTransactions(
chainIdsForCoin: [BraveWallet.CoinType: [String]],
Expand Down
18 changes: 9 additions & 9 deletions Sources/BraveWallet/Preview Content/MockContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ extension BraveWallet.TransactionInfo {
txType: .erc20Approve,
txParams: ["address", "uint256"],
txArgs: ["0xe592427a0aece92de3edee1f18e0157c05861564Z", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],
createdTime: Date(timeIntervalSince1970: 1636399671),
submittedTime: Date(timeIntervalSince1970: 1636399673),
confirmedTime: Date(timeIntervalSince1970: 1636402508),
createdTime: Date(timeIntervalSince1970: 1636399671), // Monday, November 8, 2021 7:27:51 PM
submittedTime: Date(timeIntervalSince1970: 1636399673), // Monday, November 8, 2021 7:27:53 PM
confirmedTime: Date(timeIntervalSince1970: 1636402508), // Monday, November 8, 2021 8:15:08 PM
originInfo: .init(),
groupId: nil,
chainId: BraveWallet.MainnetChainId
Expand Down Expand Up @@ -283,13 +283,13 @@ extension BraveWallet.TransactionInfo {
confirmedTime: Date(timeIntervalSince1970: 1667854820), // Monday, November 7, 2022 9:00:20 PM GMT
originInfo: .init(),
groupId: nil,
chainId: BraveWallet.MainnetChainId
chainId: BraveWallet.SolanaMainnet
)
}
/// Solana Token Transfer
static var previewConfirmedSolTokenTransfer: BraveWallet.TransactionInfo {
BraveWallet.TransactionInfo(
id: "3d3c7715-f5f2-4f70-ab97-7fb8d3b2a3cd",
id: "12345675-f5f2-4f70-ab97-7fb8d3b2a3cd",
fromAddress: "6WRQXT2wMAkSjTjGQSqfEnuqgnqskTp5FnT28tScDsAd",
txHash: "",
txDataUnion: .init(
Expand All @@ -315,12 +315,12 @@ extension BraveWallet.TransactionInfo {
txType: .solanaSplTokenTransfer,
txParams: [],
txArgs: [],
createdTime: Date(timeIntervalSince1970: 1636399671),
submittedTime: Date(timeIntervalSince1970: 1636399673),
confirmedTime: Date(timeIntervalSince1970: 1636402508),
createdTime: Date(timeIntervalSince1970: 1636399671), // Monday, November 8, 2021 7:27:51 PM
submittedTime: Date(timeIntervalSince1970: 1636399673), // Monday, November 8, 2021 7:27:53 PM
confirmedTime: Date(timeIntervalSince1970: 1636402508), // Monday, November 8, 2021 8:15:08 PM
originInfo: .init(),
groupId: nil,
chainId: BraveWallet.MainnetChainId
chainId: BraveWallet.SolanaMainnet
)
}
static private func _transactionBase64ToData(_ base64String: String) -> [NSNumber] {
Expand Down
85 changes: 82 additions & 3 deletions Tests/BraveWalletTests/TransactionConfirmationStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import BraveCore
.eth : BraveWallet.NetworkInfo.mockMainnet,
.sol : BraveWallet.NetworkInfo.mockSolana
],
allNetworksForCoinType: [BraveWallet.CoinType: [BraveWallet.NetworkInfo]] = [
.eth: [.mockMainnet, .mockGoerli],
.sol: [.mockSolana, .mockSolanaTestnet]
],
accountInfos: [BraveWallet.AccountInfo] = [.mockEthAccount, .mockSolAccount],
allTokens: [BraveWallet.BlockchainToken] = [],
transactions: [BraveWallet.TransactionInfo] = [],
Expand All @@ -40,6 +44,9 @@ import BraveCore
rpcService._network = { coin, origin, completion in
completion(selectedNetworkForCoinType[coin] ?? .mockMainnet)
}
rpcService._allNetworks = { coin, completion in
completion(allNetworksForCoinType[coin] ?? [])
}
rpcService._balance = { _, _, _, completion in
completion(mockBalanceWei, .success, "")
}
Expand All @@ -48,8 +55,15 @@ import BraveCore
}
let txService = BraveWallet.TestTxService()
txService._addObserver = { _ in }
txService._allTransactionInfo = { _, _, _, completion in
completion(transactions)
txService._allTransactionInfo = { coin, chainId, address, completion in
let filteredTransactions = transactions.filter {
if let chainId = chainId {
return $0.coin == coin && $0.chainId == chainId
} else {
return $0.coin == coin
}
}
completion(filteredTransactions)
}
let blockchainRegistry = BraveWallet.TestBlockchainRegistry()
blockchainRegistry._allTokens = { _, _, completion in
Expand Down Expand Up @@ -79,7 +93,12 @@ import BraveCore
isKeyringCreated: true,
isLocked: false,
isBackedUp: true,
accountInfos: accountInfos)
accountInfos: [])
if id == BraveWallet.DefaultKeyringId {
keyring.accountInfos = accountInfos.filter { $0.coin == .eth }
} else {
keyring.accountInfos = accountInfos.filter { $0.coin == .sol }
}
completion(keyring)
}

Expand Down Expand Up @@ -277,6 +296,66 @@ import BraveCore
await fulfillment(of: [prepareExpectation], timeout: 1)
}

/// Test `network` property is updated for the `activeTransaction`, regardess of the current selected network for that coin type.
func testPrepareTransactionNotOnSelectedNetwork() async {
let firstTransactionDate = Date(timeIntervalSince1970: 1636399671) // Monday, November 8, 2021 7:27:51 PM
let sendCopy = BraveWallet.TransactionInfo.previewConfirmedSend.copy() as! BraveWallet.TransactionInfo
sendCopy.chainId = BraveWallet.GoerliChainId
sendCopy.txStatus = .unapproved
let swapCopy = BraveWallet.TransactionInfo.previewConfirmedSwap.copy() as! BraveWallet.TransactionInfo
swapCopy.chainId = BraveWallet.MainnetChainId
swapCopy.txStatus = .unapproved
let solanaSendCopy = BraveWallet.TransactionInfo.previewConfirmedSolSystemTransfer.copy() as! BraveWallet.TransactionInfo
solanaSendCopy.chainId = BraveWallet.SolanaMainnet
let solanaSPLSendCopy = BraveWallet.TransactionInfo.previewConfirmedSolTokenTransfer.copy() as! BraveWallet.TransactionInfo
solanaSPLSendCopy.chainId = BraveWallet.SolanaTestnet
let pendingTransactions: [BraveWallet.TransactionInfo] = [
sendCopy, swapCopy, solanaSendCopy, solanaSPLSendCopy
].enumerated().map { (index, tx) in
tx.txStatus = .unapproved
// transactions sorted by created time, make sure they are in-order
tx.createdTime = firstTransactionDate.addingTimeInterval(TimeInterval(index))
return tx
}
let allTokens: [BraveWallet.BlockchainToken] = [.previewToken, .daiToken]
let store = setupStore(
allTokens: allTokens,
transactions: pendingTransactions
)
let networkExpectation = expectation(description: "network-expectation")
store.$network
.dropFirst(6) // `network` is assigned multiple times during setup
.collect(4) // collect all transactions
.sink { networks in
defer { networkExpectation.fulfill() }
XCTAssertEqual(networks.count, 4)
XCTAssertEqual(networks[safe: 0], BraveWallet.NetworkInfo.mockGoerli)
XCTAssertEqual(networks[safe: 1], BraveWallet.NetworkInfo.mockMainnet)
XCTAssertEqual(networks[safe: 2], BraveWallet.NetworkInfo.mockSolana)
XCTAssertEqual(networks[safe: 3], BraveWallet.NetworkInfo.mockSolanaTestnet)
}
.store(in: &cancellables)
let activeTransactionIdExpectation = expectation(description: "activeTransactionId-expectation")
store.$activeTransactionId
.dropFirst()
.collect(4) // collect all transactions
.sink { activeTransactionIds in
defer { activeTransactionIdExpectation.fulfill() }
XCTAssertEqual(activeTransactionIds.count, 4)
XCTAssertEqual(activeTransactionIds[safe: 0], pendingTransactions[safe: 0]?.id)
XCTAssertEqual(activeTransactionIds[safe: 1], pendingTransactions[safe: 1]?.id)
XCTAssertEqual(activeTransactionIds[safe: 2], pendingTransactions[safe: 2]?.id)
XCTAssertEqual(activeTransactionIds[safe: 3], pendingTransactions[safe: 3]?.id)
}
.store(in: &cancellables)

await store.prepare() // `sendCopy` on Goerli Testnet
store.nextTransaction() // `swapCopy` on Ethereum Mainnet
store.nextTransaction() // `solanaSendCopy` on Solana Mainnet
store.nextTransaction() // `solanaSPLSendCopy` on Solana Testnet
await fulfillment(of: [networkExpectation, activeTransactionIdExpectation], timeout: 1)
}

/// Test `editAllowance(txMetaId:spenderAddress:amount:completion)` will return false if we fail to make ERC20 approve data with `BraveWalletEthTxManagerProxy`
func testEditAllowanceFailMakeERC20ApproveData() async {
let mockAllTokens: [BraveWallet.BlockchainToken] = [.previewToken, .daiToken]
Expand Down

0 comments on commit bb503f1

Please sign in to comment.