Skip to content

Commit

Permalink
TRCL-3713 PNL Chart / No position view (#327)
Browse files Browse the repository at this point in the history
* Add feature flag and mode selector for simple UI

* WIP

* Search

* Onboarding and pnls

* Move the swiftui chart logic to dydxLineChartViewModel for reuse

* Chart

* Add delay to reduce loading state updates

* Default to show 90D PNL chart
  • Loading branch information
ruixhuang authored Jan 7, 2025
1 parent a507650 commit e32c2c6
Show file tree
Hide file tree
Showing 18 changed files with 481 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "c6d13a8669c6f06bed5cd9f1fcb0fa88055a1be183797745f849e2658b63151d",
"originHash" : "4abc8b6e37ddb299948f224ccf7a7ccf9ed6d5156db6de917ea842dc596c3bbf",
"pins" : [
{
"identity" : "bigint",
Expand Down
8 changes: 8 additions & 0 deletions dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
0273A1472ACCC4C4001B89F5 /* dydxHistoryViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0273A1462ACCC4C4001B89F5 /* dydxHistoryViewBuilder.swift */; };
0274B34828F1140D005AF69E /* dydxMarketPriceCandlesViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0274B34728F1140D005AF69E /* dydxMarketPriceCandlesViewPresenter.swift */; };
0276FA992A0DB8FD000BDF0B /* Model+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0276FA982A0DB8FD000BDF0B /* Model+Ext.swift */; };
027885DB2D1A34EB00366321 /* dydxSimpleUIPortfolioViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027885DA2D1A34EA00366321 /* dydxSimpleUIPortfolioViewPresenter.swift */; };
027885DF2D1B811400366321 /* dydxPostOnboardingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027885DE2D1B811400366321 /* dydxPostOnboardingAction.swift */; };
0279656A29D795E8004DEB20 /* tabs_v4.json in Resources */ = {isa = PBXBuildFile; fileRef = 0279655B29D795E7004DEB20 /* tabs_v4.json */; };
0279656C29D795E8004DEB20 /* routing_swiftui.json in Resources */ = {isa = PBXBuildFile; fileRef = 0279656929D795E7004DEB20 /* routing_swiftui.json */; };
0279DE482BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE472BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift */; };
Expand Down Expand Up @@ -471,6 +473,8 @@
0273A1462ACCC4C4001B89F5 /* dydxHistoryViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxHistoryViewBuilder.swift; sourceTree = "<group>"; };
0274B34728F1140D005AF69E /* dydxMarketPriceCandlesViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketPriceCandlesViewPresenter.swift; sourceTree = "<group>"; };
0276FA982A0DB8FD000BDF0B /* Model+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+Ext.swift"; sourceTree = "<group>"; };
027885DA2D1A34EA00366321 /* dydxSimpleUIPortfolioViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIPortfolioViewPresenter.swift; sourceTree = "<group>"; };
027885DE2D1B811400366321 /* dydxPostOnboardingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPostOnboardingAction.swift; sourceTree = "<group>"; };
0279655B29D795E7004DEB20 /* tabs_v4.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tabs_v4.json; sourceTree = "<group>"; };
0279656929D795E7004DEB20 /* routing_swiftui.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = routing_swiftui.json; sourceTree = "<group>"; };
0279DE472BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTargetLeverageCtaButtonViewPresenter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -862,6 +866,7 @@
0262F2D429DB4891009889E2 /* WalletAction.swift */,
27B957EC2B97C07400EF9304 /* dydxShareActionBuilder.swift */,
278A4D1D2B8EA95A003898EB /* dydxCollectFeedbackActionBuilder.swift */,
027885DE2D1B811400366321 /* dydxPostOnboardingAction.swift */,
);
path = Actions;
sourceTree = "<group>";
Expand Down Expand Up @@ -1313,6 +1318,7 @@
02D6DBB72D134BE0008AAEA1 /* components */ = {
isa = PBXGroup;
children = (
027885DA2D1A34EA00366321 /* dydxSimpleUIPortfolioViewPresenter.swift */,
028CF1412D1489E800476930 /* dydxSimpleUIMarketSearchViewPresenter.swift */,
02D6DBB82D134BE8008AAEA1 /* dydxSimpleUIMarketListViewPresenter.swift */,
);
Expand Down Expand Up @@ -2063,6 +2069,7 @@
024FEB662ACB75FA0087A55E /* dydxFeesStuctureViewBuilder.swift in Sources */,
0256F54229AFFCAC00A083C0 /* dydxOnboardConnectViewBuilder.swift in Sources */,
64A4DB972966480E008D8E20 /* dydxOrderbookPresenter.swift in Sources */,
027885DB2D1A34EB00366321 /* dydxSimpleUIPortfolioViewPresenter.swift in Sources */,
02F99F3829E4C0030009B3E8 /* dydxSearchViewPresenter.swift in Sources */,
0273A1472ACCC4C4001B89F5 /* dydxHistoryViewBuilder.swift in Sources */,
02F958232A1BDEFA00828F9A /* dydxKeyExportViewBuilder.swift in Sources */,
Expand All @@ -2086,6 +2093,7 @@
6448800B2AA248340068DD87 /* dydxAlertsWorker.swift in Sources */,
0262F2D529DB4891009889E2 /* WalletAction.swift in Sources */,
27592DF82C9A54D6002FBD4B /* dydxFrontendAlertsProvider.swift in Sources */,
027885DF2D1B811400366321 /* dydxPostOnboardingAction.swift in Sources */,
023789ED28BD381C00F212E1 /* dydxPresenters.docc in Sources */,
2751D6462C59643800B36F95 /* dydxVaultViewBuilder.swift in Sources */,
277E908B2B2118AE005CCBCB /* dydxRewardsHistoryViewPresenter.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,8 @@
"/action/wallet/etherscan": {
"destination":"dydxPresenters.WalletActionBuilder"
},
"/action/wallets/delete": {
"destination":"WalletsDelete.xib"
},
"/action/withdraw": {
"destination":"WithdrawAction.xib"
"/action/post_onboarding": {
"destination":"dydxPresenters.dydxPostOnboardingActionBuilder"
},
"/alerts":{
"destination":"dydxPresenters.dydxAlertsViewBuilder",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// dydxPostOnboardingAction.swift
// dydxPresenters
//
// Created by Rui Huang on 24/12/2024.
//

import Foundation
import Utilities
import RoutingKit
import dydxStateManager
import Combine
import dydxFormatter

public class dydxPostOnboardingActionBuilder: NSObject, ObjectBuilderProtocol {
public func build<T>() -> T? {
let action = dydxPostOnboardingAction()
return action as? T
}
}

private class dydxPostOnboardingAction: NSObject, NavigableProtocol {
private var subscriptions = Set<AnyCancellable>()

func navigate(to request: RoutingRequest?, animated: Bool, completion: RoutingCompletionBlock?) {
switch request?.path {
case "/action/post_onboarding":
let walletId = parser.asString(request?.params?["walletId"])
if let ethereumAddress = parser.asString(request?.params?["ethereumAddress"]) {
if let cosmoAddress = parser.asString(request?.params?["cosmoAddress"]),
let mnemonic = parser.asString(request?.params?["mnemonic"]) {
AbacusStateManager.shared.setV4(ethereumAddress: ethereumAddress, walletId: walletId, cosmoAddress: cosmoAddress, mnemonic: mnemonic)
} else if let apiKey = parser.asString(request?.params?["apiKey"]),
let secret = parser.asString(request?.params?["secret"]),
let passPhrase = parser.asString(request?.params?["passPhrase"]) {
AbacusStateManager.shared.setV3(ethereumAddress: ethereumAddress, walletId: walletId, apiKey: apiKey, secret: secret, passPhrase: passPhrase)
}
}
Router.shared?.navigate(to: RoutingRequest(path: "/"), animated: animated, completion: completion)
default:
completion?(nil, false)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ private class dydxOnboardConnectViewPresenter: HostedViewPresenter<dydxOnboardCo
.sink { walletInstance in
if walletInstance == nil {
let accepted: (() -> Void) = {
Router.shared?.navigate(to: RoutingRequest(path: "/portfolio", params: ["ethereumAddress": ethereumAddress, "cosmoAddress": cosmoAddress, "mnemonic": mnemonic, "walletId": walletId]), animated: true, completion: nil)
Router.shared?.navigate(to: RoutingRequest(path: "/action/post_onboarding", params: ["ethereumAddress": ethereumAddress, "cosmoAddress": cosmoAddress, "mnemonic": mnemonic, "walletId": walletId]), animated: true, completion: nil)
}
Router.shared?.navigate(to: RoutingRequest(path: "/onboard/tos", params: ["accepted": accepted]), animated: true, completion: nil)
} else {
Router.shared?.navigate(to: RoutingRequest(path: "/portfolio", params: ["ethereumAddress": ethereumAddress, "cosmoAddress": cosmoAddress, "mnemonic": mnemonic, "walletId": walletId]), animated: true, completion: nil)
Router.shared?.navigate(to: RoutingRequest(path: "/action/post_onboarding", params: ["ethereumAddress": ethereumAddress, "cosmoAddress": cosmoAddress, "mnemonic": mnemonic, "walletId": walletId]), animated: true, completion: nil)
}
}
.store(in: &subscriptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private class dydxOnboardQRCodeViewPresenter: HostedViewPresenter<dydxOnboardQRC
case .signed(let result):
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let cosmoAddress = result.cosmoAddress, let mnemonic = result.mnemonic {
Router.shared?.navigate(to: RoutingRequest(path: "/portfolio", params: ["ethereumAddress": result.ethereumAddress, "cosmoAddress": cosmoAddress, "mnemonic": mnemonic, "walletId": result.walletId ?? ""]), animated: true, completion: nil)
Router.shared?.navigate(to: RoutingRequest(path: "/action/post_onboarding", params: ["ethereumAddress": result.ethereumAddress, "cosmoAddress": cosmoAddress, "mnemonic": mnemonic, "walletId": result.walletId ?? ""]), animated: true, completion: nil)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ private class dydxOnboardScanViewPresenter: HostedViewPresenter<dydxOnboardScanV
walletId: nil,
cosmoAddress: address,
mnemonic: mnemonic)
Router.shared?.navigate(to: RoutingRequest(path: "/portfolio",
Router.shared?.navigate(to: RoutingRequest(path: "/action/post_onboarding",
params: ["cosmoAddress": address, "mnemonic": mnemonic]),
animated: true, completion: nil)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ class SharedAccountPresenter: HostedViewPresenter<SharedAccountViewModel>, Share

viewModel?.freeCollateral = dydxFormatter.shared.dollar(number: account?.freeCollateral?.current?.doubleValue, size: nil)

viewModel?.buyingPower = dydxFormatter.shared.dollar(number: account?.buyingPower?.current?.doubleValue.filter(filter: .notNegative), size: nil)
viewModel?.buyingPower = dydxFormatter.shared.dollar(number: account?.buyingPower?.current?.doubleValue.filter(filter: .notNegative), digits: 2)

viewModel?.marginUsage = dydxFormatter.shared.percent(number: account?.marginUsage?.current?.doubleValue, digits: 2)

viewModel?.leverage = dydxFormatter.shared.leverage(number: account?.leverage?.current?.doubleValue)

viewModel?.equity = dydxFormatter.shared.dollar(number: account?.equity?.current?.doubleValue, size: nil)
viewModel?.equity = dydxFormatter.shared.dollar(number: account?.equity?.current?.doubleValue, digits: 2)

viewModel?.openInterest = dydxFormatter.shared.dollarVolume(number: account?.notionalTotal?.current?.doubleValue, digits: 2)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,6 @@ private class dydxPortfolioViewController: HostingViewController<PlatformView, d
if let presenter = presenter as? dydxPortfolioViewPresenterProtocol {
presenter.updateDisplayType(displayType: request?.path?.lastPathComponent)
}
let walletId = parser.asString(request?.params?["walletId"])
if let ethereumAddress = parser.asString(request?.params?["ethereumAddress"]) {
if let cosmoAddress = parser.asString(request?.params?["cosmoAddress"]),
let mnemonic = parser.asString(request?.params?["mnemonic"]) {
AbacusStateManager.shared.setV4(ethereumAddress: ethereumAddress, walletId: walletId, cosmoAddress: cosmoAddress, mnemonic: mnemonic)
} else if let apiKey = parser.asString(request?.params?["apiKey"]),
let secret = parser.asString(request?.params?["secret"]),
let passPhrase = parser.asString(request?.params?["passPhrase"]) {
AbacusStateManager.shared.setV3(ethereumAddress: ethereumAddress, walletId: walletId, apiKey: apiKey, secret: secret, passPhrase: passPhrase)
}
}
return true
}
return false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// dydxSimpleUPortfolioViewPresenter.swift
// dydxPresenters
//
// Created by Rui Huang on 23/12/2024.
//

import Utilities
import dydxViews
import PlatformParticles
import RoutingKit
import ParticlesKit
import PlatformUI
import dydxStateManager
import Abacus
import Combine
import dydxFormatter

protocol dydxSimpleUIPortfolioViewPresenterProtocol: HostedViewPresenterProtocol {
var viewModel: dydxSimpleUIPortfolioViewModel? { get }
}

class dydxSimpleUIPortfolioViewPresenter: HostedViewPresenter<dydxSimpleUIPortfolioViewModel>, dydxSimpleUIPortfolioViewPresenterProtocol {

private let accountPresenter = SharedAccountPresenter()
private lazy var childPresenters: [HostedViewPresenterProtocol] = [
accountPresenter
]

private var loadingStartTime: Date?

private static let loadingDelay: TimeInterval = 2.0

override init() {
let viewModel = dydxSimpleUIPortfolioViewModel()

accountPresenter.$viewModel.assign(to: &viewModel.$sharedAccountViewModel)

super.init()

self.viewModel = viewModel

attachChildren(workers: childPresenters)
}

override func start() {
super.start()

AbacusStateManager.shared.setHistoricalPNLPeriod(period: HistoricalPnlPeriod.period90d)

loadingStartTime = Date()
Publishers.CombineLatest4(
AbacusStateManager.shared.state.selectedSubaccount,
AbacusStateManager.shared.state.selectedSubaccountPNLs,
AbacusStateManager.shared.state.onboarded,
Timer.publish(every: Self.loadingDelay, on: .main, in: .default).autoconnect()
)
.sink { [weak self] subaccount, pnls, onboarded, _ in
if subaccount?.freeCollateral?.current?.doubleValue ?? 0 > 0 {
self?.viewModel?.state = .hasBalance
self?.viewModel?.buttonAction = nil
if let subaccount = subaccount {
self?.updatePNLs(pnls: pnls, subaccount: subaccount)
}
} else if let loadingStartTime = self?.loadingStartTime, Date().timeIntervalSince(loadingStartTime) > Self.loadingDelay {
if onboarded {
self?.viewModel?.state = .walletConnected
self?.viewModel?.buttonAction = {
Router.shared?.navigate(to: RoutingRequest(path: "/transfer"), animated: true, completion: nil)
}
} else {
self?.viewModel?.state = .loggedOut
self?.viewModel?.buttonAction = {
Router.shared?.navigate(to: RoutingRequest(path: "/onboard"), animated: true, completion: nil)
}
}
}
}
.store(in: &subscriptions)

attachChild(worker: accountPresenter)
}

private func updatePNLs(pnls: [SubaccountHistoricalPNL], subaccount: Subaccount) {
let firstTotalPnl = pnls.first?.totalPnl
let targetTotalPnl = subaccount.pnlTotal?.doubleValue ?? pnls.last?.totalPnl
let beginning = pnls.first?.equity

if let firstTotalPnl = firstTotalPnl, let targetTotalPnl = targetTotalPnl, let beginning = beginning, beginning != 0 {
viewModel?.pnlAmount = dydxFormatter.shared.dollar(number: targetTotalPnl - firstTotalPnl, digits: 2)
let percent = dydxFormatter.shared.percent(number: abs(targetTotalPnl - firstTotalPnl) / beginning, digits: 2)
viewModel?.pnlPercent = SignedAmountViewModel(text: percent, sign: targetTotalPnl >= firstTotalPnl ? .plus : .minus, coloringOption: .textOnly)
}

var chartEntries = pnls.compactMap {
let date = $0.createdAtMilliseconds / 1000
let value = $0.equity
return dydxLineChartViewModel.Entry(date: date, value: value)
}
if let currentValue = subaccount.equity?.current?.doubleValue {
chartEntries.append(dydxLineChartViewModel.Entry(date: Double(Date().millisecondsSince1970) / 1000, value: currentValue))
}
let maxValue = chartEntries.max { $0.value < $1.value }?.value ?? 0
let minValue = chartEntries.min { $0.value < $1.value }?.value ?? 0
viewModel?.chart.entries = chartEntries
viewModel?.chart.showYLabels = false
viewModel?.chart.valueLowerBoundOffset = (maxValue - minValue) * 0.8
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,20 @@ public class dydxSimpleUIMarketsViewPresenter: HostedViewPresenter<dydxSimpleUIM

private let marketListPresenter = dydxSimpleUIMarketListViewPresenter()
private let marketSearchPresenter = dydxSimpleUIMarketSearchViewPresenter()
private let portfolioPresenter = dydxSimpleUIPortfolioViewPresenter()

private lazy var childPresenters: [HostedViewPresenterProtocol] = [
marketListPresenter,
marketSearchPresenter
marketSearchPresenter,
portfolioPresenter
]

override init() {
let viewModel = dydxSimpleUIMarketsViewModel()

marketListPresenter.$viewModel.assign(to: &viewModel.$marketList)
marketSearchPresenter.$viewModel.assign(to: &viewModel.$marketSearch)
portfolioPresenter.$viewModel.assign(to: &viewModel.$portfolio)
marketSearchPresenter.viewModel?.$searchText.assign(to: &marketListPresenter.$searchText)
marketSearchPresenter.viewModel?.$focused.assign(to: &viewModel.$keyboardUp)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private class dydxVaultViewBuilderPresenter: HostedViewPresenter<dydxVaultViewMo
}

private func updateChartState(vault: Abacus.Vault?, valueType: dydxVaultChartViewModel.ValueTypeOption, timeType: dydxVaultChartViewModel.ValueTimeOption) {
let entries: [dydxVaultChartViewModel.Entry] = vault?.details?.history?.reversed()
let entries: [dydxLineChartViewModel.Entry] = vault?.details?.history?.reversed()
.compactMap { entry in
let secondsSince1970 = (entry.date?.doubleValue ?? 0) / 1000.0
let minSecondsSince1970: Double
Expand All @@ -189,6 +189,6 @@ private class dydxVaultViewBuilderPresenter: HostedViewPresenter<dydxVaultViewMo
return nil
}
} ?? []
viewModel?.vaultChart?.entries = entries
viewModel?.vaultChart?.chart.entries = entries
}
}
Loading

0 comments on commit e32c2c6

Please sign in to comment.