Skip to content

Commit

Permalink
Merge pull request #1664 from p2p-org/feature/pwn-784-pnl
Browse files Browse the repository at this point in the history
Feature/pwn 784 pnl
  • Loading branch information
bigearsenal authored Feb 2, 2024
2 parents 9975a30 + ed65976 commit 1665812
Show file tree
Hide file tree
Showing 34 changed files with 788 additions and 147 deletions.
18 changes: 18 additions & 0 deletions Packages/KeyAppKit/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ let package = Package(
targets: ["TokenService"]
),

.library(
name: "PnLService",
targets: ["PnLService"]
),

.library(
name: "SendService",
targets: ["SendService"]
Expand Down Expand Up @@ -325,6 +330,19 @@ let package = Package(
]
),

.target(
name: "PnLService",
dependencies: [
"KeyAppNetworking",
]
),

.testTarget(
name: "PnLServiceTests",
dependencies: ["PnLService"],
path: "Tests/UnitTests/PnLServiceTests"
),

.target(
name: "KeyAppBusiness",
dependencies: [
Expand Down
7 changes: 0 additions & 7 deletions Packages/KeyAppKit/Sources/KeyAppKitCore/Repository.swift

This file was deleted.

30 changes: 30 additions & 0 deletions Packages/KeyAppKit/Sources/PnLService/PnLModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

public protocol PnLModel {
var total: RPCPnLResponseDetail? { get }
var pnlByMint: [String: RPCPnLResponseDetail] { get }
}

struct PnLRPCRequest: Codable {
let userWallet: String
let mints: [String]

enum CodingKeys: String, CodingKey {
case userWallet = "user_wallet"
case mints
}
}

public struct RPCPnLResponseDetail: Codable {
public let usdAmount, percent: String

public init(usdAmount: String, percent: String) {
self.usdAmount = usdAmount
self.percent = percent
}

enum CodingKeys: String, CodingKey {
case usdAmount = "usd_amount"
case percent
}
}
41 changes: 41 additions & 0 deletions Packages/KeyAppKit/Sources/PnLService/PnLService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation
import KeyAppNetworking

public protocol PnLService {
associatedtype PnL: PnLModel
func getPNL(userWallet: String, mints: [String]) async throws -> PnL
}

extension Dictionary: PnLModel where Key == String, Value == RPCPnLResponseDetail {
public var total: RPCPnLResponseDetail? {
self["total"]
}

public var pnlByMint: [String: RPCPnLResponseDetail] {
var dict = self
dict.removeValue(forKey: "total")
return dict
}
}

public class PnLServiceImpl: PnLService {
private let urlSession: HTTPURLSession

public init(urlSession: HTTPURLSession = URLSession.shared) {
self.urlSession = urlSession
}

public func getPNL(userWallet: String, mints: [String]) async throws -> [String: RPCPnLResponseDetail] {
try await JSONRPCHTTPClient(urlSession: urlSession)
.request(
baseURL: "https://pnl.key.app",
body: .init(
method: "get_pnl",
params: PnLRPCRequest(
userWallet: userWallet,
mints: mints
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ public struct RepositoryView<
@ObservedObject var repository: R

/// View to handle state when repository is loading, for example ProgressView or Skeleton
let loadingView: () -> LoadingView
let loadingView: (P.ItemType?) -> LoadingView

/// View to handle state when an error occurred
let errorView: (Error) -> ErrorView
let errorView: (Error, P.ItemType?) -> ErrorView

/// View to handle state when repository is loaded
let content: (P.ItemType?) -> LoadedView
Expand All @@ -33,10 +33,8 @@ public struct RepositoryView<
/// - loadedView: View to handle state when repository is loaded
public init(
repository: R,
@ViewBuilder loadingView: @escaping () -> LoadingView,
@ViewBuilder errorView: @escaping (Error) -> ErrorView = { error in
Text(String(reflecting: error)).foregroundStyle(Color.red)
},
@ViewBuilder loadingView: @escaping (P.ItemType?) -> LoadingView,
@ViewBuilder errorView: @escaping (Error, P.ItemType?) -> ErrorView,
@ViewBuilder content: @escaping (P.ItemType?) -> LoadedView
) {
self.repository = repository
Expand All @@ -49,13 +47,15 @@ public struct RepositoryView<

public var body: some View {
switch repository.state {
case .initialized, .loading:
loadingView()
case .initialized:
loadingView(nil)
case .loading:
loadingView(repository.data)
case .loaded:
content(repository.data)
case .error:
if let error = repository.error {
errorView(error)
errorView(error, repository.data)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import KeyAppNetworking
import XCTest
@testable import PnLService

final class PnLServiceTests: XCTestCase {
func testEncodingRequest() async throws {
let request = PnLRPCRequest(
userWallet: "EXRV9Hu3VswiEDYVcx9tLfF7EX3z3zEH19g363KQe3Kd",
mints: [
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo",
]
)

let encoded = try String(data: JSONEncoder().encode(request), encoding: .utf8)!
XCTAssertEqual(
encoded,
#"{"user_wallet":"EXRV9Hu3VswiEDYVcx9tLfF7EX3z3zEH19g363KQe3Kd","mints":["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo"]}"#
)
}

func testRequest_SuccessfulResponse_ReturnsData() async throws {
let mockURLSession =
MockURLSession(
responseString: #"{"jsonrpc":"2.0","id":"12","result":{"total":{"usd_amount":"+1.23","percent":"-3.45"},"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v":{"usd_amount":"+1.23","percent":"-3.45"},"CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo":{"usd_amount":"+1.23","percent":"-3.45"}}}"#
)

let service = PnLServiceImpl(urlSession: mockURLSession)
let result = try await service.getPNL(userWallet: "", mints: [])

XCTAssertEqual(result.total?.usdAmount, "+1.23")
XCTAssertEqual(result.total?.percent, "-3.45")
XCTAssertEqual(result.pnlByMint["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"]?.usdAmount, "+1.23")
XCTAssertEqual(result.pnlByMint["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"]?.percent, "-3.45")
XCTAssertEqual(result.pnlByMint["CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo"]?.usdAmount, "+1.23")
XCTAssertEqual(result.pnlByMint["CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo"]?.percent, "-3.45")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Combine
import Foundation
import SwiftUI

@MainActor
class BottomSheetInfoCoordinator<Content: View>: Coordinator<Void> {
let vc: UIBottomSheetHostingController<Content>
let parentVC: UIViewController

init(
parentVC: UIViewController,
rootView: Content,
shouldIgnoresKeyboard: Bool = true
) {
self.parentVC = parentVC
vc = .init(rootView: rootView, shouldIgnoresKeyboard: shouldIgnoresKeyboard)
vc.view.layer.cornerRadius = 20
}

// MARK: - Methods

override func start() -> AnyPublisher<Void, Never> {
parentVC.present(vc, interactiveDismissalType: .standard)
return vc.deallocatedPublisher()
}
}
51 changes: 51 additions & 0 deletions p2p_wallet/Common/PnL/AccountPnLRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation
import KeyAppBusiness
import PnLService
import Repository

enum PnLError: String, Error {
case invalidUserWallet
}

class PnLProvider: Provider {
// MARK: - Dependencies

let service: any PnLService
let userWalletsManager: UserWalletManager
let solanaAccountsService: SolanaAccountsService

// MARK: - Initializer

init(
service: any PnLService,
userWalletsManager: UserWalletManager,
solanaAccountsService: SolanaAccountsService
) {
self.service = service
self.userWalletsManager = userWalletsManager
self.solanaAccountsService = solanaAccountsService
}

func fetch() async throws -> PnLModel? {
guard let userWallet = userWalletsManager.wallet?.account.publicKey.base58EncodedString else {
throw PnLError.invalidUserWallet
}
return try await service.getPNL(
userWallet: userWallet,
mints: solanaAccountsService.state.value.map(\.mintAddress)
)
}
}

class PnLRepository: Repository<PnLProvider> {
weak var timer: Timer?

override init(initialData: ItemType?, provider: PnLProvider) {
super.init(initialData: initialData, provider: provider)
timer = Timer.scheduledTimer(withTimeInterval: 5 * 60, repeats: true) { [weak self] _ in
guard let self else { return }
Task { await self.refresh() }
}
timer?.fire()
}
}
29 changes: 29 additions & 0 deletions p2p_wallet/Common/PnL/MockPnLService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation
import PnLService
import SolanaSwift

struct MockPnLModel: PnLModel {
let total: RPCPnLResponseDetail?
let pnlByMint: [String: RPCPnLResponseDetail]
}

class MockPnLService: PnLService {
func getPNL(userWallet _: String, mints _: [String]) async throws -> MockPnLModel {
try await Task.sleep(nanoseconds: 1_000_000_000)
return .init(
total: .init(usdAmount: "+1.23", percent: "-1.2"),
pnlByMint: [
"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263": .init(usdAmount: "-5", percent: "-5"),
"EjChnoTPcQ9DxKZkBM7g1M4DBF4J2Mx75CYTPoDZyYXB": .init(usdAmount: "-2.2", percent: "-2.2"),
"7SdFACfxmg2eetZEhEYZhsNMVAu84USVtfJ64jFDCg9Y": .init(usdAmount: "-1.1", percent: "-1.1"),
"98eKvPL8rJeFPVLft3JMfsCn1Yi9UN7MmG2htMQ4t4FS": .init(usdAmount: "-3.3", percent: "-3.3"),
"EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp": .init(usdAmount: "+4.4", percent: "+4.4"),
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": .init(usdAmount: "-6.6", percent: "-6.6"),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": .init(usdAmount: "+7.7", percent: "+7.7"),
"JET6zMJWkCN9tpRT2v2jfAmm5VnQFDpUBCyaKojmGtz": .init(usdAmount: "-8.8", percent: "-8.8"),
"3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh": .init(usdAmount: "-9.9", percent: "-9.9"),
"2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk": .init(usdAmount: "-10.1", percent: "-10.1"),
]
)
}
}
19 changes: 19 additions & 0 deletions p2p_wallet/Injection/Resolver+registerAllServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Moonpay
import NameService
import Onboarding
import OrcaSwapSwift
import PnLService
import Reachability
import Resolver
import Sell
Expand Down Expand Up @@ -569,6 +570,24 @@ extension Resolver: ResolverRegistering {
}
.implements(JupiterTokensProvider.self)
.scope(.session)

register {
PnLServiceImpl()
}
.implements((any PnLService).self)
.scope(.session)

register {
PnLRepository(
initialData: nil,
provider: .init(
service: resolve(),
userWalletsManager: resolve(),
solanaAccountsService: resolve()
)
)
}
.scope(.session)
}

/// Shared scope: share between screens
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Group 1523461.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group [email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group [email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions p2p_wallet/Resources/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,8 @@
"Token 2022 transfer fee" = "Token 2022 transfer fee";
"Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance";
"Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance";
"%@ all the time" = "%@ all the time";
"Here’s how do we count your profits for total balance and every single token" = "Here’s how do we count your profits for total balance and every single token";
"Based on absolute and relative profitability of each trade. It shows the relative potential %% profits or losses of your trading strategy." = "Based on absolute and relative profitability of each trade. It shows the relative potential %% profits or losses of your trading strategy.";
"%@%% last 24h" = "%@%% last 24h";
"Result of an investment, trading strategy per 24 hours" = "Result of an investment, trading strategy per 24 hours";
5 changes: 5 additions & 0 deletions p2p_wallet/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -585,3 +585,8 @@
"Token 2022 transfer fee" = "Token 2022 transfer fee";
"Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance";
"Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance";
"%@ all the time" = "%@ all the time";
"Here’s how do we count your profits for total balance and every single token" = "Here’s how do we count your profits for total balance and every single token";
"Based on absolute and relative profitability of each trade. It shows the relative potential %% profits or losses of your trading strategy." = "Based on absolute and relative profitability of each trade. It shows the relative potential %% profits or losses of your trading strategy.";
"%@%% last 24h" = "%@%% last 24h";
"Result of an investment, trading strategy per 24 hours" = "Result of an investment, trading strategy per 24 hours";
Loading

0 comments on commit 1665812

Please sign in to comment.