Skip to content

Commit

Permalink
cancel orders
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-dydx committed Jun 19, 2024
1 parent 340627f commit 5206867
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 11 deletions.
15 changes: 15 additions & 0 deletions PlatformUI/PlatformUI/PlatformViewModel+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import UIKit
import SwiftUI

public extension PlatformViewModel {
var safeAreaInsets: UIEdgeInsets? {
Expand All @@ -21,3 +22,17 @@ public extension PlatformViewModel {
.last
}
}

public extension View {
var safeAreaInsets: UIEdgeInsets? {
keyWindow?.safeAreaInsets
}

var keyWindow: UIWindow? {
UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.last
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import PlatformParticles
import RoutingKit
import ParticlesKit
import PlatformUI
import dydxStateManager
import Combine

public class dydxCancelPendingIsolatedOrdersViewBuilder: NSObject, ObjectBuilderProtocol {
public func build<T>() -> T? {
Expand All @@ -22,7 +24,10 @@ public class dydxCancelPendingIsolatedOrdersViewBuilder: NSObject, ObjectBuilder

private class dydxCancelPendingIsolatedOrdersViewBuilderController: HostingViewController<PlatformView, dydxCancelPendingIsolatedOrdersViewModel> {
override public func arrive(to request: RoutingRequest?, animated: Bool) -> Bool {
if request?.path == "/portfolio/cancel_pending_position" {
if let marketId = request?.params?["market"] as? String,
request?.path == "/portfolio/cancel_pending_position",
let presenter = presenter as? dydxCancelPendingIsolatedOrdersViewBuilderPresenterProtocol {
presenter.marketId = marketId
return true
}
return false
Expand All @@ -31,12 +36,92 @@ private class dydxCancelPendingIsolatedOrdersViewBuilderController: HostingViewC

private protocol dydxCancelPendingIsolatedOrdersViewBuilderPresenterProtocol: HostedViewPresenterProtocol {
var viewModel: dydxCancelPendingIsolatedOrdersViewModel? { get }
var marketId: String? { get set }
}

private class dydxCancelPendingIsolatedOrdersViewBuilderPresenter: HostedViewPresenter<dydxCancelPendingIsolatedOrdersViewModel>, dydxCancelPendingIsolatedOrdersViewBuilderPresenterProtocol {
fileprivate var marketId: String?

override init() {
super.init()

self.viewModel = .previewValue
self.viewModel = .init(marketLogoUrl: nil, marketName: "", marketId: "", orderCount: 0, cancelAction: {})
}

override func start() {
super.start()

Publishers.CombineLatest(
AbacusStateManager.shared.state.configsAndAssetMap,
AbacusStateManager.shared.state.selectedSubaccountOrders
)
.receive(on: RunLoop.main)
.sink { [weak self] configsAndAssetMap, orders in
guard let self = self,
let marketId = self.marketId,
let asset = configsAndAssetMap[marketId]?.asset
else { return }
let pendingOrders = orders.filter { $0.marketId == marketId && $0.status == .open }
self.viewModel?.marketLogoUrl = URL(string: asset.resources?.imageUrl ?? "")
self.viewModel?.marketName = asset.name ?? "--"
self.viewModel?.marketId = asset.id
self.viewModel?.orderCount = pendingOrders.count
self.viewModel?.failureCount = self.viewModel?.failureCount
self.viewModel?.cancelAction = { [weak self] in
self?.tryCancelOrders(orderIds: orders.map(\.id))
}
}
.store(in: &subscriptions)
}

private func tryCancelOrders(orderIds: [String]) {
viewModel?.state = viewModel?.failureCount == nil ? .submitting : .resubmitting
Task { [weak self] in
guard let self = self else { return }

// Create an array to hold the results of the cancellations
var results: [Result<AbacusStateManager.SubmissionStatus, Error>] = []

// Use a TaskGroup to kick off multiple calls and wait for all to finish
await withTaskGroup(of: Result<AbacusStateManager.SubmissionStatus, Error>.self) { group in
for orderId in orderIds {
group.addTask {
do {
let status = try await AbacusStateManager.shared.cancelOrder(orderId: orderId)
return .success(status)
} catch {
return .failure(error)
}
}
}

// Collect the results of all tasks
for await result in group {
results.append(result)
}
}

// Count the number of failed cancellations
let failureCount = results.filter { result in
if case .failure = result {
return true
}
return false
}.count

await updateState(failureCount: failureCount)
}
}

@MainActor
private func updateState(failureCount: Int) {
self.viewModel?.failureCount = failureCount

if failureCount > 0 {
self.viewModel?.state = .failed
} else {
Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: nil)
}
}

}
17 changes: 16 additions & 1 deletion dydx/dydxStateManager/dydxStateManager/AbacusStateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,22 @@ extension AbacusStateManager {
}
}
}
}

public func cancelOrder(orderId: String) async throws -> SubmissionStatus {
try await withCheckedThrowingContinuation { continuation in
asyncStateManager.cancelOrder(orderId: orderId) { successful, error, _ in
if successful.boolValue {
continuation.resume(returning: .success)
} else {
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(throwing: ParsingError.unknown)
}
}
}
}
}}

public extension V4Environment {
var usdcTokenInfo: TokenInfo? {
Expand Down
2 changes: 2 additions & 0 deletions dydx/dydxStateManager/dydxStateManager/Models+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ extension ParsingError: Error {
}
return message
}

public static let unknown = ParsingError(type: .unhandled, message: "", stringKey: "APP.GENERAL.UNKNOWN_ERROR", stackTrace: nil, codespace: nil)
}

public extension TradeInput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,34 @@ import Utilities
import SDWebImageSwiftUI

public class dydxCancelPendingIsolatedOrdersViewModel: PlatformViewModel {
public enum State {
case readyToSubmit
case failed
case submitting
case resubmitting
}

@Published public var state: State = .readyToSubmit

@Published public var marketLogoUrl: URL?
@Published public var marketName: String
@Published public var marketId: String
@Published public var orderCount: Int
@Published public var failureCount: Int?
@Published public var cancelAction: (() -> Void)

public init(marketLogoUrl: URL?,
marketName: String,
marketId: String,
orderCount: Int,
failureCount: Int? = nil,
cancelAction: @escaping (() -> Void)
) {
self.marketLogoUrl = marketLogoUrl
self.marketName = marketName
self.marketId = marketId
self.orderCount = orderCount
self.failureCount = failureCount
self.cancelAction = cancelAction
}

Expand All @@ -35,15 +47,82 @@ public class dydxCancelPendingIsolatedOrdersViewModel: PlatformViewModel {
marketName: "Ethereum",
marketId: "ETH-USDC",
orderCount: 1,
failureCount: nil,
cancelAction: {}
)
}

public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformUI.PlatformView {
PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] _ in
guard let self = self else { return AnyView(PlatformView.nilView) }
return AnyView(CancelView(viewModel: self))
switch self.state {
case .readyToSubmit, .submitting:
return CancelView(viewModel: self).wrappedInAnyView()
case .failed, .resubmitting:
return TryAgainView(viewModel: self).wrappedInAnyView()
}
}
}
}

