Skip to content

Commit

Permalink
Fix brave/brave-ios#8114: Support CoW Swap šŸ® orders in Safer Sign (brā€¦
Browse files Browse the repository at this point in the history
ā€¦ave/brave-ios#8533)

* Refactor `SwapTransactionConfirmationView` into `SaferSignTransactionContainerView` & `SaferSignTransactionView`, remove dependency on `ParsedTransaction` for re-use by Sign panel.

* Add SaferSignMessageRequestContainerView & `SignMessageRequestStore` for Safer Sign CoW swaps in Signature Requests.

* Refactor `SignMessageRequestContentView` out of `SignMessageRequestView` for re-use. Update `SaferSignMessageRequestContainerView` to use `SignMessageRequestContentView` for details view display.

* Add explorer url button to tokens in `SaferSignTransactionView`

* Address review comment; always show receiving address in safer sign even when it's same as from address/account.
  • Loading branch information
StephenHeaps committed Dec 12, 2023
1 parent a6f17a8 commit 92aa60d
Show file tree
Hide file tree
Showing 14 changed files with 1,195 additions and 537 deletions.
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
}
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

0 comments on commit 92aa60d

Please sign in to comment.