From a132a696cb9ed8801093c6741f9ac92216177006 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:22:04 +0900 Subject: [PATCH 01/12] implement SwapTransactionService --- Features/Transactions/Package.swift | 4 +- .../Sources/Types/TransactionSection.swift | 3 + .../TransactionSceneViewModel.swift | 108 +++++++++++++++++- .../TransactionStatusViewModel.swift | 58 +++++++++- .../TransactionSwapHashViewModel.swift | 79 +++++++++++++ .../TransactionSceneViewModelTests.swift | 73 +++++++++++- .../TransactionsNavigationStack.swift | 5 +- .../Wallet/WalletNavigationStack.swift | 17 +-- Gem/Services/AppResolver+Services.swift | 3 + Gem/Services/ServicesFactory.swift | 2 + Gem/Types/Environment.swift | 1 + .../SwapService/SwapService.swift | 1 - .../SwapService/SwapTransactionService.swift | 63 ++++++---- ...SwapperSwapResult+GemstonePrimitives.swift | 40 +++++++ Packages/Localization/Sources/Localized.swift | 8 ++ .../Primitives/Sources/Swap/SwapResult.swift | 21 ++++ .../ViewModels/TransactionViewModel.swift | 4 + core | 2 +- 18 files changed, 447 insertions(+), 45 deletions(-) create mode 100644 Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift create mode 100644 Packages/GemstonePrimitives/Sources/Extensions/SwapperSwapResult+GemstonePrimitives.swift create mode 100644 Packages/Primitives/Sources/Swap/SwapResult.swift diff --git a/Features/Transactions/Package.swift b/Features/Transactions/Package.swift index a5f9c80d9..6b5158d8c 100644 --- a/Features/Transactions/Package.swift +++ b/Features/Transactions/Package.swift @@ -37,6 +37,7 @@ let package = Package( "PrimitivesComponents", .product(name: "ExplorerService", package: "ChainServices"), .product(name: "TransactionsService", package: "FeatureServices"), + .product(name: "SwapService", package: "FeatureServices"), .product(name: "WalletService", package: "FeatureServices"), "Preferences", "InfoSheet" @@ -49,7 +50,8 @@ let package = Package( .product(name: "PrimitivesTestKit", package: "Primitives"), .product(name: "PreferencesTestKit", package: "Preferences"), "Transactions", - "PrimitivesComponents" + "PrimitivesComponents", + .product(name: "SwapService", package: "FeatureServices") ] ), ] diff --git a/Features/Transactions/Sources/Types/TransactionSection.swift b/Features/Transactions/Sources/Types/TransactionSection.swift index 468e0ebb3..e8987db9a 100644 --- a/Features/Transactions/Sources/Types/TransactionSection.swift +++ b/Features/Transactions/Sources/Types/TransactionSection.swift @@ -19,6 +19,9 @@ public enum TransactionItem: Identifiable, Equatable, Sendable { case swapButton case date case status + case swapStatus + case sourceTransaction + case destinationTransaction case participant case memo case network diff --git a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift index 8e28d0391..48a4ce851 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift @@ -9,29 +9,36 @@ import Primitives import PrimitivesComponents import Store import SwiftUI +import SwapService @Observable @MainActor public final class TransactionSceneViewModel { private let preferences: Preferences private let explorerService: ExplorerService + private let swapTransactionService: any SwapResultProviding var request: TransactionRequest var transactionExtended: TransactionExtended + var swapResult: SwapResult? var isPresentingShareSheet = false var isPresentingInfoSheet: InfoSheetType? = .none + private var swapStatusTask: Task? public init( transaction: TransactionExtended, walletId: String, preferences: Preferences = Preferences.standard, - explorerService: ExplorerService = ExplorerService.standard + explorerService: ExplorerService = ExplorerService.standard, + swapTransactionService: any SwapResultProviding ) { self.preferences = preferences self.explorerService = explorerService + self.swapTransactionService = swapTransactionService self.transactionExtended = transaction self.request = TransactionRequest(walletId: walletId, transactionId: transaction.id) + startSwapStatusUpdates() } var title: String { model.titleTextValue.text } @@ -45,7 +52,7 @@ extension TransactionSceneViewModel: ListSectionProvideable { [ ListSection(type: .header, [.header]), ListSection(type: .swapAction, [.swapButton]), - ListSection(type: .details, [.date, .status, .participant, .memo, .network, .pnl, .price, .size, .provider, .fee]), + ListSection(type: .details, detailItems), ListSection(type: .explorer, [.explorerLink]) ] } @@ -55,7 +62,20 @@ extension TransactionSceneViewModel: ListSectionProvideable { case .header: headerViewModel case .swapButton: TransactionSwapButtonViewModel(transaction: transactionExtended) case .date: TransactionDateViewModel(date: model.transaction.transaction.createdAt) - case .status: TransactionStatusViewModel(state: model.transaction.transaction.state, onInfoAction: onSelectStatusInfo) + case .status: + TransactionStatusViewModel(state: model.transaction.transaction.state, onInfoAction: onSelectStatusInfo) + case .swapStatus: + TransactionStatusViewModel(title: model.swapStatusTitle, state: model.transaction.transaction.state, swapResult: swapResult, onInfoAction: onSelectStatusInfo) + case .sourceTransaction: + TransactionSwapHashViewModel( + kind: .source(state: model.transaction.transaction.state, infoAction: onSelectStatusInfo), + hash: model.transaction.transaction.hash + ) + case .destinationTransaction: + TransactionSwapHashViewModel( + kind: .destination, + hash: swapResult?.toTxHash + ) case .participant: TransactionParticipantViewModel(transactionViewModel: model) case .memo: TransactionMemoViewModel(transaction: model.transaction.transaction) case .network: TransactionNetworkViewModel(chain: model.transaction.asset.chain) @@ -75,6 +95,7 @@ extension TransactionSceneViewModel { func onChangeTransaction(_ oldValue: TransactionExtended, _ newValue: TransactionExtended) { if oldValue != newValue { transactionExtended = newValue + restartSwapStatusUpdates() } } @@ -113,7 +134,7 @@ extension TransactionSceneViewModel { currency: preferences.currency ) } - + private var headerViewModel: TransactionHeaderViewModel { TransactionHeaderViewModel( transaction: model.transaction, @@ -127,4 +148,83 @@ extension TransactionSceneViewModel { explorerService: explorerService ) } + + private var detailItems: [TransactionItem] { + var items: [TransactionItem] = [.date] + if isCrossChainSwap { + items.append(contentsOf: [.swapStatus, .sourceTransaction, .destinationTransaction]) + } else { + items.append(.status) + } + + items.append(contentsOf: [.participant, .memo, .network, .pnl, .price, .size, .provider, .fee]) + return items + } + + private var swapMetadata: TransactionSwapMetadata? { + guard case let .swap(metadata) = transactionExtended.transaction.metadata else { return nil } + return metadata + } + + private var isCrossChainSwap: Bool { + guard let metadata = swapMetadata else { return false } + return metadata.fromAsset.chain != metadata.toAsset.chain + } + + private var sourceChainName: String? { + swapMetadata?.fromAsset.chain.rawValue.capitalized + } + + private var destinationChainName: String? { + swapMetadata?.toAsset.chain.rawValue.capitalized + } + + private var swapProviderIdentifier: String? { + swapMetadata?.provider + } + + private func restartSwapStatusUpdates() { + swapStatusTask?.cancel() + swapStatusTask = nil + swapResult = nil + startSwapStatusUpdates() + } + + private func startSwapStatusUpdates() { + guard swapStatusTask == nil else { return } + guard isCrossChainSwap, let provider = swapProviderIdentifier else { return } + + let chain = transactionExtended.transaction.assetId.chain + let hash = transactionExtended.transaction.hash + + swapStatusTask = Task { [weak self] in + await self?.pollSwapStatus(chain: chain, provider: provider, hash: hash) + } + } + + private func pollSwapStatus(chain: Chain, provider: String, hash: String) async { + defer { swapStatusTask = nil } + + var backoff: Duration = .seconds(5) + + while !Task.isCancelled { + do { + let result = try await swapTransactionService.getSwapResult( + chain: chain, + providerId: provider, + transactionHash: hash + ) + swapResult = result + if result.status != .pending { break } + backoff = .seconds(5) + } catch { + NSLog("TransactionSceneViewModel swap status error: \(error)") + try? await Task.sleep(for: backoff) + backoff = min(backoff * 2, .seconds(300)) + continue + } + + try? await Task.sleep(for: .seconds(30)) + } + } } diff --git a/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift index b00c5fb17..b16604e4d 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift @@ -9,20 +9,70 @@ import Components import Style public struct TransactionStatusViewModel { + private let title: String private let state: TransactionState + private let swapResult: SwapResult? private let onInfoAction: VoidAction public init( + title: String = Localized.Transaction.status, state: TransactionState, + swapResult: SwapResult? = nil, onInfoAction: VoidAction ) { + self.title = title self.state = state + self.swapResult = swapResult self.onInfoAction = onInfoAction } private var stateViewModel: TransactionStateViewModel { TransactionStateViewModel(state: state) } + + private var swapStateViewModel: TransactionStateViewModel? { + guard let swapResult else { return nil } + let mappedState: TransactionState = { + switch swapResult.status { + case .pending: .pending + case .completed: .confirmed + case .failed: .failed + case .refunded: .failed + } + }() + return TransactionStateViewModel(state: mappedState) + } + + private var statusTitle: String { + if let swapResult { + switch swapResult.status { + case .pending: return Localized.Transaction.Status.pending + case .completed: return Localized.Transaction.Status.confirmed + case .failed: return Localized.Transaction.Status.failed + case .refunded: return Localized.Transaction.Status.refunded + } + } + return stateViewModel.title + } + + private var statusColor: Color { + swapStateViewModel?.color ?? stateViewModel.color + } + + private var statusBackground: Color { + swapStateViewModel?.background ?? stateViewModel.background + } + + private var statusTag: TitleTagType { + if let swapResult, swapResult.status == .pending { + return .progressView() + } + if state == .pending && swapResult == nil { + return .progressView() + } + let image = (swapStateViewModel ?? stateViewModel).stateImage + return .image(image) + } } // MARK: - ItemModelProvidable @@ -30,10 +80,10 @@ public struct TransactionStatusViewModel { extension TransactionStatusViewModel: ItemModelProvidable { public var itemModel: TransactionItemModel { .listItem(ListItemModel( - title: Localized.Transaction.status, - titleTagType: state == .pending ? .progressView() : .image(stateViewModel.stateImage), - subtitle: stateViewModel.title, - subtitleStyle: TextStyle(font: .callout, color: stateViewModel.color), + title: title, + titleTagType: statusTag, + subtitle: statusTitle, + subtitleStyle: TextStyle(font: .callout, color: statusColor, background: statusBackground), infoAction: onInfoAction )) } diff --git a/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift new file mode 100644 index 000000000..baeecdbd8 --- /dev/null +++ b/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift @@ -0,0 +1,79 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import Components +import Localization +import Primitives +import PrimitivesComponents +import Style + +struct TransactionSwapHashViewModel: ItemModelProvidable { + enum Kind { + case source(state: TransactionState, infoAction: VoidAction) + case destination + } + + private let kind: Kind + private let hash: String? + + init(kind: Kind, hash: String?) { + self.kind = kind + self.hash = hash + } + + private var title: String { + switch kind { + case .source: return Localized.Transaction.sourceTransaction + case .destination: return Localized.Transaction.destinationTransaction + } + } + + private var stateViewModel: TransactionStateViewModel? { + guard case let .source(state, _) = kind else { return nil } + return TransactionStateViewModel(state: state) + } + + private var titleTagType: TitleTagType { + guard case let .source(state, _) = kind else { return .none } + switch state { + case .pending: return .progressView() + case .confirmed, .failed, .reverted: return .image(stateViewModel?.stateImage ?? Images.Transaction.State.success) + } + } + + private var titleExtra: String? { + stateViewModel?.title + } + + private var subtitle: String { + if let hash, hash.isEmpty == false { + return hash + } + return Localized.Transaction.Status.pending + } + + private var subtitleStyle: TextStyle { + if let hash, hash.isEmpty == false { + return TextStyle(font: .callout, color: Colors.black) + } + return TextStyle(font: .callout, color: Colors.orange) + } + + private var infoAction: VoidAction { + switch kind { + case let .source(_, action): return action + case .destination: return nil + } + } + + var itemModel: TransactionItemModel { + .listItem(ListItemModel( + title: title, + titleTagType: titleTagType, + titleExtra: titleExtra, + subtitle: subtitle, + subtitleStyle: subtitleStyle, + infoAction: infoAction + )) + } +} diff --git a/Features/Transactions/Tests/TransactionsTests/TransactionSceneViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/TransactionSceneViewModelTests.swift index b8ad99676..ddc120550 100644 --- a/Features/Transactions/Tests/TransactionsTests/TransactionSceneViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/TransactionSceneViewModelTests.swift @@ -7,6 +7,7 @@ import PreferencesTestKit import PrimitivesComponents import Style import Components +import SwapService @testable import Transactions @testable import Store @@ -92,6 +93,47 @@ struct TransactionSceneViewModelTests { } } + @Test + func swapStatusReflectsSwapResult() async { + let swapResult = SwapResult( + status: .completed, + fromChain: .ethereum, + fromTxHash: "0xsource", + toChain: .some(.arbitrum), + toTxHash: "0xdestination" + ) + + let metadata = TransactionSwapMetadata( + fromAsset: AssetId(chain: .ethereum, tokenId: nil), + fromValue: "1", + toAsset: AssetId(chain: .arbitrum, tokenId: nil), + toValue: "1", + provider: "across" + ) + + let swapModel = TransactionSceneViewModel.mock( + type: .swap, + metadata: .swap(metadata), + swapResultProvider: .init { _, _, _ in swapResult } + ) + + await Task.yield() + + if case .listItem(let item) = swapModel.item(for: TransactionItem.swapStatus) { + #expect(item.title == Localized.Transaction.swapStatus) + #expect(item.subtitle == Localized.Transaction.Status.confirmed) + } else { + Issue.record("Expected swap status list item") + } + + if case .listItem(let destinationItem) = swapModel.item(for: TransactionItem.destinationTransaction) { + #expect(destinationItem.title == Localized.Transaction.destinationTransaction) + #expect(destinationItem.subtitle == "0xdestination") + } else { + Issue.record("Expected destination transaction list item") + } + } + @Test func participantItemModel() { let transaction = TransactionExtended.mock( @@ -105,7 +147,8 @@ struct TransactionSceneViewModelTests { let modelWithAddresses = TransactionSceneViewModel( transaction: transaction, walletId: "test_wallet_id", - preferences: Preferences.standard + preferences: Preferences.standard, + swapTransactionService: SwapResultProviderMock.noop ) if case .participant(let item) = modelWithAddresses.item(for: TransactionItem.participant) { @@ -232,7 +275,9 @@ extension TransactionSceneViewModel { direction: TransactionDirection = .outgoing, toAddress: String = "participant_address", memo: String? = nil, - createdAt: Date = Date() + createdAt: Date = Date(), + metadata: TransactionMetadata? = nil, + swapResultProvider: SwapResultProviderMock = .noop ) -> TransactionSceneViewModel { TransactionSceneViewModel( transaction: TransactionExtended.mock( @@ -241,11 +286,31 @@ extension TransactionSceneViewModel { state: state, direction: direction, to: toAddress, - memo: memo + memo: memo, + metadata: metadata ) ), walletId: "test_wallet_id", - preferences: Preferences.standard + preferences: Preferences.standard, + swapTransactionService: swapResultProvider ) } } + +private struct SwapResultProviderMock: SwapResultProviding { + private let handler: (Chain, String?, String) async throws -> SwapResult + + init(handler: @escaping (Chain, String?, String) async throws -> SwapResult) { + self.handler = handler + } + + static var noop: SwapResultProviderMock { + SwapResultProviderMock { _, _, _ in + SwapResult(status: .pending, fromChain: .ethereum, fromTxHash: "", toChain: nil, toTxHash: nil) + } + } + + func getSwapResult(chain: Chain, providerId: String?, transactionHash: String) async throws -> SwapResult { + try await handler(chain, providerId, transactionHash) + } +} diff --git a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift index aa6d17904..7692f56d3 100644 --- a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift +++ b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift @@ -8,6 +8,7 @@ import Transactions import Store import Assets import AssetsService +import SwapService struct TransactionsNavigationStack: View { @Environment(\.navigationState) private var navigationState @@ -17,6 +18,7 @@ struct TransactionsNavigationStack: View { @Environment(\.assetsService) private var assetsService @Environment(\.priceObserverService) private var priceObserverService @Environment(\.bannerService) private var bannerService + @Environment(\.swapTransactionService) private var swapTransactionService @State private var model: TransactionsViewModel @@ -56,7 +58,8 @@ struct TransactionsNavigationStack: View { TransactionNavigationView( model: TransactionSceneViewModel( transaction: $0, - walletId: model.wallet.id + walletId: model.wallet.id, + swapTransactionService: swapTransactionService ) ) } diff --git a/Gem/Navigation/Wallet/WalletNavigationStack.swift b/Gem/Navigation/Wallet/WalletNavigationStack.swift index 33866cac9..2bd13d8c5 100644 --- a/Gem/Navigation/Wallet/WalletNavigationStack.swift +++ b/Gem/Navigation/Wallet/WalletNavigationStack.swift @@ -14,6 +14,7 @@ import Transfer import StakeService import PriceAlerts import AssetsService +import SwapService struct WalletNavigationStack: View { @Environment(\.walletsService) private var walletsService @@ -27,6 +28,7 @@ struct WalletNavigationStack: View { @Environment(\.stakeService) private var stakeService @Environment(\.perpetualService) private var perpetualService @Environment(\.balanceService) private var balanceService + @Environment(\.swapTransactionService) private var swapTransactionService @State private var model: WalletSceneViewModel @@ -105,14 +107,15 @@ struct WalletNavigationStack: View { ) ) } - .navigationDestination(for: TransactionExtended.self) { - TransactionNavigationView( - model: TransactionSceneViewModel( - transaction: $0, - walletId: model.wallet.id + .navigationDestination(for: TransactionExtended.self) { + TransactionNavigationView( + model: TransactionSceneViewModel( + transaction: $0, + walletId: model.wallet.id, + swapTransactionService: swapTransactionService + ) ) - ) - } + } .navigationDestination(for: Scenes.Price.self) { ChartScene( model: ChartsViewModel( diff --git a/Gem/Services/AppResolver+Services.swift b/Gem/Services/AppResolver+Services.swift index 4d303a6ab..4b7088847 100644 --- a/Gem/Services/AppResolver+Services.swift +++ b/Gem/Services/AppResolver+Services.swift @@ -49,6 +49,7 @@ extension AppResolver { let nftService: NFTService let avatarService: AvatarService let swapService: SwapService + let swapTransactionService: SwapTransactionService let subscriptionsService: SubscriptionService let appReleaseService: AppReleaseService let deviceObserverService: DeviceObserverService @@ -83,6 +84,7 @@ extension AppResolver { nftService: NFTService, avatarService: AvatarService, swapService: SwapService, + swapTransactionService: SwapTransactionService, appReleaseService: AppReleaseService, subscriptionsService: SubscriptionService, deviceObserverService: DeviceObserverService, @@ -116,6 +118,7 @@ extension AppResolver { self.nftService = nftService self.avatarService = avatarService self.swapService = swapService + self.swapTransactionService = swapTransactionService self.appReleaseService = appReleaseService self.deviceObserverService = deviceObserverService self.subscriptionsService = subscriptionsService diff --git a/Gem/Services/ServicesFactory.swift b/Gem/Services/ServicesFactory.swift index 704918324..49d9789ca 100644 --- a/Gem/Services/ServicesFactory.swift +++ b/Gem/Services/ServicesFactory.swift @@ -121,6 +121,7 @@ struct ServicesFactory { ) let explorerService = ExplorerService.standard let swapService = SwapService(nodeProvider: nodeService) + let swapTransactionService = SwapTransactionService(nodeProvider: nodeService) let presenter = WalletConnectorPresenter() let walletConnectorManager = WalletConnectorManager(presenter: presenter) @@ -220,6 +221,7 @@ struct ServicesFactory { nftService: nftService, avatarService: avatarService, swapService: swapService, + swapTransactionService: swapTransactionService, appReleaseService: releaseService, subscriptionsService: subscriptionService, deviceObserverService: deviceObserverService, diff --git a/Gem/Types/Environment.swift b/Gem/Types/Environment.swift index f27b49788..2c8da5d29 100644 --- a/Gem/Types/Environment.swift +++ b/Gem/Types/Environment.swift @@ -56,6 +56,7 @@ extension EnvironmentValues { @Entry var releaseService: AppReleaseService = AppResolver.main.services.appReleaseService @Entry var scanService: ScanService = AppResolver.main.services.scanService @Entry var swapService: SwapService = AppResolver.main.services.swapService + @Entry var swapTransactionService: SwapTransactionService = AppResolver.main.services.swapTransactionService @Entry var perpetualService: PerpetualService = AppResolver.main.services.perpetualService @Entry var perpetualObserverService: PerpetualObserverService = AppResolver.main.services.perpetualObserverService @Entry var transactionService: TransactionService = AppResolver.main.services.transactionService diff --git a/Packages/FeatureServices/SwapService/SwapService.swift b/Packages/FeatureServices/SwapService/SwapService.swift index 72d73004a..2e5650f49 100644 --- a/Packages/FeatureServices/SwapService/SwapService.swift +++ b/Packages/FeatureServices/SwapService/SwapService.swift @@ -17,7 +17,6 @@ import func Gemstone.getDefaultSlippage import GemstonePrimitives import NativeProviderService import Primitives -import enum Primitives.AnyError import enum Primitives.Chain import enum Primitives.EVMChain diff --git a/Packages/FeatureServices/SwapService/SwapTransactionService.swift b/Packages/FeatureServices/SwapService/SwapTransactionService.swift index 1eb104cf5..f855c493b 100644 --- a/Packages/FeatureServices/SwapService/SwapTransactionService.swift +++ b/Packages/FeatureServices/SwapService/SwapTransactionService.swift @@ -2,34 +2,53 @@ import Foundation import class Gemstone.GemSwapper +import enum Gemstone.SwapperProvider +import struct Gemstone.SwapperSwapResult +import class Gemstone.SwapProviderConfig import GemstonePrimitives -import ChainService import NativeProviderService import Primitives -public struct SwapTransactionService: Sendable { - private let nodeProvider: any NodeURLFetchable +public protocol SwapResultProviding: Sendable { + func getSwapResult( + chain: Primitives.Chain, + providerId: String?, + transactionHash: String + ) async throws -> SwapResult +} + +public struct SwapTransactionService: SwapResultProviding, Sendable { private let swapper: GemSwapper - private let swapConfig = GemstoneConfig.shared.getSwapConfig() - - public init( - nodeProvider: any NodeURLFetchable - ) { - self.nodeProvider = nodeProvider - self.swapper = GemSwapper( - rpcProvider: NativeProvider(nodeProvider: nodeProvider) + + public init(nodeProvider: any NodeURLFetchable) { + self.swapper = GemSwapper(rpcProvider: NativeProvider(nodeProvider: nodeProvider)) + } + + public func getSwapResult( + chain: Primitives.Chain, + providerId: String?, + transactionHash: String + ) async throws -> SwapResult { + guard let providerId, !providerId.isEmpty else { + throw AnyError("Swap provider is missing") + } + + guard let swapProvider = providerId.toSwapperProvider() else { + throw AnyError("Invalid swap provider: \(providerId)") + } + + let result = try await swapper.getSwapResult( + chain: chain.rawValue, + swapProvider: swapProvider, + transactionHash: transactionHash ) + + return try result.asPrimitives() } - - public func getTransactionStatus(chain: Primitives.Chain, provider: String?, hash: String) async throws { - //let provider = SwapProviderConfig - - //let provider = SwapProvider.across - -// let status = try await swapper.getTransactionStatus( -// chain: chain.rawValue, -// swapProvider: .across, -// transactionHash: hash -// ) +} + +private extension String { + func toSwapperProvider() -> SwapperProvider? { + SwapProviderConfig.fromString(id: self).inner().id } } diff --git a/Packages/GemstonePrimitives/Sources/Extensions/SwapperSwapResult+GemstonePrimitives.swift b/Packages/GemstonePrimitives/Sources/Extensions/SwapperSwapResult+GemstonePrimitives.swift new file mode 100644 index 000000000..391344298 --- /dev/null +++ b/Packages/GemstonePrimitives/Sources/Extensions/SwapperSwapResult+GemstonePrimitives.swift @@ -0,0 +1,40 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import struct Gemstone.SwapperSwapResult +import enum Gemstone.SwapperSwapStatus +import Primitives + +public extension SwapperSwapResult { + func asPrimitives() throws -> SwapResult { + guard let primitiveFromChain = Chain(rawValue: fromChain) else { + throw AnyError("Invalid swap result chain: \(fromChain)") + } + + let destinationChain = try toChain.map { chainId -> Chain in + guard let chain = Chain(rawValue: chainId) else { + throw AnyError("Invalid destination chain: \(chainId)") + } + return chain + } + + return SwapResult( + status: SwapStatus(status), + fromChain: primitiveFromChain, + fromTxHash: fromTxHash, + toChain: destinationChain, + toTxHash: toTxHash + ) + } +} + +private extension SwapStatus { + init(_ status: SwapperSwapStatus) { + switch status { + case .pending: self = .pending + case .completed: self = .completed + case .failed: self = .failed + case .refunded: self = .refunded + } + } +} diff --git a/Packages/Localization/Sources/Localized.swift b/Packages/Localization/Sources/Localized.swift index 5237d3ee3..caf9ce89e 100644 --- a/Packages/Localization/Sources/Localized.swift +++ b/Packages/Localization/Sources/Localized.swift @@ -1112,6 +1112,12 @@ public enum Localized { public static let status = Localized.tr("Localizable", "transaction.status", fallback: "Status") /// Swap Again public static let swapAgain = Localized.tr("Localizable", "transaction.swap_again", fallback: "Swap Again") + /// Swap Status + public static let swapStatus = Localized.tr("Localizable", "transaction.swap_status", fallback: "Swap Status") + /// Source Transaction + public static let sourceTransaction = Localized.tr("Localizable", "transaction.source_transaction", fallback: "Source Transaction") + /// Destination Transaction + public static let destinationTransaction = Localized.tr("Localizable", "transaction.destination_transaction", fallback: "Destination Transaction") /// View on %@ public static func viewOn(_ p1: Any) -> String { return Localized.tr("Localizable", "transaction.view_on", String(describing: p1), fallback: "View on %@") @@ -1125,6 +1131,8 @@ public enum Localized { public static let pending = Localized.tr("Localizable", "transaction.status.pending", fallback: "Pending") /// Reverted public static let reverted = Localized.tr("Localizable", "transaction.status.reverted", fallback: "Reverted") + /// Refunded + public static let refunded = Localized.tr("Localizable", "transaction.status.refunded", fallback: "Refunded") } public enum Title { /// Received diff --git a/Packages/Primitives/Sources/Swap/SwapResult.swift b/Packages/Primitives/Sources/Swap/SwapResult.swift new file mode 100644 index 000000000..79ca1e138 --- /dev/null +++ b/Packages/Primitives/Sources/Swap/SwapResult.swift @@ -0,0 +1,21 @@ +/* + Generated by typeshare 1.13.2 + */ + +import Foundation + +public struct SwapResult: Codable, Equatable, Hashable, Sendable { + public let status: SwapStatus + public let fromChain: Chain + public let fromTxHash: String + public let toChain: Chain? + public let toTxHash: String? + + public init(status: SwapStatus, fromChain: Chain, fromTxHash: String, toChain: Chain?, toTxHash: String?) { + self.status = status + self.fromChain = fromChain + self.fromTxHash = fromTxHash + self.toChain = toChain + self.toTxHash = toTxHash + } +} diff --git a/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift b/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift index 8736ad52c..7251579a7 100644 --- a/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift +++ b/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift @@ -150,6 +150,10 @@ public struct TransactionViewModel: Sendable { } } + public var swapStatusTitle: String { + Localized.Transaction.swapStatus + } + public var titleTagTextValue: TextValue? { let title: String? = switch transaction.transaction.state { case .confirmed: .none diff --git a/core b/core index 1d7139bef..168bb3710 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 1d7139bef604ecd4ab1afbcbcc41a747d3749a84 +Subproject commit 168bb3710647a1454bc3799ac7a3ffb7081fe727 From 5971cbac45fb96c51eb96da979c590bd32caff1c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:41:11 +0900 Subject: [PATCH 02/12] Update TransactionSwapHashViewModel.swift --- .../Sources/ViewModels/TransactionSwapHashViewModel.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift index baeecdbd8..85b85fa19 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift @@ -41,10 +41,6 @@ struct TransactionSwapHashViewModel: ItemModelProvidable { } } - private var titleExtra: String? { - stateViewModel?.title - } - private var subtitle: String { if let hash, hash.isEmpty == false { return hash @@ -70,7 +66,6 @@ struct TransactionSwapHashViewModel: ItemModelProvidable { .listItem(ListItemModel( title: title, titleTagType: titleTagType, - titleExtra: titleExtra, subtitle: subtitle, subtitleStyle: subtitleStyle, infoAction: infoAction From 923f6df3314b9d3c57ccb05cd4f2c7c8f9702c06 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:46:27 +0900 Subject: [PATCH 03/12] remove sourceTransaction and destinationTransaction --- .../Sources/Types/TransactionSection.swift | 2 - .../TransactionSceneViewModel.swift | 24 +----- .../TransactionSwapHashViewModel.swift | 74 ------------------- Packages/Localization/Sources/Localized.swift | 4 - core | 2 +- 5 files changed, 5 insertions(+), 101 deletions(-) delete mode 100644 Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift diff --git a/Features/Transactions/Sources/Types/TransactionSection.swift b/Features/Transactions/Sources/Types/TransactionSection.swift index e8987db9a..0e6a59d4e 100644 --- a/Features/Transactions/Sources/Types/TransactionSection.swift +++ b/Features/Transactions/Sources/Types/TransactionSection.swift @@ -20,8 +20,6 @@ public enum TransactionItem: Identifiable, Equatable, Sendable { case date case status case swapStatus - case sourceTransaction - case destinationTransaction case participant case memo case network diff --git a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift index 48a4ce851..50f189c55 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift @@ -66,16 +66,6 @@ extension TransactionSceneViewModel: ListSectionProvideable { TransactionStatusViewModel(state: model.transaction.transaction.state, onInfoAction: onSelectStatusInfo) case .swapStatus: TransactionStatusViewModel(title: model.swapStatusTitle, state: model.transaction.transaction.state, swapResult: swapResult, onInfoAction: onSelectStatusInfo) - case .sourceTransaction: - TransactionSwapHashViewModel( - kind: .source(state: model.transaction.transaction.state, infoAction: onSelectStatusInfo), - hash: model.transaction.transaction.hash - ) - case .destinationTransaction: - TransactionSwapHashViewModel( - kind: .destination, - hash: swapResult?.toTxHash - ) case .participant: TransactionParticipantViewModel(transactionViewModel: model) case .memo: TransactionMemoViewModel(transaction: model.transaction.transaction) case .network: TransactionNetworkViewModel(chain: model.transaction.asset.chain) @@ -152,8 +142,10 @@ extension TransactionSceneViewModel { private var detailItems: [TransactionItem] { var items: [TransactionItem] = [.date] if isCrossChainSwap { - items.append(contentsOf: [.swapStatus, .sourceTransaction, .destinationTransaction]) - } else { + items.append(.swapStatus) + } + + if !isCrossChainSwap { items.append(.status) } @@ -171,14 +163,6 @@ extension TransactionSceneViewModel { return metadata.fromAsset.chain != metadata.toAsset.chain } - private var sourceChainName: String? { - swapMetadata?.fromAsset.chain.rawValue.capitalized - } - - private var destinationChainName: String? { - swapMetadata?.toAsset.chain.rawValue.capitalized - } - private var swapProviderIdentifier: String? { swapMetadata?.provider } diff --git a/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift deleted file mode 100644 index 85b85fa19..000000000 --- a/Features/Transactions/Sources/ViewModels/TransactionSwapHashViewModel.swift +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import Foundation -import Components -import Localization -import Primitives -import PrimitivesComponents -import Style - -struct TransactionSwapHashViewModel: ItemModelProvidable { - enum Kind { - case source(state: TransactionState, infoAction: VoidAction) - case destination - } - - private let kind: Kind - private let hash: String? - - init(kind: Kind, hash: String?) { - self.kind = kind - self.hash = hash - } - - private var title: String { - switch kind { - case .source: return Localized.Transaction.sourceTransaction - case .destination: return Localized.Transaction.destinationTransaction - } - } - - private var stateViewModel: TransactionStateViewModel? { - guard case let .source(state, _) = kind else { return nil } - return TransactionStateViewModel(state: state) - } - - private var titleTagType: TitleTagType { - guard case let .source(state, _) = kind else { return .none } - switch state { - case .pending: return .progressView() - case .confirmed, .failed, .reverted: return .image(stateViewModel?.stateImage ?? Images.Transaction.State.success) - } - } - - private var subtitle: String { - if let hash, hash.isEmpty == false { - return hash - } - return Localized.Transaction.Status.pending - } - - private var subtitleStyle: TextStyle { - if let hash, hash.isEmpty == false { - return TextStyle(font: .callout, color: Colors.black) - } - return TextStyle(font: .callout, color: Colors.orange) - } - - private var infoAction: VoidAction { - switch kind { - case let .source(_, action): return action - case .destination: return nil - } - } - - var itemModel: TransactionItemModel { - .listItem(ListItemModel( - title: title, - titleTagType: titleTagType, - subtitle: subtitle, - subtitleStyle: subtitleStyle, - infoAction: infoAction - )) - } -} diff --git a/Packages/Localization/Sources/Localized.swift b/Packages/Localization/Sources/Localized.swift index caf9ce89e..e8ab97f95 100644 --- a/Packages/Localization/Sources/Localized.swift +++ b/Packages/Localization/Sources/Localized.swift @@ -1114,10 +1114,6 @@ public enum Localized { public static let swapAgain = Localized.tr("Localizable", "transaction.swap_again", fallback: "Swap Again") /// Swap Status public static let swapStatus = Localized.tr("Localizable", "transaction.swap_status", fallback: "Swap Status") - /// Source Transaction - public static let sourceTransaction = Localized.tr("Localizable", "transaction.source_transaction", fallback: "Source Transaction") - /// Destination Transaction - public static let destinationTransaction = Localized.tr("Localizable", "transaction.destination_transaction", fallback: "Destination Transaction") /// View on %@ public static func viewOn(_ p1: Any) -> String { return Localized.tr("Localizable", "transaction.view_on", String(describing: p1), fallback: "View on %@") diff --git a/core b/core index 168bb3710..bd525a8bf 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 168bb3710647a1454bc3799ac7a3ffb7081fe727 +Subproject commit bd525a8bf7e2a0f7edf9cc46866528a7eec9786e From 61972bd6084178d9fd3048c6109068685ea9640b Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:50:04 +0900 Subject: [PATCH 04/12] refactor SwapStatusProviding and fix tests --- .../TransactionSceneViewModel.swift | 19 +++++++----- .../TransactionSceneViewModelTests.swift | 30 ++++++++----------- .../SwapService/SwapTransactionService.swift | 21 ++++++++----- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift index 50f189c55..6d6f66441 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift @@ -8,15 +8,15 @@ import Preferences import Primitives import PrimitivesComponents import Store -import SwiftUI import SwapService +import SwiftUI @Observable @MainActor public final class TransactionSceneViewModel { private let preferences: Preferences private let explorerService: ExplorerService - private let swapTransactionService: any SwapResultProviding + private let swapTransactionService: any SwapStatusProviding var request: TransactionRequest var transactionExtended: TransactionExtended @@ -31,7 +31,7 @@ public final class TransactionSceneViewModel { walletId: String, preferences: Preferences = Preferences.standard, explorerService: ExplorerService = ExplorerService.standard, - swapTransactionService: any SwapResultProviding + swapTransactionService: any SwapStatusProviding ) { self.preferences = preferences self.explorerService = explorerService @@ -179,14 +179,16 @@ extension TransactionSceneViewModel { guard isCrossChainSwap, let provider = swapProviderIdentifier else { return } let chain = transactionExtended.transaction.assetId.chain - let hash = transactionExtended.transaction.hash + let transactionId = transactionExtended.transaction.hash + // Stellar might be different here + let memo = transactionExtended.transaction.memo ?? transactionExtended.transaction.to swapStatusTask = Task { [weak self] in - await self?.pollSwapStatus(chain: chain, provider: provider, hash: hash) + await self?.pollSwapStatus(provider: provider, chain: chain, transactionId: transactionId, memo: memo) } } - private func pollSwapStatus(chain: Chain, provider: String, hash: String) async { + private func pollSwapStatus(provider: String, chain: Chain, transactionId: String, memo: String?) async { defer { swapStatusTask = nil } var backoff: Duration = .seconds(5) @@ -194,9 +196,10 @@ extension TransactionSceneViewModel { while !Task.isCancelled { do { let result = try await swapTransactionService.getSwapResult( - chain: chain, providerId: provider, - transactionHash: hash + chain: chain, + transactionId: transactionId, + memo: memo ) swapResult = result if result.status != .pending { break } diff --git a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift index 8f5b9d488..a418edcba 100644 --- a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift @@ -114,7 +114,7 @@ struct TransactionSceneViewModelTests { let swapModel = TransactionSceneViewModel.mock( type: .swap, metadata: .swap(metadata), - swapResultProvider: .init { _, _, _ in swapResult } + swapStatusProvider: .init { _, _, _, _ in swapResult } ) await Task.yield() @@ -126,12 +126,6 @@ struct TransactionSceneViewModelTests { Issue.record("Expected swap status list item") } - if case .listItem(let destinationItem) = swapModel.item(for: TransactionItem.destinationTransaction) { - #expect(destinationItem.title == Localized.Transaction.destinationTransaction) - #expect(destinationItem.subtitle == "0xdestination") - } else { - Issue.record("Expected destination transaction list item") - } } @Test @@ -148,7 +142,7 @@ struct TransactionSceneViewModelTests { transaction: transaction, walletId: "test_wallet_id", preferences: Preferences.standard, - swapTransactionService: SwapResultProviderMock.noop + swapTransactionService: SwapStatusProviderMock.noop ) if case .participant(let item) = modelWithAddresses.item(for: TransactionItem.participant) { @@ -269,7 +263,7 @@ struct TransactionSceneViewModelTests { } extension TransactionSceneViewModel { - static func mock( + fileprivate static func mock( type: TransactionType = .transfer, state: TransactionState = .confirmed, direction: TransactionDirection = .outgoing, @@ -278,7 +272,7 @@ extension TransactionSceneViewModel { memo: String? = nil, createdAt: Date = Date(), metadata: TransactionMetadata? = nil, - swapResultProvider: SwapResultProviderMock = .noop + swapStatusProvider: SwapStatusProviderMock = .noop ) -> TransactionSceneViewModel { TransactionSceneViewModel( transaction: TransactionExtended.mock( @@ -294,25 +288,25 @@ extension TransactionSceneViewModel { ), walletId: "test_wallet_id", preferences: Preferences.standard, - swapTransactionService: swapResultProvider + swapTransactionService: swapStatusProvider ) } } -private struct SwapResultProviderMock: SwapResultProviding { - private let handler: (Chain, String?, String) async throws -> SwapResult +fileprivate struct SwapStatusProviderMock: SwapStatusProviding { + private let handler: @Sendable (String?, Chain, String, String?) async throws -> SwapResult - init(handler: @escaping (Chain, String?, String) async throws -> SwapResult) { + init(handler: @escaping @Sendable (String?, Chain, String, String?) async throws -> SwapResult) { self.handler = handler } - static var noop: SwapResultProviderMock { - SwapResultProviderMock { _, _, _ in + static var noop: SwapStatusProviderMock { + SwapStatusProviderMock { _, _, _, _ in SwapResult(status: .pending, fromChain: .ethereum, fromTxHash: "", toChain: nil, toTxHash: nil) } } - func getSwapResult(chain: Chain, providerId: String?, transactionHash: String) async throws -> SwapResult { - try await handler(chain, providerId, transactionHash) + func getSwapResult(providerId: String?, chain: Chain, transactionId: String, memo: String?) async throws -> SwapResult { + try await handler(providerId, chain, transactionId, memo) } } diff --git a/Packages/FeatureServices/SwapService/SwapTransactionService.swift b/Packages/FeatureServices/SwapService/SwapTransactionService.swift index f855c493b..88910c366 100644 --- a/Packages/FeatureServices/SwapService/SwapTransactionService.swift +++ b/Packages/FeatureServices/SwapService/SwapTransactionService.swift @@ -9,25 +9,27 @@ import GemstonePrimitives import NativeProviderService import Primitives -public protocol SwapResultProviding: Sendable { +public protocol SwapStatusProviding: Sendable { func getSwapResult( - chain: Primitives.Chain, providerId: String?, - transactionHash: String + chain: Primitives.Chain, + transactionId: String, + memo: String? ) async throws -> SwapResult } -public struct SwapTransactionService: SwapResultProviding, Sendable { +public struct SwapTransactionService: SwapStatusProviding, Sendable { private let swapper: GemSwapper public init(nodeProvider: any NodeURLFetchable) { - self.swapper = GemSwapper(rpcProvider: NativeProvider(nodeProvider: nodeProvider)) + swapper = GemSwapper(rpcProvider: NativeProvider(nodeProvider: nodeProvider)) } public func getSwapResult( - chain: Primitives.Chain, providerId: String?, - transactionHash: String + chain: Primitives.Chain, + transactionId: String, + memo: String? ) async throws -> SwapResult { guard let providerId, !providerId.isEmpty else { throw AnyError("Swap provider is missing") @@ -37,6 +39,11 @@ public struct SwapTransactionService: SwapResultProviding, Sendable { throw AnyError("Invalid swap provider: \(providerId)") } + let transactionHash = if swapProvider == .nearIntents, let memo, !memo.isEmpty { + memo + } else { + transactionId + } let result = try await swapper.getSwapResult( chain: chain.rawValue, swapProvider: swapProvider, From 005a7b8b095fd118f6172372c355c233b394160b Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:52:52 +0900 Subject: [PATCH 05/12] Update TransactionStatusViewModel.swift --- .../Sources/ViewModels/TransactionStatusViewModel.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift index b16604e4d..1fbb4638a 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionStatusViewModel.swift @@ -64,10 +64,7 @@ public struct TransactionStatusViewModel { } private var statusTag: TitleTagType { - if let swapResult, swapResult.status == .pending { - return .progressView() - } - if state == .pending && swapResult == nil { + if swapResult?.status == .pending || (swapResult == nil && state == .pending) { return .progressView() } let image = (swapStateViewModel ?? stateViewModel).stateImage From 77c4d35a5f54456f45d2a478edad8f9273931433 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:39:38 +0900 Subject: [PATCH 06/12] Update TransactionSceneViewModelTests.swift --- .../ViewModels/TransactionSceneViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift index a418edcba..e3d9741e8 100644 --- a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift @@ -207,11 +207,11 @@ struct TransactionSceneViewModelTests { func feeItemModel() { let model = TransactionSceneViewModel.mock() - if case .listItem(let item) = model.item(for: TransactionItem.fee) { + if case .fee(let item) = model.item(for: TransactionItem.fee) { #expect(item.title == Localized.Transfer.networkFee) #expect(item.infoAction != nil) } else { - Issue.record("Expected listItem for fee") + Issue.record("Expected fee item for network fee") } } From eb0365a2ae910e364539acbd54d78c57485c826b Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:46:05 +0900 Subject: [PATCH 07/12] refactor ExplorerLinkFetchable for swap status --- .../TransactionExplorerViewModel.swift | 18 ++++---- .../TransactionViewModelTests.swift | 25 ++++++++++- .../ExplorerService/ExplorerService.swift | 40 ++++++++++++++---- .../Protocols/ExplorerAdressFetchable.swift | 42 ++++++++++++++++++- ...lorerLinkFetchable+PrimitivesTestKit.swift | 11 ++++- .../Sources/Components/TransactionView.swift | 8 +++- .../ViewModels/TransactionViewModel.swift | 16 ++++--- core | 2 +- 8 files changed, 131 insertions(+), 31 deletions(-) diff --git a/Features/Transactions/Sources/ViewModels/TransactionExplorerViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionExplorerViewModel.swift index fde4e223a..13e300d5d 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionExplorerViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionExplorerViewModel.swift @@ -18,18 +18,14 @@ public struct TransactionExplorerViewModel: Sendable { self.explorerService = explorerService } - private var swapProvider: String? { - guard case let .swap(metadata) = transactionViewModel.transaction.transaction.metadata else { - return nil - } - return metadata.provider - } - private var transactionLink: BlockExplorerLink { - explorerService.transactionUrl( - chain: transactionViewModel.transaction.transaction.assetId.chain, - hash: transactionViewModel.transaction.transaction.hash, - swapProvider: swapProvider + let chain = transactionViewModel.transaction.transaction.assetId.chain + let hash = transactionViewModel.transaction.transaction.hash + return explorerService.transactionLink( + chain: chain, + provider: transactionViewModel.transaction.transaction.swapProvider, + hash: hash, + recipient: transactionViewModel.transaction.transaction.to ) } diff --git a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift index 4d812c54b..b1a3d0bae 100644 --- a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift @@ -70,6 +70,26 @@ final class TransactionViewModelTests { #expect(unknownViewModel.titleExtraTextValue?.text.contains("5678") == true) } + @Test + func swapTransactionsUseProviderSpecificExplorer() { + let model = TransactionViewModel.mock( + participant: "recipient.near", + swapProvider: SwapProvider.nearIntents.rawValue + ) + + #expect(model.transactionExplorerUrl.absoluteString.contains("recipient.near")) + } + + @Test + func swapIdentifierUsesRecipientForNearIntents() { + let model = TransactionViewModel.mock( + participant: "recipient.near", + swapProvider: SwapProvider.nearIntents.rawValue + ) + + #expect(model.transactionExplorerUrl.absoluteString.contains("recipient.near")) + } + func testTransactionTitle(expectedTitle: String, transaction: Transaction) { #expect(TransactionViewModel(explorerService: MockExplorerLink(), transaction: .mock(transaction: transaction), currency: "USD").titleTextValue.text == expectedTitle) } @@ -85,7 +105,8 @@ extension TransactionViewModel { participant: String = "", memo: String? = nil, fromAddress: AddressName? = nil, - toAddress: AddressName? = nil + toAddress: AddressName? = nil, + swapProvider: String? = nil ) -> TransactionViewModel { let fromAsset = Asset.mockEthereum() let toAsset = Asset.mockEthereumUSDT() @@ -96,7 +117,7 @@ extension TransactionViewModel { fromValue: fromValue, toAsset: toAsset.id, toValue: toValue, - provider: "" + provider: swapProvider ) ) diff --git a/Packages/ChainServices/ExplorerService/ExplorerService.swift b/Packages/ChainServices/ExplorerService/ExplorerService.swift index ae343ebc9..4ca5566d7 100644 --- a/Packages/ChainServices/ExplorerService/ExplorerService.swift +++ b/Packages/ChainServices/ExplorerService/ExplorerService.swift @@ -26,20 +26,44 @@ public struct ExplorerService { Gemstone.Config.shared.getBlockExplorers(chain: chain.id) } - public func transactionUrl(chain: Chain, hash: String, swapProvider: String?) -> BlockExplorerLink { + public func transactionUrl(chain: Chain, hash: String) -> BlockExplorerLink { let name = explorerNameOrDefault(chain: chain) let explorer = Gemstone.Explorer(chain: chain.id) - if let swapProvider, let url = explorer.getTransactionSwapUrl( - explorerName: name, - transactionId: hash, - providerId: swapProvider - ) { - return BlockExplorerLink(name: url.name, link: url.url) - } let url = URL(string: explorer.getTransactionUrl(explorerName: name, transactionId: hash))! return BlockExplorerLink(name: name, link: url.absoluteString) } + public func swapIdentifier( + chain: Chain, + provider: String, + hash: String, + recipient: String? = nil + ) -> String { + switch provider.lowercased() { + case SwapProvider.nearIntents.rawValue.lowercased(): + if let recipient = recipient?.trimmingCharacters(in: .whitespacesAndNewlines), !recipient.isEmpty { + return recipient + } + return hash + default: + return hash + } + } + + public func swapTransactionUrl(chain: Chain, provider: String, identifier: String) -> BlockExplorerLink? { + let name = explorerNameOrDefault(chain: chain) + let explorer = Gemstone.Explorer(chain: chain.id) + guard let url = explorer.getTransactionSwapUrl( + explorerName: name, + transactionId: identifier, + providerId: provider + ) else { + return nil + } + + return BlockExplorerLink(name: url.name, link: url.url) + } + public func addressUrl(chain: Chain, address: String) -> BlockExplorerLink { let name = explorerNameOrDefault(chain: chain) let explorer = Gemstone.Explorer(chain: chain.id) diff --git a/Packages/Primitives/Sources/Protocols/ExplorerAdressFetchable.swift b/Packages/Primitives/Sources/Protocols/ExplorerAdressFetchable.swift index 8f5b2dda2..62d6bf5f4 100644 --- a/Packages/Primitives/Sources/Protocols/ExplorerAdressFetchable.swift +++ b/Packages/Primitives/Sources/Protocols/ExplorerAdressFetchable.swift @@ -4,5 +4,45 @@ import Foundation public protocol ExplorerLinkFetchable: Sendable { func addressUrl(chain: Chain, address: String) -> BlockExplorerLink - func transactionUrl(chain: Chain, hash: String, swapProvider: String?) -> BlockExplorerLink + func transactionUrl(chain: Chain, hash: String) -> BlockExplorerLink + func swapTransactionUrl(chain: Chain, provider: String, identifier: String) -> BlockExplorerLink? + func swapIdentifier(chain: Chain, provider: String, hash: String, recipient: String?) -> String +} + +public extension ExplorerLinkFetchable { + func swapIdentifier( + chain: Chain, + provider: String, + hash: String, + recipient: String? + ) -> String { + hash + } + + func transactionLink( + chain: Chain, + provider: String?, + hash: String, + recipient: String? + ) -> BlockExplorerLink { + guard + let provider, + !provider.isEmpty + else { + return transactionUrl(chain: chain, hash: hash) + } + + let identifier = swapIdentifier( + chain: chain, + provider: provider, + hash: hash, + recipient: recipient + ) + + if let link = swapTransactionUrl(chain: chain, provider: provider, identifier: identifier) { + return link + } + + return transactionUrl(chain: chain, hash: hash) + } } diff --git a/Packages/Primitives/TestKit/ExplorerLinkFetchable+PrimitivesTestKit.swift b/Packages/Primitives/TestKit/ExplorerLinkFetchable+PrimitivesTestKit.swift index a779cfe55..7841c7843 100644 --- a/Packages/Primitives/TestKit/ExplorerLinkFetchable+PrimitivesTestKit.swift +++ b/Packages/Primitives/TestKit/ExplorerLinkFetchable+PrimitivesTestKit.swift @@ -13,10 +13,19 @@ public struct MockExplorerLink: ExplorerLinkFetchable { ) } - public func transactionUrl(chain: Chain, hash: String, swapProvider: String? = .none) -> BlockExplorerLink { + public func transactionUrl(chain: Chain, hash: String) -> BlockExplorerLink { BlockExplorerLink( name: "MockExplorer", link: "https://mock.explorer/\(chain.rawValue)/tx/\(hash)" ) } + + public func swapTransactionUrl(chain: Chain, provider: String, identifier: String) -> BlockExplorerLink? { + BlockExplorerLink( + name: "MockExplorer", + link: "https://mock.explorer/\(chain.rawValue)/swap/\(provider)/\(identifier)" + ) + } + + public func swapIdentifier(chain: Chain, provider: String, hash: String, recipient: String?) -> String { recipient ?? hash } } diff --git a/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift b/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift index af8a4b348..cdb408b71 100644 --- a/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift +++ b/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift @@ -36,9 +36,15 @@ private struct ExplorerMock: ExplorerLinkFetchable { func addressUrl(chain: Chain, address: String) -> BlockExplorerLink { .init(name: "", link: "") } - func transactionUrl(chain: Chain, hash: String, swapProvider: String?) -> BlockExplorerLink { + func transactionUrl(chain: Chain, hash: String) -> BlockExplorerLink { .init(name: "", link: "") } + + func swapTransactionUrl(chain: Chain, provider: String, identifier: String) -> BlockExplorerLink? { + .init(name: "", link: "") + } + + func swapIdentifier(chain: Chain, provider: String, hash: String, recipient: String?) -> String { hash } } #Preview { diff --git a/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift b/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift index 7251579a7..334c05acf 100644 --- a/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift +++ b/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift @@ -328,17 +328,21 @@ public struct TransactionViewModel: Sendable { } private var transactionLink: BlockExplorerLink { - let swapProvider: String? = switch transaction.transaction.metadata { - case .swap(let metadata): metadata.provider - default: .none - } - return explorerService.transactionUrl( + explorerService.transactionLink( chain: assetId.chain, + provider: transaction.transaction.swapProvider, hash: transaction.transaction.hash, - swapProvider: swapProvider + recipient: transaction.transaction.to ) } private var addressLink: BlockExplorerLink { explorerService.addressUrl(chain: assetId.chain, address: participant) } private var assetId: AssetId { transaction.transaction.assetId } } + +public extension Primitives.Transaction { + var swapProvider: String? { + guard case let .swap(metadata) = metadata else { return nil } + return metadata.provider + } +} diff --git a/core b/core index f6df16f40..6b2ec2604 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit f6df16f40d5a3e441d87e66404006e5fdcf60f7e +Subproject commit 6b2ec2604a4d526f56b489a03a3bc068f09b4af7 From b27c50c6ac03644b81544b05bf86f43c5d7d50ca Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:51:18 +0900 Subject: [PATCH 08/12] refactor monitorSwapStatuses --- .../ViewModels/DeveloperViewModel.swift | 6 +- .../TransactionSceneViewModel.swift | 63 +------- .../TransactionSceneViewModelTests.swift | 82 ++++++----- .../TransactionsNavigationStack.swift | 3 +- .../Wallet/WalletNavigationStack.swift | 3 +- Gem/Services/ServicesFactory.swift | 12 +- Packages/FeatureServices/Package.swift | 2 + .../TestKit/TransactionsService+TestKit.swift | 16 ++- .../TransactionsService.swift | 134 +++++++++++++++++- .../Sources/TransactionMetadataTypes.swift | 4 +- .../Primitives/Sources/TransferDataType.swift | 3 +- ...actionSwapMetadata+PrimitivesTestKit.swift | 6 +- .../Types/TransactionHeaderBuilder.swift | 3 +- .../Sources/Stores/TransactionStore.swift | 26 ++++ core | 2 +- 15 files changed, 250 insertions(+), 115 deletions(-) diff --git a/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift b/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift index 247a4eea8..dc608bab4 100644 --- a/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift +++ b/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift @@ -166,7 +166,8 @@ public final class DeveloperViewModel { fromValue: BigInt(2767611111).description, toAsset: AssetId.init(chain: .solana), toValue: BigInt(812312312).description, - provider: .none + provider: .none, + swapResult: .none ) ), createdAt: Date().addingTimeInterval(-122223) @@ -235,7 +236,8 @@ public final class DeveloperViewModel { fromValue: BigInt(276767623311111111).description, toAsset: AssetId.init(chain: .bitcoin), toValue: BigInt(32312312).description, - provider: .none + provider: .none, + swapResult: .none ) ), createdAt: Date().addingTimeInterval(-1344411) diff --git a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift index 2935f931a..16a92028e 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionSceneViewModel.swift @@ -8,7 +8,6 @@ import Preferences import Primitives import PrimitivesComponents import Store -import SwapService import SwiftUI @Observable @@ -16,30 +15,25 @@ import SwiftUI public final class TransactionSceneViewModel { private let preferences: Preferences private let explorerService: ExplorerService - private let swapTransactionService: any SwapStatusProviding var request: TransactionRequest var transactionExtended: TransactionExtended - var swapResult: SwapResult? + var swapResult: SwapResult? { swapMetadata?.swapResult } var isPresentingShareSheet = false var isPresentingInfoSheet: InfoSheetType? = .none - private var swapStatusTask: Task? var isPresentingTransactionSheet: TransactionSheetType? public init( transaction: TransactionExtended, walletId: String, preferences: Preferences = Preferences.standard, - explorerService: ExplorerService = ExplorerService.standard, - swapTransactionService: any SwapStatusProviding + explorerService: ExplorerService = ExplorerService.standard ) { self.preferences = preferences self.explorerService = explorerService - self.swapTransactionService = swapTransactionService self.transactionExtended = transaction self.request = TransactionRequest(walletId: walletId, transactionId: transaction.id) - startSwapStatusUpdates() } var title: String { model.titleTextValue.text } @@ -86,7 +80,6 @@ extension TransactionSceneViewModel { func onChangeTransaction(_ oldValue: TransactionExtended, _ newValue: TransactionExtended) { if oldValue != newValue { transactionExtended = newValue - restartSwapStatusUpdates() } } @@ -168,58 +161,6 @@ extension TransactionSceneViewModel { return metadata.fromAsset.chain != metadata.toAsset.chain } - private var swapProviderIdentifier: String? { - swapMetadata?.provider - } - - private func restartSwapStatusUpdates() { - swapStatusTask?.cancel() - swapStatusTask = nil - swapResult = nil - startSwapStatusUpdates() - } - - private func startSwapStatusUpdates() { - guard swapStatusTask == nil else { return } - guard isCrossChainSwap, let provider = swapProviderIdentifier else { return } - - let chain = transactionExtended.transaction.assetId.chain - let transactionId = transactionExtended.transaction.hash - // Stellar might be different here - let memo = transactionExtended.transaction.memo ?? transactionExtended.transaction.to - - swapStatusTask = Task { [weak self] in - await self?.pollSwapStatus(provider: provider, chain: chain, transactionId: transactionId, memo: memo) - } - } - - private func pollSwapStatus(provider: String, chain: Chain, transactionId: String, memo: String?) async { - defer { swapStatusTask = nil } - - var backoff: Duration = .seconds(5) - - while !Task.isCancelled { - do { - let result = try await swapTransactionService.getSwapResult( - providerId: provider, - chain: chain, - transactionId: transactionId, - memo: memo - ) - swapResult = result - if result.status != .pending { break } - backoff = .seconds(5) - } catch { - NSLog("TransactionSceneViewModel swap status error: \(error)") - try? await Task.sleep(for: backoff) - backoff = min(backoff * 2, .seconds(300)) - continue - } - - try? await Task.sleep(for: .seconds(30)) - } - } - var feeDetailsViewModel: NetworkFeeSceneViewModel { NetworkFeeSceneViewModel( chain: model.transaction.transaction.assetId.chain, diff --git a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift index e3d9741e8..104492185 100644 --- a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift @@ -7,7 +7,6 @@ import PreferencesTestKit import PrimitivesComponents import Style import Components -import SwapService @testable import Transactions @testable import Store @@ -128,6 +127,35 @@ struct TransactionSceneViewModelTests { } + @Test + func swapStatusLoadsPersistedResult() { + let expected = SwapResult( + status: .completed, + fromChain: .ethereum, + fromTxHash: "0xsource", + toChain: .some(.arbitrum), + toTxHash: "0xdestination" + ) + + let metadata = TransactionMetadata.swap( + TransactionSwapMetadata( + fromAsset: .mock(.ethereum), + fromValue: "1", + toAsset: .mock(.arbitrum), + toValue: "1", + provider: "across", + swapResult: expected + ) + ) + + let model = TransactionSceneViewModel.mock( + type: .swap, + metadata: metadata + ) + + #expect(model.swapResult == expected) + } + @Test func participantItemModel() { let transaction = TransactionExtended.mock( @@ -142,7 +170,7 @@ struct TransactionSceneViewModelTests { transaction: transaction, walletId: "test_wallet_id", preferences: Preferences.standard, - swapTransactionService: SwapStatusProviderMock.noop + explorerService: ExplorerService.standard ) if case .participant(let item) = modelWithAddresses.item(for: TransactionItem.participant) { @@ -271,42 +299,24 @@ extension TransactionSceneViewModel { toAddress: String = "participant_address", memo: String? = nil, createdAt: Date = Date(), - metadata: TransactionMetadata? = nil, - swapStatusProvider: SwapStatusProviderMock = .noop + metadata: TransactionMetadata? = nil ) -> TransactionSceneViewModel { - TransactionSceneViewModel( - transaction: TransactionExtended.mock( - transaction: Transaction.mock( - type: type, - state: state, - direction: direction, - assetId: assetId, - to: toAddress, - memo: memo, - metadata: metadata - ) - ), - walletId: "test_wallet_id", - preferences: Preferences.standard, - swapTransactionService: swapStatusProvider + let transactionExtended = TransactionExtended.mock( + transaction: Transaction.mock( + type: type, + state: state, + direction: direction, + assetId: assetId, + to: toAddress, + memo: memo, + metadata: metadata + ) ) - } -} - -fileprivate struct SwapStatusProviderMock: SwapStatusProviding { - private let handler: @Sendable (String?, Chain, String, String?) async throws -> SwapResult - init(handler: @escaping @Sendable (String?, Chain, String, String?) async throws -> SwapResult) { - self.handler = handler - } - - static var noop: SwapStatusProviderMock { - SwapStatusProviderMock { _, _, _, _ in - SwapResult(status: .pending, fromChain: .ethereum, fromTxHash: "", toChain: nil, toTxHash: nil) - } - } - - func getSwapResult(providerId: String?, chain: Chain, transactionId: String, memo: String?) async throws -> SwapResult { - try await handler(providerId, chain, transactionId, memo) + return TransactionSceneViewModel( + transaction: transactionExtended, + walletId: "test_wallet_id", + preferences: Preferences.standard + ) } } diff --git a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift index 7692f56d3..a5da34031 100644 --- a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift +++ b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift @@ -58,8 +58,7 @@ struct TransactionsNavigationStack: View { TransactionNavigationView( model: TransactionSceneViewModel( transaction: $0, - walletId: model.wallet.id, - swapTransactionService: swapTransactionService + walletId: model.wallet.id ) ) } diff --git a/Gem/Navigation/Wallet/WalletNavigationStack.swift b/Gem/Navigation/Wallet/WalletNavigationStack.swift index 1bedb4186..bac2b5ebe 100644 --- a/Gem/Navigation/Wallet/WalletNavigationStack.swift +++ b/Gem/Navigation/Wallet/WalletNavigationStack.swift @@ -112,8 +112,7 @@ struct WalletNavigationStack: View { TransactionNavigationView( model: TransactionSceneViewModel( transaction: $0, - walletId: model.wallet.id, - swapTransactionService: swapTransactionService + walletId: model.wallet.id ) ) } diff --git a/Gem/Services/ServicesFactory.swift b/Gem/Services/ServicesFactory.swift index 49d9789ca..903f77626 100644 --- a/Gem/Services/ServicesFactory.swift +++ b/Gem/Services/ServicesFactory.swift @@ -83,13 +83,16 @@ struct ServicesFactory { nftStore: storeManager.nftStore, deviceService: deviceService ) + let swapTransactionService = SwapTransactionService(nodeProvider: nodeService) let transactionsService = Self.makeTransactionsService( transactionStore: storeManager.transactionStore, assetsService: assetsService, walletStore: storeManager.walletStore, deviceService: deviceService, - addressStore: storeManager.addressStore + addressStore: storeManager.addressStore, + swapTransactionService: swapTransactionService ) + transactionsService.monitorPendingSwapStatuses() let transactionService = Self.makeTransactionService( transactionStore: storeManager.transactionStore, stakeService: stakeService, @@ -121,7 +124,6 @@ struct ServicesFactory { ) let explorerService = ExplorerService.standard let swapService = SwapService(nodeProvider: nodeService) - let swapTransactionService = SwapTransactionService(nodeProvider: nodeService) let presenter = WalletConnectorPresenter() let walletConnectorManager = WalletConnectorManager(presenter: presenter) @@ -327,14 +329,16 @@ extension ServicesFactory { assetsService: AssetsService, walletStore: WalletStore, deviceService: any DeviceServiceable, - addressStore: AddressStore + addressStore: AddressStore, + swapTransactionService: SwapTransactionService ) -> TransactionsService { TransactionsService( transactionStore: transactionStore, assetsService: assetsService, walletStore: walletStore, deviceService: deviceService, - addressStore: addressStore + addressStore: addressStore, + swapTransactionService: swapTransactionService ) } diff --git a/Packages/FeatureServices/Package.swift b/Packages/FeatureServices/Package.swift index 2aaedcb5c..559997b18 100644 --- a/Packages/FeatureServices/Package.swift +++ b/Packages/FeatureServices/Package.swift @@ -227,6 +227,7 @@ let package = Package( "Preferences", "AssetsService", "DeviceService", + "SwapService" ], path: "TransactionsService", exclude: ["TestKit"] @@ -238,6 +239,7 @@ let package = Package( "AssetsServiceTestKit", "TransactionsService", "DeviceServiceTestKit", + "SwapService" ], path: "TransactionsService/TestKit" ), diff --git a/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift b/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift index a9141a630..1f4d7e2ed 100644 --- a/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift +++ b/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift @@ -5,6 +5,8 @@ import TransactionsService import StoreTestKit import AssetsServiceTestKit import DeviceServiceTestKit +import SwapService +import Primitives public extension TransactionsService { static func mock() -> TransactionsService { @@ -13,7 +15,19 @@ public extension TransactionsService { assetsService: .mock(), walletStore: .mock(), deviceService: DeviceServiceMock(), - addressStore: .mock() + addressStore: .mock(), + swapTransactionService: SwapStatusProvidingMock() ) } } + +private struct SwapStatusProvidingMock: SwapStatusProviding { + func getSwapResult( + providerId: String?, + chain: Chain, + transactionId: String, + memo: String? + ) async throws -> SwapResult { + SwapResult(status: .pending, fromChain: chain, fromTxHash: transactionId, toChain: nil, toTxHash: nil) + } +} diff --git a/Packages/FeatureServices/TransactionsService/TransactionsService.swift b/Packages/FeatureServices/TransactionsService/TransactionsService.swift index a2dff98fb..a53301c80 100644 --- a/Packages/FeatureServices/TransactionsService/TransactionsService.swift +++ b/Packages/FeatureServices/TransactionsService/TransactionsService.swift @@ -7,6 +7,7 @@ import Store import Preferences import AssetsService import DeviceService +import SwapService public final class TransactionsService: Sendable { let provider: any GemAPITransactionService @@ -15,6 +16,8 @@ public final class TransactionsService: Sendable { let walletStore: WalletStore private let deviceService: any DeviceServiceable private let addressStore: AddressStore + private let swapTransactionService: any SwapStatusProviding + private let swapStatusTasks = SwapStatusTaskManager() public init( provider: any GemAPITransactionService = GemAPIService(), @@ -22,7 +25,8 @@ public final class TransactionsService: Sendable { assetsService: AssetsService, walletStore: WalletStore, deviceService: any DeviceServiceable, - addressStore: AddressStore + addressStore: AddressStore, + swapTransactionService: any SwapStatusProviding ) { self.provider = provider self.transactionStore = transactionStore @@ -30,6 +34,7 @@ public final class TransactionsService: Sendable { self.walletStore = walletStore self.deviceService = deviceService self.addressStore = addressStore + self.swapTransactionService = swapTransactionService } public func updateAll(walletId: WalletId) async throws { @@ -51,6 +56,7 @@ public final class TransactionsService: Sendable { try addressStore.addAddressNames(response.addressNames) store.transactionsTimestamp = newTimestamp + monitorSwapStatuses(for: response.transactions) } public func updateForAsset(wallet: Wallet, assetId: AssetId) async throws { @@ -72,16 +78,44 @@ public final class TransactionsService: Sendable { try addressStore.addAddressNames(response.addressNames) store.setTransactionsForAssetTimestamp(assetId: assetId.identifier, value: newTimestamp) + monitorSwapStatuses(for: response.transactions) } public func addTransaction(walletId: WalletId, transaction: Transaction) throws { try transactionStore.addTransactions(walletId: walletId.id, transactions: [transaction]) + monitorSwapStatuses(for: [transaction]) + } + + public func updateSwapResult(transactionId: String, swapResult: SwapResult) throws { + let record = try transactionStore.getTransactionRecord(transactionId: transactionId) + guard let metadata = record.metadata else { return } + + guard case let .swap(swapMetadata) = metadata else { + return + } + + let updatedMetadata = TransactionMetadata.swap( + TransactionSwapMetadata( + fromAsset: swapMetadata.fromAsset, + fromValue: swapMetadata.fromValue, + toAsset: swapMetadata.toAsset, + toValue: swapMetadata.toValue, + provider: swapMetadata.provider, + swapResult: swapResult + ) + ) + try transactionStore.updateMetadata(transactionId: transactionId, metadata: updatedMetadata) } public func getTransaction(walletId: WalletId, transactionId: String) throws -> TransactionExtended { try transactionStore.getTransaction(walletId: walletId.id, transactionId: transactionId) } + public func monitorPendingSwapStatuses() { + guard let pending = try? transactionStore.getSwapTransactionsNeedingStatusUpdate() else { return } + monitorSwapStatuses(for: pending.map(\.transaction)) + } + private func prefetchAssets(walletId: WalletId, transactions: [Transaction]) async throws { let assetIds = transactions.map { $0.assetIds }.flatMap { $0 } if assetIds.isEmpty { @@ -90,4 +124,102 @@ public final class TransactionsService: Sendable { let newAssets = try await assetsService.prefetchAssets(assetIds: assetIds) try assetsService.addBalancesIfMissing(walletId: walletId, assetIds: newAssets) } + + private func monitorSwapStatuses(for transactions: [Transaction]) { + guard !transactions.isEmpty else { return } + + Task { [weak self] in + guard let self else { return } + for transaction in transactions { + guard let context = self.makeSwapStatusContext(for: transaction) else { continue } + await self.swapStatusTasks.enqueue(id: transaction.id) { + await self.trackSwapStatus(using: context, transactionId: transaction.id) + } + } + } + } + + private func trackSwapStatus(using context: SwapStatusContext, transactionId: String) async { + var backoff: Duration = .seconds(5) + + while !Task.isCancelled { + do { + let result = try await swapTransactionService.getSwapResult( + providerId: context.provider, + chain: context.chain, + transactionId: context.transactionId, + memo: context.identifier + ) + + do { + try updateSwapResult(transactionId: transactionId, swapResult: result) + } catch { + NSLog("TransactionsService update swap result error: \(error)") + } + + if result.status != .pending { + break + } + + backoff = .seconds(5) + } catch { + NSLog("TransactionsService swap status error: \(error)") + try? await Task.sleep(for: backoff) + backoff = min(backoff * 2, .seconds(300)) + continue + } + + try? await Task.sleep(for: .seconds(30)) + } + } + + private func makeSwapStatusContext(for transaction: Transaction) -> SwapStatusContext? { + guard case let .swap(metadata) = transaction.metadata else { return nil } + guard let provider = metadata.provider, !provider.isEmpty else { return nil } + + if metadata.fromAsset.chain == metadata.toAsset.chain { + return nil + } + + if let swapResult = metadata.swapResult, swapResult.status != .pending { + return nil + } + + let identifier: String? + if provider.lowercased() == SwapProvider.nearIntents.rawValue.lowercased() { + identifier = transaction.to + } else { + identifier = transaction.memo + } + + return SwapStatusContext( + provider: provider, + chain: transaction.assetId.chain, + transactionId: transaction.hash, + identifier: identifier + ) + } +} + +private struct SwapStatusContext { + let provider: String + let chain: Chain + let transactionId: String + let identifier: String? +} + +private actor SwapStatusTaskManager { + private var tasks: [String: Task] = [:] + + func enqueue(id: String, operation: @escaping () async -> Void) { + guard tasks[id] == nil else { return } + tasks[id] = Task { + await operation() + await self.removeTask(id: id) + } + } + + private func removeTask(id: String) { + tasks[id] = nil + } } diff --git a/Packages/Primitives/Sources/TransactionMetadataTypes.swift b/Packages/Primitives/Sources/TransactionMetadataTypes.swift index 79087a1df..888bc2e7d 100644 --- a/Packages/Primitives/Sources/TransactionMetadataTypes.swift +++ b/Packages/Primitives/Sources/TransactionMetadataTypes.swift @@ -34,12 +34,14 @@ public struct TransactionSwapMetadata: Codable, Sendable { public let toAsset: AssetId public let toValue: String public let provider: String? + public let swapResult: SwapResult? - public init(fromAsset: AssetId, fromValue: String, toAsset: AssetId, toValue: String, provider: String?) { + public init(fromAsset: AssetId, fromValue: String, toAsset: AssetId, toValue: String, provider: String?, swapResult: SwapResult?) { self.fromAsset = fromAsset self.fromValue = fromValue self.toAsset = toAsset self.toValue = toValue self.provider = provider + self.swapResult = swapResult } } diff --git a/Packages/Primitives/Sources/TransferDataType.swift b/Packages/Primitives/Sources/TransferDataType.swift index 19b355a7e..6a7cab660 100644 --- a/Packages/Primitives/Sources/TransferDataType.swift +++ b/Packages/Primitives/Sources/TransferDataType.swift @@ -69,7 +69,8 @@ public enum TransferDataType: Hashable, Equatable, Sendable { fromValue: data.quote.fromValue, toAsset: toAsset.id, toValue: data.quote.toValue, - provider: data.quote.providerData.provider.rawValue + provider: data.quote.providerData.provider.rawValue, + swapResult: nil ) ) case .transferNft(let asset): diff --git a/Packages/Primitives/TestKit/TransactionSwapMetadata+PrimitivesTestKit.swift b/Packages/Primitives/TestKit/TransactionSwapMetadata+PrimitivesTestKit.swift index b81cbd4e4..7aad782a2 100644 --- a/Packages/Primitives/TestKit/TransactionSwapMetadata+PrimitivesTestKit.swift +++ b/Packages/Primitives/TestKit/TransactionSwapMetadata+PrimitivesTestKit.swift @@ -9,14 +9,16 @@ public extension TransactionSwapMetadata { fromValue: String = "", toAsset: AssetId = .mock(.smartChain), toValue: String = "", - provider: String? = nil + provider: String? = nil, + swapResult: SwapResult? = nil ) -> TransactionSwapMetadata { TransactionSwapMetadata( fromAsset: fromAsset, fromValue: fromValue, toAsset: toAsset, toValue: toValue, - provider: provider + provider: provider, + swapResult: swapResult ) } } diff --git a/Packages/PrimitivesComponents/Sources/Types/TransactionHeaderBuilder.swift b/Packages/PrimitivesComponents/Sources/Types/TransactionHeaderBuilder.swift index 5eecc635c..6373cf291 100644 --- a/Packages/PrimitivesComponents/Sources/Types/TransactionHeaderBuilder.swift +++ b/Packages/PrimitivesComponents/Sources/Types/TransactionHeaderBuilder.swift @@ -86,7 +86,8 @@ public struct TransactionHeaderTypeBuilder { fromValue: data.quote.fromValue, toAsset: toAsset.id, toValue: data.quote.toValue, - provider: data.quote.providerData.provider.rawValue + provider: data.quote.providerData.provider.rawValue, + swapResult: nil )) ) ) diff --git a/Packages/Store/Sources/Stores/TransactionStore.swift b/Packages/Store/Sources/Stores/TransactionStore.swift index 289d4edae..9b8ad1d21 100644 --- a/Packages/Store/Sources/Stores/TransactionStore.swift +++ b/Packages/Store/Sources/Stores/TransactionStore.swift @@ -34,6 +34,18 @@ public struct TransactionStore: Sendable { } } + public func getSwapTransactionsNeedingStatusUpdate() throws -> [TransactionWallet] { + try db.read { db in + try TransactionRecord + .including(required: TransactionRecord.wallet) + .filter(TransactionRecord.Columns.type == TransactionType.swap.rawValue) + .asRequest(of: WalletTransactionInfo.self) + .fetchAll(db) + .map(\.transactionWallet) + .filter(\.needsSwapStatusUpdate) + } + } + public func getTransactionRecord(transactionId: String) throws -> TransactionRecord { try db.read { db in guard let transaction = try TransactionRecord @@ -161,3 +173,17 @@ public struct TransactionStore: Sendable { } } } + +private extension TransactionWallet { + var needsSwapStatusUpdate: Bool { + guard case let .swap(metadata) = transaction.metadata else { return false } + guard let provider = metadata.provider, !provider.isEmpty else { return false } + if metadata.fromAsset.chain == metadata.toAsset.chain { + return false + } + if let swapResult = metadata.swapResult, swapResult.status != .pending { + return false + } + return true + } +} diff --git a/core b/core index 6b2ec2604..07e8b3bea 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 6b2ec2604a4d526f56b489a03a3bc068f09b4af7 +Subproject commit 07e8b3bea839ddf17f225aea3dcb4ca3329d2be8 From 6aaa47200086d02b0a533103bc2b205d94c01352 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:21:03 +0900 Subject: [PATCH 09/12] Check provider mode and fix tests --- .../TransactionSceneViewModelTests.swift | 23 ++++++++++--------- .../TransactionViewModelTests.swift | 6 +++-- .../SwapService/SwapTransactionService.swift | 9 ++++++++ .../SwapperProviderType+TestKit.swift | 3 ++- .../TransactionsService.swift | 12 +++++++--- .../StoreTests/TransactionStoreTests.swift | 4 ++-- core | 2 +- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift index 104492185..1c6a277f1 100644 --- a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionSceneViewModelTests.swift @@ -7,6 +7,7 @@ import PreferencesTestKit import PrimitivesComponents import Style import Components +import ExplorerService @testable import Transactions @testable import Store @@ -93,7 +94,7 @@ struct TransactionSceneViewModelTests { } @Test - func swapStatusReflectsSwapResult() async { + func swapStatusReflectsSwapResult() { let swapResult = SwapResult( status: .completed, fromChain: .ethereum, @@ -102,22 +103,22 @@ struct TransactionSceneViewModelTests { toTxHash: "0xdestination" ) - let metadata = TransactionSwapMetadata( - fromAsset: AssetId(chain: .ethereum, tokenId: nil), - fromValue: "1", - toAsset: AssetId(chain: .arbitrum, tokenId: nil), - toValue: "1", - provider: "across" + let metadata = TransactionMetadata.swap( + TransactionSwapMetadata( + fromAsset: AssetId(chain: .ethereum, tokenId: nil), + fromValue: "1", + toAsset: AssetId(chain: .arbitrum, tokenId: nil), + toValue: "1", + provider: "across", + swapResult: swapResult + ) ) let swapModel = TransactionSceneViewModel.mock( type: .swap, - metadata: .swap(metadata), - swapStatusProvider: .init { _, _, _, _ in swapResult } + metadata: metadata ) - await Task.yield() - if case .listItem(let item) = swapModel.item(for: TransactionItem.swapStatus) { #expect(item.title == Localized.Transaction.swapStatus) #expect(item.subtitle == Localized.Transaction.Status.confirmed) diff --git a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift index b1a3d0bae..53358820d 100644 --- a/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift +++ b/Features/Transactions/Tests/TransactionsTests/ViewModels/TransactionViewModelTests.swift @@ -106,7 +106,8 @@ extension TransactionViewModel { memo: String? = nil, fromAddress: AddressName? = nil, toAddress: AddressName? = nil, - swapProvider: String? = nil + swapProvider: String? = nil, + swapResult: SwapResult? = nil ) -> TransactionViewModel { let fromAsset = Asset.mockEthereum() let toAsset = Asset.mockEthereumUSDT() @@ -117,7 +118,8 @@ extension TransactionViewModel { fromValue: fromValue, toAsset: toAsset.id, toValue: toValue, - provider: swapProvider + provider: swapProvider, + swapResult: swapResult ) ) diff --git a/Packages/FeatureServices/SwapService/SwapTransactionService.swift b/Packages/FeatureServices/SwapService/SwapTransactionService.swift index 88910c366..0535d00d7 100644 --- a/Packages/FeatureServices/SwapService/SwapTransactionService.swift +++ b/Packages/FeatureServices/SwapService/SwapTransactionService.swift @@ -16,6 +16,15 @@ public protocol SwapStatusProviding: Sendable { transactionId: String, memo: String? ) async throws -> SwapResult + + func shouldUpdate(id: SwapProvider) -> Bool +} + +public extension SwapStatusProviding { + func shouldUpdate(id: SwapProvider) -> Bool { + let providerConfig = SwapProviderConfig.fromString(id: id.rawValue) + return providerConfig.inner().mode != .onChain + } } public struct SwapTransactionService: SwapStatusProviding, Sendable { diff --git a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift index 620c518f2..f86f21005 100644 --- a/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift +++ b/Packages/FeatureServices/SwapService/TestKit/Types+Mock/SwapperProviderType+TestKit.swift @@ -10,7 +10,8 @@ public extension SwapperProviderType { id: .pancakeswapV3, name: "PancakeSwap", protocol: "v3", - protocolId: "pancakeswap_v3" + protocolId: "pancakeswap_v3", + mode: .onChain ) } } diff --git a/Packages/FeatureServices/TransactionsService/TransactionsService.swift b/Packages/FeatureServices/TransactionsService/TransactionsService.swift index a53301c80..b4b94a2cd 100644 --- a/Packages/FeatureServices/TransactionsService/TransactionsService.swift +++ b/Packages/FeatureServices/TransactionsService/TransactionsService.swift @@ -163,7 +163,7 @@ public final class TransactionsService: Sendable { backoff = .seconds(5) } catch { - NSLog("TransactionsService swap status error: \(error)") + NSLog("TransactionsService swap status \(context) error: \(error)") try? await Task.sleep(for: backoff) backoff = min(backoff * 2, .seconds(300)) continue @@ -175,7 +175,13 @@ public final class TransactionsService: Sendable { private func makeSwapStatusContext(for transaction: Transaction) -> SwapStatusContext? { guard case let .swap(metadata) = transaction.metadata else { return nil } - guard let provider = metadata.provider, !provider.isEmpty else { return nil } + guard + let provider = metadata.provider, + let providerType = SwapProvider(rawValue: provider), + swapTransactionService.shouldUpdate(id: providerType) + else { + return nil + } if metadata.fromAsset.chain == metadata.toAsset.chain { return nil @@ -215,7 +221,7 @@ private actor SwapStatusTaskManager { guard tasks[id] == nil else { return } tasks[id] = Task { await operation() - await self.removeTask(id: id) + self.removeTask(id: id) } } diff --git a/Packages/Store/Tests/StoreTests/TransactionStoreTests.swift b/Packages/Store/Tests/StoreTests/TransactionStoreTests.swift index 81f372b5d..87548f452 100644 --- a/Packages/Store/Tests/StoreTests/TransactionStoreTests.swift +++ b/Packages/Store/Tests/StoreTests/TransactionStoreTests.swift @@ -34,7 +34,7 @@ struct TransactionStoreTests { type: .swap, assetId: btc, metadata: .swap(TransactionSwapMetadata( - fromAsset: btc, fromValue: "100", toAsset: eth, toValue: "200", provider: nil + fromAsset: btc, fromValue: "100", toAsset: eth, toValue: "200", provider: nil, swapResult: nil )) ) ]) @@ -45,7 +45,7 @@ struct TransactionStoreTests { type: .swap, assetId: btc, metadata: .swap(TransactionSwapMetadata( - fromAsset: btc, fromValue: "100", toAsset: sol, toValue: "300", provider: nil + fromAsset: btc, fromValue: "100", toAsset: sol, toValue: "300", provider: nil, swapResult: nil )) ) ]) diff --git a/core b/core index 07e8b3bea..94988db32 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 07e8b3bea839ddf17f225aea3dcb4ca3329d2be8 +Subproject commit 94988db32616212ed6f88724519b1b6a122f69fa From da6f1c9dde7c82da4fbec92b8b0ba6aca3f94dfb Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:51:30 +0900 Subject: [PATCH 10/12] code cleanup --- .../SwapService/SwapTransactionService.swift | 41 ++++++------------- .../TestKit/TransactionsService+TestKit.swift | 4 +- .../TransactionsService.swift | 20 +++------ core | 2 +- 4 files changed, 21 insertions(+), 46 deletions(-) diff --git a/Packages/FeatureServices/SwapService/SwapTransactionService.swift b/Packages/FeatureServices/SwapService/SwapTransactionService.swift index 0535d00d7..66c08d296 100644 --- a/Packages/FeatureServices/SwapService/SwapTransactionService.swift +++ b/Packages/FeatureServices/SwapService/SwapTransactionService.swift @@ -11,20 +11,11 @@ import Primitives public protocol SwapStatusProviding: Sendable { func getSwapResult( - providerId: String?, + providerId: SwapProvider, chain: Primitives.Chain, transactionId: String, - memo: String? + recipient: String ) async throws -> SwapResult - - func shouldUpdate(id: SwapProvider) -> Bool -} - -public extension SwapStatusProviding { - func shouldUpdate(id: SwapProvider) -> Bool { - let providerConfig = SwapProviderConfig.fromString(id: id.rawValue) - return providerConfig.inner().mode != .onChain - } } public struct SwapTransactionService: SwapStatusProviding, Sendable { @@ -34,22 +25,20 @@ public struct SwapTransactionService: SwapStatusProviding, Sendable { swapper = GemSwapper(rpcProvider: NativeProvider(nodeProvider: nodeProvider)) } + func shouldUpdate(id: SwapProvider) -> Bool { + let providerConfig = SwapProviderConfig.fromString(id: id.rawValue) + return providerConfig.inner().mode != .onChain + } + public func getSwapResult( - providerId: String?, + providerId: SwapProvider, chain: Primitives.Chain, transactionId: String, - memo: String? + recipient: String ) async throws -> SwapResult { - guard let providerId, !providerId.isEmpty else { - throw AnyError("Swap provider is missing") - } - - guard let swapProvider = providerId.toSwapperProvider() else { - throw AnyError("Invalid swap provider: \(providerId)") - } - - let transactionHash = if swapProvider == .nearIntents, let memo, !memo.isEmpty { - memo + let swapProvider = SwapProviderConfig.fromString(id: providerId.rawValue).inner().id + let transactionHash = if swapProvider == .nearIntents { + recipient } else { transactionId } @@ -62,9 +51,3 @@ public struct SwapTransactionService: SwapStatusProviding, Sendable { return try result.asPrimitives() } } - -private extension String { - func toSwapperProvider() -> SwapperProvider? { - SwapProviderConfig.fromString(id: self).inner().id - } -} diff --git a/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift b/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift index 1f4d7e2ed..a6168bb40 100644 --- a/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift +++ b/Packages/FeatureServices/TransactionsService/TestKit/TransactionsService+TestKit.swift @@ -23,10 +23,10 @@ public extension TransactionsService { private struct SwapStatusProvidingMock: SwapStatusProviding { func getSwapResult( - providerId: String?, + providerId: SwapProvider, chain: Chain, transactionId: String, - memo: String? + recipient: String ) async throws -> SwapResult { SwapResult(status: .pending, fromChain: chain, fromTxHash: transactionId, toChain: nil, toTxHash: nil) } diff --git a/Packages/FeatureServices/TransactionsService/TransactionsService.swift b/Packages/FeatureServices/TransactionsService/TransactionsService.swift index b4b94a2cd..a1db0bb90 100644 --- a/Packages/FeatureServices/TransactionsService/TransactionsService.swift +++ b/Packages/FeatureServices/TransactionsService/TransactionsService.swift @@ -148,7 +148,7 @@ public final class TransactionsService: Sendable { providerId: context.provider, chain: context.chain, transactionId: context.transactionId, - memo: context.identifier + recipient: context.recipient ) do { @@ -177,8 +177,7 @@ public final class TransactionsService: Sendable { guard case let .swap(metadata) = transaction.metadata else { return nil } guard let provider = metadata.provider, - let providerType = SwapProvider(rawValue: provider), - swapTransactionService.shouldUpdate(id: providerType) + let swapProvider = SwapProvider(rawValue: provider) else { return nil } @@ -191,27 +190,20 @@ public final class TransactionsService: Sendable { return nil } - let identifier: String? - if provider.lowercased() == SwapProvider.nearIntents.rawValue.lowercased() { - identifier = transaction.to - } else { - identifier = transaction.memo - } - return SwapStatusContext( - provider: provider, + provider: swapProvider, chain: transaction.assetId.chain, transactionId: transaction.hash, - identifier: identifier + recipient: transaction.to ) } } private struct SwapStatusContext { - let provider: String + let provider: SwapProvider let chain: Chain let transactionId: String - let identifier: String? + let recipient: String } private actor SwapStatusTaskManager { diff --git a/core b/core index 94988db32..4502dd274 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 94988db32616212ed6f88724519b1b6a122f69fa +Subproject commit 4502dd2749516b1c16f637c40c4fcfbb7f855ad8 From e45be1d5cfa07e9d1734dd027fb43ff14bc2a7bb Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:04:12 +0900 Subject: [PATCH 11/12] cleanup --- Gem/Navigation/Transactions/TransactionsNavigationStack.swift | 1 - Gem/Navigation/Wallet/WalletNavigationStack.swift | 2 -- Gem/Services/AppResolver+Services.swift | 2 -- Gem/Types/Environment.swift | 1 - 4 files changed, 6 deletions(-) diff --git a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift index a5da34031..17c29ff4e 100644 --- a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift +++ b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift @@ -18,7 +18,6 @@ struct TransactionsNavigationStack: View { @Environment(\.assetsService) private var assetsService @Environment(\.priceObserverService) private var priceObserverService @Environment(\.bannerService) private var bannerService - @Environment(\.swapTransactionService) private var swapTransactionService @State private var model: TransactionsViewModel diff --git a/Gem/Navigation/Wallet/WalletNavigationStack.swift b/Gem/Navigation/Wallet/WalletNavigationStack.swift index bac2b5ebe..e5dc290ca 100644 --- a/Gem/Navigation/Wallet/WalletNavigationStack.swift +++ b/Gem/Navigation/Wallet/WalletNavigationStack.swift @@ -14,7 +14,6 @@ import Transfer import StakeService import PriceAlerts import AssetsService -import SwapService struct WalletNavigationStack: View { @Environment(\.walletsService) private var walletsService @@ -28,7 +27,6 @@ struct WalletNavigationStack: View { @Environment(\.stakeService) private var stakeService @Environment(\.perpetualService) private var perpetualService @Environment(\.balanceService) private var balanceService - @Environment(\.swapTransactionService) private var swapTransactionService @State private var model: WalletSceneViewModel diff --git a/Gem/Services/AppResolver+Services.swift b/Gem/Services/AppResolver+Services.swift index 4b7088847..7bc8b6a76 100644 --- a/Gem/Services/AppResolver+Services.swift +++ b/Gem/Services/AppResolver+Services.swift @@ -49,7 +49,6 @@ extension AppResolver { let nftService: NFTService let avatarService: AvatarService let swapService: SwapService - let swapTransactionService: SwapTransactionService let subscriptionsService: SubscriptionService let appReleaseService: AppReleaseService let deviceObserverService: DeviceObserverService @@ -118,7 +117,6 @@ extension AppResolver { self.nftService = nftService self.avatarService = avatarService self.swapService = swapService - self.swapTransactionService = swapTransactionService self.appReleaseService = appReleaseService self.deviceObserverService = deviceObserverService self.subscriptionsService = subscriptionsService diff --git a/Gem/Types/Environment.swift b/Gem/Types/Environment.swift index 2c8da5d29..f27b49788 100644 --- a/Gem/Types/Environment.swift +++ b/Gem/Types/Environment.swift @@ -56,7 +56,6 @@ extension EnvironmentValues { @Entry var releaseService: AppReleaseService = AppResolver.main.services.appReleaseService @Entry var scanService: ScanService = AppResolver.main.services.scanService @Entry var swapService: SwapService = AppResolver.main.services.swapService - @Entry var swapTransactionService: SwapTransactionService = AppResolver.main.services.swapTransactionService @Entry var perpetualService: PerpetualService = AppResolver.main.services.perpetualService @Entry var perpetualObserverService: PerpetualObserverService = AppResolver.main.services.perpetualObserverService @Entry var transactionService: TransactionService = AppResolver.main.services.transactionService From cbda82dec0e15e8399b62344a95d68575135d898 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:24:00 +0900 Subject: [PATCH 12/12] more cleanup --- .../Transactions/TransactionsNavigationStack.swift | 1 - Gem/Navigation/Wallet/WalletNavigationStack.swift | 14 +++++++------- Gem/Services/AppResolver+Services.swift | 1 - Gem/Services/ServicesFactory.swift | 1 - 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift index 17c29ff4e..aa6d17904 100644 --- a/Gem/Navigation/Transactions/TransactionsNavigationStack.swift +++ b/Gem/Navigation/Transactions/TransactionsNavigationStack.swift @@ -8,7 +8,6 @@ import Transactions import Store import Assets import AssetsService -import SwapService struct TransactionsNavigationStack: View { @Environment(\.navigationState) private var navigationState diff --git a/Gem/Navigation/Wallet/WalletNavigationStack.swift b/Gem/Navigation/Wallet/WalletNavigationStack.swift index e5dc290ca..fa190f09c 100644 --- a/Gem/Navigation/Wallet/WalletNavigationStack.swift +++ b/Gem/Navigation/Wallet/WalletNavigationStack.swift @@ -106,14 +106,14 @@ struct WalletNavigationStack: View { ) ) } - .navigationDestination(for: TransactionExtended.self) { - TransactionNavigationView( - model: TransactionSceneViewModel( - transaction: $0, - walletId: model.wallet.id - ) + .navigationDestination(for: TransactionExtended.self) { + TransactionNavigationView( + model: TransactionSceneViewModel( + transaction: $0, + walletId: model.wallet.id ) - } + ) + } .navigationDestination(for: Scenes.Price.self) { ChartScene( model: ChartsViewModel( diff --git a/Gem/Services/AppResolver+Services.swift b/Gem/Services/AppResolver+Services.swift index 7bc8b6a76..4d303a6ab 100644 --- a/Gem/Services/AppResolver+Services.swift +++ b/Gem/Services/AppResolver+Services.swift @@ -83,7 +83,6 @@ extension AppResolver { nftService: NFTService, avatarService: AvatarService, swapService: SwapService, - swapTransactionService: SwapTransactionService, appReleaseService: AppReleaseService, subscriptionsService: SubscriptionService, deviceObserverService: DeviceObserverService, diff --git a/Gem/Services/ServicesFactory.swift b/Gem/Services/ServicesFactory.swift index 903f77626..18abd7eca 100644 --- a/Gem/Services/ServicesFactory.swift +++ b/Gem/Services/ServicesFactory.swift @@ -223,7 +223,6 @@ struct ServicesFactory { nftService: nftService, avatarService: avatarService, swapService: swapService, - swapTransactionService: swapTransactionService, appReleaseService: releaseService, subscriptionsService: subscriptionService, deviceObserverService: deviceObserverService,