diff --git a/Features/Swap/Sources/Scenes/SwapScene.swift b/Features/Swap/Sources/Scenes/SwapScene.swift index c7e372865..31f746c7d 100644 --- a/Features/Swap/Sources/Scenes/SwapScene.swift +++ b/Features/Swap/Sources/Scenes/SwapScene.swift @@ -94,7 +94,8 @@ extension SwapScene { } footer: { SwapChangeView( fromId: $model.pairSelectorModel.fromAssetId, - toId: $model.pairSelectorModel.toAssetId + toId: $model.pairSelectorModel.toAssetId, + isLoading: model.isLoading ) .padding(.top, .small) .frame(maxWidth: .infinity) @@ -110,7 +111,7 @@ extension SwapScene { SwapTokenView( model: model.swapTokenModel(type: .receive(chains: [], assetIds: [])), text: $model.toValue, - showLoading: model.isLoading, + isLoading: model.isLoading, disabledTextField: true, onBalanceAction: {}, onSelectAssetAction: model.onSelectAssetReceive diff --git a/Features/Swap/Sources/Types/SwapFetchTrigger.swift b/Features/Swap/Sources/Types/SwapFetchTrigger.swift index ba585e0dd..7c8f324d6 100644 --- a/Features/Swap/Sources/Types/SwapFetchTrigger.swift +++ b/Features/Swap/Sources/Types/SwapFetchTrigger.swift @@ -1,6 +1,7 @@ // Copyright (c). Gem Wallet. All rights reserved. import Components +import SwapService struct SwapFetchTrigger: DebouncableTrigger { let input: SwapQuoteInput diff --git a/Features/Swap/Sources/Types/SwapQuoteInput.swift b/Features/Swap/Sources/Types/SwapQuoteInput.swift index b39486b73..ba62f8601 100644 --- a/Features/Swap/Sources/Types/SwapQuoteInput.swift +++ b/Features/Swap/Sources/Types/SwapQuoteInput.swift @@ -1,17 +1,8 @@ // Copyright (c). Gem Wallet. All rights reserved. -import Foundation -import Primitives import BigInt - -public struct SwapQuoteInput: Hashable, Sendable { - public let fromAsset: Asset - public let toAsset: Asset - public let value: BigInt - public let useMaxAmount: Bool -} - -// MARK: - Identifiable +import Primitives +import SwapService extension SwapQuoteInput: Identifiable { public var id: String { diff --git a/Features/Swap/Sources/ViewModels/SwapSceneViewModel.swift b/Features/Swap/Sources/ViewModels/SwapSceneViewModel.swift index 9a4ce2337..f9bfa87da 100644 --- a/Features/Swap/Sources/ViewModels/SwapSceneViewModel.swift +++ b/Features/Swap/Sources/ViewModels/SwapSceneViewModel.swift @@ -15,6 +15,7 @@ import BalanceService import PriceService import enum Gemstone.SwapperError import struct Gemstone.SwapperQuote +import struct Gemstone.SwapperProviderType import Formatters import Validators @@ -30,6 +31,8 @@ public final class SwapSceneViewModel { public var swapState: SwapState = .init() public var isPresentingInfoSheet: SwapSheetType? + private(set) var isStreaming: Bool = false + public let fromAssetQuery: ObservableQuery public let toAssetQuery: ObservableQuery @@ -113,7 +116,7 @@ public final class SwapSceneViewModel { } var isLoading: Bool { - swapState.quotes.isLoading + swapState.quotes.isLoading || isStreaming } var assetIds: [AssetId] { @@ -339,21 +342,36 @@ extension SwapSceneViewModel { } private func performFetch(input: SwapQuoteInput) async { do { + let preferredProvider = selectedSwapQuote?.data.provider swapState.swapTransferData = .noData swapState.quotes = .loading + isStreaming = true resetToValue() - let swapQuotes = try await swapQuotesProvider.fetchQuotes( - wallet: wallet, - fromAsset: input.fromAsset, - toAsset: input.toAsset, - amount: input.value, - useMaxAmount: input.useMaxAmount - ) - swapState.quotes = .data(swapQuotes) - selectedSwapQuote = swapQuotes.first(where: { $0 == selectedSwapQuote }) ?? swapQuotes.first - if let selectedSwapQuote, let asset = toAsset?.asset { - applyQuote(selectedSwapQuote, asset: asset) + var quotes: [SwapperQuote] = [] + var errors: [Error] = [] + + for await result in swapQuotesProvider.fetchQuotes(wallet: wallet, input: input) { + try Task.checkCancellation() + + switch result { + case .success(let quote): + quotes.append(quote) + applyQuotes("es) + case .failure(let error): + errors.append(error) + } + } + + try Task.checkCancellation() + + if quotes.isEmpty { + let swapError = minInputAmountError(errors) ?? SwapperError.NoQuoteAvailable + swapState.quotes = .error(swapError) + selectedSwapQuote = nil + amountInputModel.update(error: nil) + } else { + selectedSwapQuote = quotes.first(where: { $0.data.provider == preferredProvider }) ?? quotes.first } } catch { if !error.isCancelled && !Task.isCancelled { @@ -363,6 +381,25 @@ extension SwapSceneViewModel { debugLog("SwapScene get quotes error: \(error)") } } + isStreaming = false + } + + private func applyQuotes(_ quotes: inout [SwapperQuote]) { + quotes.sort { BigInt.fromString($0.toValue) > BigInt.fromString($1.toValue) } + swapState.quotes = .data(quotes) + selectedSwapQuote = quotes.first + if let selectedSwapQuote, let asset = toAsset?.asset { + applyQuote(selectedSwapQuote, asset: asset) + } + } + + private func minInputAmountError(_ errors: [Error]) -> SwapperError? { + let minAmounts: [BigInt] = errors.compactMap { error in + guard case .InputAmountError(let minAmount) = error as? SwapperError else { return nil } + return minAmount.flatMap { BigInt($0) } + } + guard let min = minAmounts.min() else { return nil } + return .InputAmountError(minAmount: String(min.increase(byPercent: 10))) } private func performUpdate(for assetIds: [AssetId]) async { diff --git a/Features/Swap/Sources/Views/SwapChangeView.swift b/Features/Swap/Sources/Views/SwapChangeView.swift index aaa52f2b8..7cb62cc5c 100644 --- a/Features/Swap/Sources/Views/SwapChangeView.swift +++ b/Features/Swap/Sources/Views/SwapChangeView.swift @@ -2,31 +2,40 @@ import Foundation import SwiftUI +import Components import Style import Primitives struct SwapChangeView: View { @Binding private var fromId: AssetId? @Binding private var toId: AssetId? + var isLoading: Bool init( fromId: Binding = .constant(.none), - toId: Binding = .constant(.none) + toId: Binding = .constant(.none), + isLoading: Bool = false ) { _fromId = fromId _toId = toId + self.isLoading = isLoading } var body: some View { - Button { - swap(&fromId, &toId) - } label: { - Images.System.arrowSwap - .resizable() - .renderingMode(.template) - .scaledToFit() + if isLoading { + LoadingView(tint: Colors.gray) .frame(size: .large) - .foregroundStyle(Colors.gray) + } else { + Button { + swap(&fromId, &toId) + } label: { + Images.System.arrowSwap + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(size: .large) + .foregroundStyle(Colors.gray) + } } } } diff --git a/Features/Swap/Sources/Views/SwapTokenView.swift b/Features/Swap/Sources/Views/SwapTokenView.swift index 53cdd3f4e..d4e3fbdab 100644 --- a/Features/Swap/Sources/Views/SwapTokenView.swift +++ b/Features/Swap/Sources/Views/SwapTokenView.swift @@ -8,7 +8,7 @@ import Primitives struct SwapTokenView: View { let model: SwapTokenViewModel @Binding var text: String - var showLoading: Bool = false + var isLoading: Bool = false var disabledTextField: Bool = false var onBalanceAction: (() -> Void) var onSelectAssetAction: (() -> Void) @@ -26,19 +26,14 @@ struct SwapTokenView: View { } } } - + private var inputView: some View { - HStack { - if showLoading { - LoadingView() - } - TextField(showLoading ? "" : String.zero, text: $text) - .keyboardType(.decimalPad) - .foregroundStyle(Colors.black) - .font(.app.title1) - .disabled(disabledTextField) - .multilineTextAlignment(.leading) - } + TextField(String.zero, text: $text) + .keyboardType(.decimalPad) + .foregroundStyle(isLoading ? Colors.gray : Colors.black) + .font(.app.title1) + .disabled(disabledTextField) + .multilineTextAlignment(.leading) } private var fiatBalanceView: some View { diff --git a/Features/Swap/Tests/SwapTests/SwapQuoteInputTests.swift b/Features/Swap/Tests/SwapTests/SwapQuoteInputTests.swift index ac25001d4..7c5969aa4 100644 --- a/Features/Swap/Tests/SwapTests/SwapQuoteInputTests.swift +++ b/Features/Swap/Tests/SwapTests/SwapQuoteInputTests.swift @@ -4,6 +4,7 @@ import Testing import BigInt import Primitives import PrimitivesTestKit +import SwapService @testable import Swap struct SwapQuoteInputTests { diff --git a/Features/Swap/Tests/SwapTests/SwapSceneViewModelTests.swift b/Features/Swap/Tests/SwapTests/SwapSceneViewModelTests.swift index 1597c2545..fd37b449a 100644 --- a/Features/Swap/Tests/SwapTests/SwapSceneViewModelTests.swift +++ b/Features/Swap/Tests/SwapTests/SwapSceneViewModelTests.swift @@ -11,6 +11,8 @@ import SwapServiceTestKit import BigInt import protocol Gemstone.GemSwapperProtocol import enum Gemstone.SwapperError +import struct Gemstone.SwapperQuote +import struct Gemstone.SwapperProviderType import Keystore import KeystoreTestKit import Primitives @@ -132,12 +134,42 @@ struct SwapSceneViewModelTests { #expect(model.fetchTrigger?.isImmediate == true) } + @Test + func preservesProviderSelectionOnRefresh() async { + let quoteA = SwapperQuote.mock(toValue: "100", data: .mock(provider: .mock(id: .uniswapV3))) + let quoteB = SwapperQuote.mock(toValue: "200", data: .mock(provider: .mock(id: .thorchain))) + let model = SwapSceneViewModel.mock(quotesProvider: SwapQuotesProviderMock(results: [.success(quoteA), .success(quoteB)])) + + await model.fetch() + model.onFinishSwapProviderSelection(quoteA) + + #expect(model.selectedSwapQuote?.data.provider.id == .uniswapV3) + + await model.fetch() + + #expect(model.selectedSwapQuote?.data.provider.id == .uniswapV3) + } + + @Test + func minAmountErrorPicksSmallest() async { + let model = SwapSceneViewModel.mock(quotesProvider: SwapQuotesProviderMock(results: [ + .failure(SwapperError.InputAmountError(minAmount: "2000")), + .failure(SwapperError.InputAmountError(minAmount: "1000")), + ])) + + await model.fetch() + + #expect(model.buttonViewModel.buttonAction == .useMinAmount(amount: "1100", asset: .mockEthereum())) + } + // MARK: - Private methods private func model( toValueMock: String = "250000000000" ) async -> SwapSceneViewModel { - let swapper = GemSwapperMock(quotes: [.mock(toValue: toValueMock)]) + let swapper = GemSwapperMock( + quoteByProvider: .mock(toValue: toValueMock) + ) let model = SwapSceneViewModel.mock(swapper: swapper) await model.fetch() return model @@ -145,7 +177,7 @@ struct SwapSceneViewModelTests { } extension SwapSceneViewModel { - static func mock(swapper: GemSwapperProtocol = GemSwapperMock()) -> SwapSceneViewModel { + static func mock(swapper: GemSwapperProtocol = GemSwapperMock(), quotesProvider: SwapQuotesProvidable? = nil) -> SwapSceneViewModel { let model = SwapSceneViewModel( preferences: .mock(), input: .init( @@ -154,7 +186,7 @@ extension SwapSceneViewModel { ), balanceUpdater: .mock(), priceUpdater: .mock(), - swapQuotesProvider: SwapQuotesProvider(swapService: .mock(swapper: swapper)), + swapQuotesProvider: quotesProvider ?? SwapQuotesProvider(swapService: .mock(swapper: swapper)), swapQuoteDataProvider: SwapQuoteDataProvider(keystore: LocalKeystore.mock(), swapService: .mock(swapper: swapper)) ) model.fromAssetQuery.value = .mock(asset: .mockEthereum(), balance: .mock()) @@ -168,3 +200,4 @@ extension SwapSceneViewModel { private struct TestError: Error, RetryableError { var isRetryAvailable: Bool = true } + diff --git a/Packages/FeatureServices/SwapService/SwapQuoteInput.swift b/Packages/FeatureServices/SwapService/SwapQuoteInput.swift new file mode 100644 index 000000000..3af123cab --- /dev/null +++ b/Packages/FeatureServices/SwapService/SwapQuoteInput.swift @@ -0,0 +1,18 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import BigInt +import Primitives + +public struct SwapQuoteInput: Hashable, Sendable { + public let fromAsset: Asset + public let toAsset: Asset + public let value: BigInt + public let useMaxAmount: Bool + + public init(fromAsset: Asset, toAsset: Asset, value: BigInt, useMaxAmount: Bool) { + self.fromAsset = fromAsset + self.toAsset = toAsset + self.value = value + self.useMaxAmount = useMaxAmount + } +} diff --git a/Packages/FeatureServices/SwapService/SwapQuotesProvider.swift b/Packages/FeatureServices/SwapService/SwapQuotesProvider.swift index b00fc93c2..27176540d 100644 --- a/Packages/FeatureServices/SwapService/SwapQuotesProvider.swift +++ b/Packages/FeatureServices/SwapService/SwapQuotesProvider.swift @@ -5,10 +5,11 @@ import BigInt import Primitives import struct Gemstone.SwapperQuote +import struct Gemstone.SwapperProviderType public protocol SwapQuotesProvidable: Sendable { func supportedAssets(for assetId: AssetId) -> ([Primitives.Chain], [Primitives.AssetId]) - func fetchQuotes(wallet: Wallet, fromAsset: Asset, toAsset: Asset, amount: BigInt, useMaxAmount: Bool) async throws -> [Gemstone.SwapperQuote] + func fetchQuotes(wallet: Wallet, input: SwapQuoteInput) -> AsyncStream> } public struct SwapQuotesProvider: SwapQuotesProvidable { @@ -22,17 +23,44 @@ public struct SwapQuotesProvider: SwapQuotesProvidable { swapService.supportedAssets(for: assetId) } - public func fetchQuotes(wallet: Wallet, fromAsset: Asset, toAsset: Asset, amount: BigInt, useMaxAmount: Bool) async throws -> [Gemstone.SwapperQuote] { - let walletAddress = try wallet.account(for: fromAsset.chain).address - let destinationAddress = try wallet.account(for: toAsset.chain).address - let quotes = try await swapService.getQuotes( - fromAsset: fromAsset, - toAsset: toAsset, - value: amount.description, - walletAddress: walletAddress, - destinationAddress: destinationAddress, - useMaxAmount: useMaxAmount - ) - return try quotes.sorted { try BigInt.from(string: $0.toValue) > BigInt.from(string: $1.toValue) } + public func fetchQuotes(wallet: Wallet, input: SwapQuoteInput) -> AsyncStream> { + AsyncStream { continuation in + let task = Task { + do { + let walletAddress = try wallet.account(for: input.fromAsset.chain).address + let destinationAddress = try wallet.account(for: input.toAsset.chain).address + let providers = try swapService.getProvidersForQuote(input: input, walletAddress: walletAddress, destinationAddress: destinationAddress) + await fetchFromProviders(providers, input: input, walletAddress: walletAddress, destinationAddress: destinationAddress, continuation: continuation) + } catch { + continuation.yield(.failure(error)) + } + continuation.finish() + } + continuation.onTermination = { _ in + task.cancel() + } + } + } + + private func fetchFromProviders(_ providers: [SwapperProviderType], input: SwapQuoteInput, walletAddress: String, destinationAddress: String, continuation: AsyncStream>.Continuation) async { + await withTaskGroup(of: Result?.self) { group in + for provider in providers { + group.addTask { [swapService] in + guard !Task.isCancelled else { return nil } + do { + let quote = try await swapService.getQuoteByProvider(provider: provider.id, input: input, walletAddress: walletAddress, destinationAddress: destinationAddress) + return .success(quote) + } catch { + guard !Task.isCancelled else { return nil } + return .failure(error) + } + } + } + for await result in group { + if let result { + continuation.yield(result) + } + } + } } } diff --git a/Packages/FeatureServices/SwapService/SwapService.swift b/Packages/FeatureServices/SwapService/SwapService.swift index 6c1405ad2..bcea1f637 100644 --- a/Packages/FeatureServices/SwapService/SwapService.swift +++ b/Packages/FeatureServices/SwapService/SwapService.swift @@ -18,6 +18,8 @@ import GemstonePrimitives import NativeProviderService import Primitives import enum Primitives.AnyError +import enum Gemstone.SwapperProvider +import struct Gemstone.SwapperProviderType import enum Primitives.Chain import enum Primitives.EVMChain @@ -51,24 +53,33 @@ public final class SwapService: Sendable, SwappableChainsProvider { ) } - public func getQuotes(fromAsset: Asset, toAsset: Asset, value: String, walletAddress: String, destinationAddress: String, useMaxAmount: Bool) async throws -> [SwapperQuote] { - let swapRequest = SwapperQuoteRequest( - fromAsset: SwapperQuoteAsset(asset: fromAsset), - toAsset: SwapperQuoteAsset(asset: toAsset), + public func getProvidersForQuote(input: SwapQuoteInput, walletAddress: String, destinationAddress: String) throws -> [SwapperProviderType] { + let request = buildRequest(input: input, walletAddress: walletAddress, destinationAddress: destinationAddress) + return try swapper.getProvidersForRequest(request: request) + } + + public func getQuoteByProvider(provider: SwapperProvider, input: SwapQuoteInput, walletAddress: String, destinationAddress: String) async throws -> SwapperQuote { + let request = buildRequest(input: input, walletAddress: walletAddress, destinationAddress: destinationAddress) + let quote = try await swapper.getQuoteByProvider(provider: provider, request: request) + try Task.checkCancellation() + return quote + } + + private func buildRequest(input: SwapQuoteInput, walletAddress: String, destinationAddress: String) -> SwapperQuoteRequest { + SwapperQuoteRequest( + fromAsset: SwapperQuoteAsset(asset: input.fromAsset), + toAsset: SwapperQuoteAsset(asset: input.toAsset), walletAddress: walletAddress, destinationAddress: destinationAddress, - value: value, + value: input.value.description, mode: .exactIn, options: SwapperOptions( - slippage: getDefaultSlippage(chain: fromAsset.id.chain.rawValue), + slippage: getDefaultSlippage(chain: input.fromAsset.id.chain.rawValue), fee: getReferralFees(), preferredProviders: [], - useMaxAmount: useMaxAmount + useMaxAmount: input.useMaxAmount ) ) - let quotes = try await swapper.getQuote(request: swapRequest) - try Task.checkCancellation() - return quotes } public func getQuoteData(_ request: SwapperQuote, data: FetchQuoteData) async throws -> GemSwapQuoteData { diff --git a/Packages/FeatureServices/SwapService/TestKit/GemSwapperMock.swift b/Packages/FeatureServices/SwapService/TestKit/GemSwapperMock.swift index 22b848ba4..d8a15e770 100644 --- a/Packages/FeatureServices/SwapService/TestKit/GemSwapperMock.swift +++ b/Packages/FeatureServices/SwapService/TestKit/GemSwapperMock.swift @@ -15,7 +15,6 @@ import struct Gemstone.SwapperSwapResult public final class GemSwapperMock: GemSwapperProtocol { private let permit2ForQuote: Permit2ApprovalData - private let quotes: [SwapperQuote] private let quoteByProvider: SwapperQuote private let quoteData: GemSwapQuoteData private let providers: [SwapperProviderType] @@ -27,7 +26,6 @@ public final class GemSwapperMock: GemSwapperProtocol { public init( permit2ForQuote: Permit2ApprovalData = .mock(), - quotes: [SwapperQuote] = [.mock()], quoteByProvider: SwapperQuote = .mock(), quoteData: GemSwapQuoteData = .mock(), providers: [SwapperProviderType] = [.mock()], @@ -38,7 +36,6 @@ public final class GemSwapperMock: GemSwapperProtocol { fetchQuoteError: Error? = nil ) { self.permit2ForQuote = permit2ForQuote - self.quotes = quotes self.quoteByProvider = quoteByProvider self.quoteData = quoteData self.providers = providers @@ -60,11 +57,17 @@ public final class GemSwapperMock: GemSwapperProtocol { if let error = fetchQuoteError { throw error } - return quotes + return [quoteByProvider] } public func getQuoteByProvider(provider: SwapperProvider, request: SwapperQuoteRequest) async throws -> SwapperQuote { - quoteByProvider + if let delay = fetchQuoteDelay { + try await Task.sleep(for: delay) + } + if let error = fetchQuoteError { + throw error + } + return quoteByProvider } public func getQuoteData(quote: SwapperQuote, data: FetchQuoteData) async throws -> GemSwapQuoteData { diff --git a/Packages/FeatureServices/SwapService/TestKit/SwapQuotesProviderMock.swift b/Packages/FeatureServices/SwapService/TestKit/SwapQuotesProviderMock.swift new file mode 100644 index 000000000..e65c6b40d --- /dev/null +++ b/Packages/FeatureServices/SwapService/TestKit/SwapQuotesProviderMock.swift @@ -0,0 +1,27 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import SwapService +import Primitives +import struct Gemstone.SwapperQuote + +public struct SwapQuotesProviderMock: SwapQuotesProvidable { + private let results: [Result] + + public init(results: [Result]) { + self.results = results + } + + public func supportedAssets(for assetId: AssetId) -> ([Primitives.Chain], [Primitives.AssetId]) { + ([], []) + } + + public func fetchQuotes(wallet: Wallet, input: SwapQuoteInput) -> AsyncStream> { + let results = self.results + return AsyncStream { continuation in + for result in results { + continuation.yield(result) + } + continuation.finish() + } + } +} diff --git a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderData+TestKit.swift b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderData+TestKit.swift index 0b8a5091d..b364195da 100644 --- a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderData+TestKit.swift +++ b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderData+TestKit.swift @@ -2,12 +2,13 @@ import Foundation import struct Gemstone.SwapperProviderData +import struct Gemstone.SwapperProviderType import struct Gemstone.SwapperRoute -extension SwapperProviderData { - static func mock() -> SwapperProviderData { +public extension SwapperProviderData { + static func mock(provider: SwapperProviderType = .mock()) -> SwapperProviderData { SwapperProviderData( - provider: .mock(), + provider: provider, slippageBps: 50, routes: [.mock()] ) diff --git a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift index 405b4393d..e43ff1b66 100644 --- a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift +++ b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift @@ -5,12 +5,12 @@ import enum Gemstone.SwapperProvider import struct Gemstone.SwapperProviderType public extension SwapperProviderType { - static func mock() -> SwapperProviderType { + static func mock(id: SwapperProvider = .pancakeswapV3) -> SwapperProviderType { SwapperProviderType( - id: .pancakeswapV3, - name: "PancakeSwap", + id: id, + name: "\(id)", protocol: "v3", - protocolId: "pancakeswap_v3", + protocolId: "\(id)_v3", mode: .onChain ) } diff --git a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperQuote+TestKit.swift b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperQuote+TestKit.swift index 8d4e35a92..d703eb337 100644 --- a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperQuote+TestKit.swift +++ b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperQuote+TestKit.swift @@ -1,18 +1,20 @@ // Copyright (c). Gem Wallet. All rights reserved. import Foundation +import struct Gemstone.SwapperProviderData import struct Gemstone.SwapperQuote public extension SwapperQuote { static func mock( fromValue: String = "1000000000000000000", toValue: String = "250000000000", + data: SwapperProviderData = .mock(), etaInSeconds: UInt32? = nil ) -> SwapperQuote { SwapperQuote( fromValue: fromValue, toValue: toValue, - data: .mock(), + data: data, request: .mock(), etaInSeconds: etaInSeconds )