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

Fix #8114: Support CoW Swap 🐮 orders in Safer Sign #8533

Merged
merged 5 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Sources/BraveWallet/Crypto/Stores/CryptoStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,30 @@ public class CryptoStore: ObservableObject, WalletObserverStore {
}
}

private var signMessageRequestStore: SignMessageRequestStore?
func signMessageRequestStore(for requests: [BraveWallet.SignMessageRequest]) -> SignMessageRequestStore {
if let store = signMessageRequestStore {
DispatchQueue.main.async { // don't update in view body computation
store.requests = requests
nuo-xu marked this conversation as resolved.
Show resolved Hide resolved
}
return store
}
let store = SignMessageRequestStore(
requests: requests,
keyringService: keyringService,
rpcService: rpcService,
assetRatioService: assetRatioService,
blockchainRegistry: blockchainRegistry,
userAssetManager: userAssetManager
)
self.signMessageRequestStore = store
return store
}

func closeSignMessageRequestStore() {
self.signMessageRequestStore = nil
}

public private(set) lazy var settingsStore = SettingsStore(
keyringService: keyringService,
walletService: walletService,
Expand Down
183 changes: 183 additions & 0 deletions Sources/BraveWallet/Crypto/Stores/SignMessageRequestStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2023 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
import SwiftUI

class SignMessageRequestStore: ObservableObject {

@Published var requests: [BraveWallet.SignMessageRequest] {
didSet {
guard requests != oldValue else { return }
update()
}
}

/// The current request on display
var currentRequest: BraveWallet.SignMessageRequest {
requests[requestIndex]
}

/// Current request index
@Published var requestIndex: Int = 0
/// A map between request index and a boolean value indicates this request message needs pilcrow formating/
/// Key is the request id. This property is assigned by the view, because we need the view height to determine.
@Published var needPilcrowFormatted: [Int32: Bool] = [:]
/// A map between request index and a boolean value indicates this request message is displayed as
/// its original content. Key is the request id.
@Published var showOrignalMessage: [Int32: Bool] = [:]
/// EthSwapDetails for CoW swap requests. Key is the request id.
@Published var ethSwapDetails: [Int32: EthSwapDetails] = [:]

private let keyringService: BraveWalletKeyringService
private let rpcService: BraveWalletJsonRpcService
private let assetRatioService: BraveWalletAssetRatioService
private let blockchainRegistry: BraveWalletBlockchainRegistry
private let assetManager: WalletUserAssetManagerType

/// Cancellable for the last running `update()` Task.
private var updateTask: Task<(), Never>?
/// 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] = []

init(
requests: [BraveWallet.SignMessageRequest],
keyringService: BraveWalletKeyringService,
rpcService: BraveWalletJsonRpcService,
assetRatioService: BraveWalletAssetRatioService,
blockchainRegistry: BraveWalletBlockchainRegistry,
userAssetManager: WalletUserAssetManagerType
) {
self.requests = requests
self.keyringService = keyringService
self.rpcService = rpcService
self.assetRatioService = assetRatioService
self.blockchainRegistry = blockchainRegistry
self.assetManager = userAssetManager
}

func update() {
self.updateTask?.cancel()
self.updateTask = Task { @MainActor in
// setup default values
for request in requests {
if showOrignalMessage[request.id] == nil {
showOrignalMessage[request.id] = true
}
if needPilcrowFormatted[request.id] == nil {
needPilcrowFormatted[request.id] = false
}
}

let cowSwapRequests: [(id: Int32, cowSwapOrder: BraveWallet.CowSwapOrder, chainId: String)] = self.requests
.compactMap { request in
guard let cowSwapOrder = request.signData.ethSignTypedData?.meta?.cowSwapOrder else {
return nil
}
return (request.id, cowSwapOrder, request.chainId)
}
guard !cowSwapRequests.isEmpty else { return }

let allNetworks = await rpcService.allNetworksForSupportedCoins(respectTestnetPreference: false)
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(
networks: allNetworks,
includingUserDeleted: true
).flatMap(\.tokens)
let allTokens = await blockchainRegistry.allTokens(in: allNetworks).flatMap(\.tokens)

let findToken: (String, String) async -> BraveWallet.BlockchainToken? = { [tokenInfoCache] contractAddress, chainId in
userAssets.first(where: {
$0.contractAddress.caseInsensitiveCompare(contractAddress) == .orderedSame
&& $0.chainId.caseInsensitiveCompare(chainId) == .orderedSame
}) ?? allTokens.first(where: {
$0.contractAddress.caseInsensitiveCompare(contractAddress) == .orderedSame
&& $0.chainId.caseInsensitiveCompare(chainId) == .orderedSame
}) ?? tokenInfoCache.first(where: {
$0.contractAddress.caseInsensitiveCompare(contractAddress) == .orderedSame
&& $0.chainId.caseInsensitiveCompare(chainId) == .orderedSame
})
}

// Gather unknown token info to fetch if needed.
var unknownTokenPairs: Set<ContractAddressChainIdPair> = .init()

for cowSwapRequest in cowSwapRequests {
let requestId = cowSwapRequest.id
let cowSwapOrder = cowSwapRequest.cowSwapOrder
let chainId = cowSwapRequest.chainId
guard let network = allNetworks.first(where: { $0.chainId.caseInsensitiveCompare(chainId) == .orderedSame }) else {
return
}

let formatter = WeiFormatter(decimalFormatStyle: .decimals(precision: Int(network.decimals)))

let fromToken: BraveWallet.BlockchainToken? = await findToken(cowSwapOrder.sellToken, chainId)
let fromTokenDecimals = Int(fromToken?.decimals ?? network.decimals)
if fromToken == nil {
unknownTokenPairs.insert(.init(contractAddress: cowSwapOrder.sellToken, chainId: chainId))
}

let toToken: BraveWallet.BlockchainToken? = await findToken(cowSwapOrder.buyToken, chainId)
let toTokenDecimals = Int(toToken?.decimals ?? network.decimals)
if toToken == nil {
unknownTokenPairs.insert(.init(contractAddress: cowSwapOrder.buyToken, chainId: chainId))
}

let formattedSellAmount = formatter.decimalString(for: cowSwapOrder.sellAmount, radix: .decimal, decimals: fromTokenDecimals)?.trimmingTrailingZeros ?? ""
let formattedMinBuyAmount = formatter.decimalString(for: cowSwapOrder.buyAmount, radix: .decimal, decimals: toTokenDecimals)?.trimmingTrailingZeros ?? ""

let details = EthSwapDetails(
fromToken: fromToken,
fromValue: cowSwapOrder.sellAmount,
fromAmount: formattedSellAmount,
fromFiat: nil, // not required for display
toToken: toToken,
minBuyValue: cowSwapOrder.buyToken,
minBuyAmount: formattedMinBuyAmount,
minBuyAmountFiat: nil, // not required for display
gasFee: nil // sign request, no gas fee
)
self.ethSwapDetails[requestId] = details
}
if !unknownTokenPairs.isEmpty {
fetchUnknownTokens(Array(unknownTokenPairs))
}
}
}

/// Advance to the next (or first if displaying the last) sign message request.
func next() {
if requestIndex + 1 < requests.count {
if let nextRequestId = requests[safe: requestIndex + 1]?.id,
showOrignalMessage[nextRequestId] == nil {
// if we have not previously assigned a `showOriginalMessage`
// value for the next request, assign it the default value now.
showOrignalMessage[nextRequestId] = true
}
requestIndex = requestIndex + 1
} else {
requestIndex = 0
}
}

private func fetchUnknownTokens(_ pairs: [ContractAddressChainIdPair]) {
Task { @MainActor in
// filter out tokens we have already fetched
let filteredPairs = pairs.filter { pair in
!tokenInfoCache.contains(where: {
$0.contractAddress.caseInsensitiveCompare(pair.contractAddress) != .orderedSame
&& $0.chainId.caseInsensitiveCompare(pair.chainId) != .orderedSame
})
}
guard !filteredPairs.isEmpty else {
return
}
let tokens = await rpcService.fetchEthTokens(for: pairs)
tokenInfoCache.append(contentsOf: tokens)
update()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,8 @@ struct PendingTransactionView: View {

// Current Active Transaction info
if confirmationStore.activeParsedTransaction.transaction.txType == .ethSwap {
SwapTransactionConfirmationView(
SaferSignTransactionContainerView(
parsedTransaction: confirmationStore.activeParsedTransaction,
network: confirmationStore.network ?? .init(),
editGasFeeTapped: {
isShowingGas = true
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2023 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 SwiftUI
import BraveCore
import BigNumber
import Strings
import DesignSystem

struct SaferSignTransactionContainerView: View {
/// The OriginInfo that created the transaction
let originInfo: BraveWallet.OriginInfo?
/// The network the transaction belongs to
let network: BraveWallet.NetworkInfo?

/// The address of the account making the swap
let fromAddress: String?
/// The name of the account
let namedFromAddress: String?

/// The token being swapped from.
let fromToken: BraveWallet.BlockchainToken?
/// The amount of the `tokToken` being swapped.
let fromAmount: String?

/// The token being swapped for.
let toToken: BraveWallet.BlockchainToken?
/// Minimum amount being bought of the `toToken`.
let minBuyAmount: String?
/// The gas fee for the transaction
let gasFee: GasFee?

let editGasFeeTapped: () -> Void
let advancedSettingsTapped: () -> Void

@Environment(\.pixelLength) private var pixelLength
@ScaledMetric private var faviconSize = 48
private let maxFaviconSize: CGFloat = 72
@ScaledMetric private var assetNetworkIconSize: CGFloat = 15
private let maxAssetNetworkIconSize: CGFloat = 20

init(
parsedTransaction: ParsedTransaction,
editGasFeeTapped: @escaping () -> Void,
advancedSettingsTapped: @escaping () -> Void
) {
self.originInfo = parsedTransaction.transaction.originInfo
self.network = parsedTransaction.network
self.fromAddress = parsedTransaction.fromAddress
self.namedFromAddress = parsedTransaction.namedFromAddress
if case .ethSwap(let details) = parsedTransaction.details {
self.fromToken = details.fromToken
self.fromAmount = details.fromAmount
self.toToken = details.toToken
self.minBuyAmount = details.minBuyAmount
} else {
self.fromToken = nil
self.fromAmount = nil
self.toToken = nil
self.minBuyAmount = nil
}
self.gasFee = parsedTransaction.gasFee
self.editGasFeeTapped = editGasFeeTapped
self.advancedSettingsTapped = advancedSettingsTapped
}

var body: some View {
VStack {
originAndFavicon

SaferSignTransactionView(
network: network,
fromAddress: fromAddress,
namedFromAddress: namedFromAddress,
receiverAddress: nil,
namedReceiverAddress: nil,
fromToken: fromToken,
fromTokenContractAddress: fromToken?.contractAddress,
fromAmount: fromAmount,
toToken: toToken,
toTokenContractAddress: toToken?.contractAddress,
minBuyAmount: minBuyAmount
)

networkFeeSection
}
}

private var originAndFavicon: some View {
VStack {
if let originInfo = originInfo {
Group {
if originInfo.isBraveWalletOrigin {
Image(uiImage: UIImage(sharedNamed: "brave.logo")!)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(Color(.braveOrange))
} else {
if let url = URL(string: originInfo.originSpec) {
FaviconReader(url: url) { image in
if let image = image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else {
Circle()
.stroke(Color(.braveSeparator), lineWidth: pixelLength)
}
}
.background(Color(.braveDisabled))
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}
}
.frame(width: min(faviconSize, maxFaviconSize), height: min(faviconSize, maxFaviconSize))

Text(originInfo: originInfo)
.foregroundColor(Color(.braveLabel))
.font(.subheadline)
.multilineTextAlignment(.center)
.padding(.top, 8)
}
}
}

private var networkFeeSection: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(Strings.Wallet.swapConfirmationNetworkFee)
.fontWeight(.medium)
.foregroundColor(Color(.secondaryBraveLabel))
Spacer()
Button(action: advancedSettingsTapped) {
Image(systemName: "gearshape")
.foregroundColor(Color(.secondaryBraveLabel))
}
.buttonStyle(.plain)
}
HStack {
Group {
if let image = network?.nativeTokenLogoImage {
Image(uiImage: image)
.resizable()
} else {
Circle()
.stroke(Color(.braveSeparator))
}
}
.frame(width: min(assetNetworkIconSize, maxAssetNetworkIconSize), height: min(assetNetworkIconSize, maxAssetNetworkIconSize))
Text(gasFee?.fiat ?? "")
.foregroundColor(Color(.braveLabel))
Button(action: editGasFeeTapped) {
Text(Strings.Wallet.editGasFeeButtonTitle)
.fontWeight(.semibold)
.foregroundColor(Color(.braveBlurpleTint))
}
Spacer()
}
}
.frame(maxWidth: .infinity)
}
}
Loading