private struct TryAgainView: View {
@StateObject var viewModel = dydxCancelPendingIsolatedOrdersViewModel.previewValue

var errorImage: some View {
Image("icon_error", bundle: .dydxView)
.resizable()
.templateColor(.colorRed)
.scaledToFit()
.frame(width: 40, height: 40)
.padding(.all, 20)
.themeColor(background: .layer3)
.clipShape(.circle)
}

var title: some View {
if let failureCount = viewModel.failureCount {
let key = viewModel.failureCount == 1 ? "APP.TRADE.CANCELING_ONE_ORDER_FAILED" : "APP.TRADE.CANCELING_N_ORDERS_FAILED"
let params = ["COUNT": "\(failureCount)"]
return Text(localizerPathKey: key, params: params)
.themeFont(fontType: .plus, fontSize: .largest)
.themeColor(foreground: .textPrimary)
} else {
return Text("")
}

}

var button: some View {
let content: Text
let buttonState: PlatformButtonState = viewModel.state == .resubmitting ? .disabled : .destructive
if viewModel.state == .resubmitting {
content = Text(localizerPathKey: "APP.TRADE.CANCELING_ORDERS_COUNT", params: ["COUNT": "\(viewModel.orderCount)"])
.themeFont(fontType: .base, fontSize: .medium)
.themeColor(foreground: .textTertiary)
} else {
content = Text(localizerPathKey: "APP.ONBOARDING.TRY_AGAIN")
.themeFont(fontType: .base, fontSize: .medium)
.themeColor(foreground: .textPrimary)
}
return PlatformButtonViewModel(content: content.wrappedViewModel,
type: .defaultType(),
state: buttonState,
action: viewModel.cancelAction)
.createView()
}

var body: some View {
VStack(spacing: 24) {
errorImage
title
button
}
.padding(.horizontal, 24)
.padding(.top, 60)
.padding(.bottom, max((safeAreaInsets?.bottom ?? 0), 24))
.themeColor(background: .layer4)
.makeSheet(sheetStyle: .fitSize)
.ignoresSafeArea(edges: [.bottom])
}
}

Expand All @@ -60,7 +139,7 @@ private struct CancelView: View {

var title: some View {
return Text(localizerPathKey: "APP.TRADE.CANCEL_ORDERS")
.themeFont(fontType: .base, fontSize: .large)
.themeFont(fontType: .plus, fontSize: .largest)
.themeColor(foreground: .textPrimary)
}

Expand Down Expand Up @@ -93,14 +172,22 @@ private struct CancelView: View {
}

var button: some View {
let key = viewModel.orderCount == 1 ? "APP.TRADE.CANCEL_ORDER" : "APP.TRADE.CANCEL_ORDERS_COUNT"
let params = ["COUNT": "\(viewModel.orderCount)"]
let content = Text(localizerPathKey: key, params: params)
.themeFont(fontType: .base, fontSize: .medium)
.themeColor(foreground: .textPrimary)
let content: Text
let buttonState: PlatformButtonState = viewModel.state == .submitting ? .disabled : .destructive
if viewModel.state == .submitting {
content = Text(localizerPathKey: "APP.TRADE.CANCELING_ORDERS_COUNT", params: ["COUNT": "\(viewModel.orderCount)"])
.themeFont(fontType: .base, fontSize: .medium)
.themeColor(foreground: .textTertiary)
} else {
let key = viewModel.orderCount == 1 ? "APP.TRADE.CANCEL_ORDER" : "APP.TRADE.CANCEL_ORDERS_COUNT"
let params = ["COUNT": "\(viewModel.orderCount)"]
content = Text(localizerPathKey: key, params: params)
.themeFont(fontType: .base, fontSize: .medium)
.themeColor(foreground: .colorRed)
}
return PlatformButtonViewModel(content: content.wrappedViewModel,
type: .defaultType(),
state: .destructive,
state: buttonState,
action: viewModel.cancelAction)
.createView()
}
Expand Down

0 comments on commit 5206867

Please sign in to comment.