Skip to content

Commit

Permalink
Merge branch 'develop' into release/2.12.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Elizaveta Semenova committed Feb 21, 2024
2 parents 23d14e2 + 8294fd2 commit ece3f47
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public final class EthereumAccountsService: NSObject, AccountsService {

// MARK: - Service

let priceService: JupiterPriceService
let priceService: CoingeckoEthereumPriceService

let errorObservable: ErrorObserver

Expand Down Expand Up @@ -38,7 +38,7 @@ public final class EthereumAccountsService: NSObject, AccountsService {
address: String,
web3: Web3,
ethereumTokenRepository: EthereumTokensRepository,
priceService: JupiterPriceService,
priceService: CoingeckoEthereumPriceService,
fiat: String,
errorObservable: any ErrorObserver,
enable: Bool
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import Combine
import Foundation
import KeyAppKitCore

public class CoingeckoEthereumPriceService: PriceService {
static let wETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"

/// Data structure for caching
enum TokenPriceRecord: Codable, Hashable {
case requested(TokenPrice?)
}

enum Error: Swift.Error {
case invalidURL
}

let endpoint: String

let database: LifetimeDatabase<String, TokenPriceRecord>

public init(endpoint: String) {
self.endpoint = endpoint
database = .init(
filePath: "ethereum-token-price",
storage: ApplicationFileStorage(),
autoFlush: false,
defaultLifetime: 60 * 5
)
}

public let onChangePublisher: AnyPublisher<Void, Never> = PassthroughSubject<Void, Never>().eraseToAnyPublisher()

public func getPrice(token: AnyToken, fiat: String, options: PriceServiceOptions) async throws -> TokenPrice? {
try await getPrices(tokens: [token], fiat: fiat, options: options).first?.value
}

public func getPrices(
tokens: [AnyToken],
fiat: String,
options: PriceServiceOptions
) async throws -> [SomeToken: TokenPrice] {
let fiat = fiat.lowercased()
if tokens.isEmpty {
return [:]
}

var result: [SomeToken: TokenPriceRecord] = [:]

// Get value from local storage
for token in tokens {
result[token.asSomeToken] = try? await database.read(for: token.id)
}

// Filter missing token price
var missingPriceTokenMints: [AnyToken] = []
for token in tokens {
let token = token.asSomeToken

if options.contains(.actualPrice) {
// Fetch all prices when actual price is requested.
missingPriceTokenMints.append(token)
} else {
// Fetch only price, that does not exists in cache.
if result[token] == nil {
missingPriceTokenMints.append(token)
}
}
}

if !missingPriceTokenMints.isEmpty {
// Request missing prices
let newPrices = try await fetchTokenPrice(tokens: missingPriceTokenMints, fiat: fiat)

// Process missing token prices
for token in missingPriceTokenMints {
let token = token.asSomeToken

let price = newPrices[token]

if let price {
let record = TokenPriceRecord.requested(price)

result[token] = record
try? await database.write(for: token.id, value: record)
} else {
let record = TokenPriceRecord.requested(nil)

result[token] = record
try? await database.write(for: token.id, value: record)
}
}

try? await database.flush()
}

// Transform values of TokenPriceRecord? to TokenPrice?
return result
.compactMapValues { record in
switch record {
case let .requested(value):
return value
}
}
}

func fetchTokenPrice(tokens: [AnyToken], fiat: String) async throws -> [SomeToken: TokenPrice] {
let contractAddresses = tokens.map(\.primaryKey).map { key in
switch key {
case .native:
return Self.wETH
case let .contract(address):
return address
}
}
.joined(separator: ",")

guard let url =
URL(
string: "\(endpoint)/api/v3/simple/token_price/ethereum?contract_addresses=\(contractAddresses)&vs_currencies=\(fiat.lowercased())"
)
else {
throw Error.invalidURL
}

let request = URLRequest(url: url)

let (data, _) = try await URLSession.shared.data(for: request)
let priceResult = try JSONDecoder().decode(
[String: [String: Double]].self,
from: data
)

var priceData: [SomeToken: TokenPrice] = [:]
for token in tokens {
let token = token.asSomeToken

let value: Double?
if token.primaryKey == .native {
value = priceResult[Self.wETH]?[fiat]
} else {
value = priceResult[token.primaryKey.id]?[fiat]
}

if let value {
priceData[token] = TokenPrice(currencyCode: fiat, value: .init(floatLiteral: value), token: token)
} else {
priceData[token] = TokenPrice(currencyCode: fiat, value: nil, token: token)
}
}

return priceData
}

public func clear() async throws {}
}
2 changes: 1 addition & 1 deletion p2p_wallet/Common/Services/GlobalAppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class GlobalAppState: ObservableObject {

@Published var bridgeEndpoint: String = (Environment.current == .release) ?
String.secretConfig("BRIDGE_PROD")! :
String.secretConfig("BRIDGE_DEV")!
String.secretConfig("BRIDGE_PROD")!

@Published var tokenEndpoint: String = (Environment.current == .release) ?
String.secretConfig("TOKEN_SERVICE_PROD")! :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import KeyAppBusiness
import KeyAppNetworking
import Resolver
import SolanaSwift
import Task_retrying
import TweetNacl

protocol ReferralProgramService {
Expand All @@ -14,7 +15,10 @@ protocol ReferralProgramService {
}

enum ReferralProgramServiceError: Error {
case failedSet
case unauthorized
case timeOut
case alreadyRegistered
case other
}

final class ReferralProgramServiceImpl {
Expand Down Expand Up @@ -50,69 +54,115 @@ extension ReferralProgramServiceImpl: ReferralProgramService {
func register() async {
guard !Defaults.referrerRegistered else { return }
do {
guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet }
try await Task.retrying(
where: { $0.isRetryable },
maxRetryCount: 10,
retryDelay: 5, // 5 secs
timeoutInSeconds: 60, // wait for 60s if no success then throw .timedOut error
operation: { [weak self] _ in
// if there is transaction, send it
try await self?.sendRegisterRequest()
}
).value
} catch {
DefaultLogManager.shared.log(
event: "\(ReferralProgramService.self)_register",
data: error.localizedDescription,
logLevel: LogLevel.error
)
}
}

func setReferent(from: String) async {
guard from != currentUserAddress else { return }
do {
guard let secret = userWallet.wallet?.account.secretKey
else { throw ReferralProgramServiceError.unauthorized }
let timestamp = Int64(Date().timeIntervalSince1970)
let signed = try RegisterUserSignature(
user: currentUserAddress, referrent: nil, timestamp: timestamp
let signed = try SetReferentSignature(
user: currentUserAddress, referent: from, timestamp: timestamp
)
.sign(secretKey: secret)
let _: String? = try await jsonrpcClient.request(
baseURL: baseURL,
body: .init(
method: "register",
params: RegisterUserRequest(
method: "set_referent",
params: SetReferentRequest(
user: currentUserAddress,
referent: from,
timedSignature: ReferralTimedSignature(
timestamp: timestamp, signature: signed.toHexString()
timestamp: timestamp,
signature: signed.toHexString()
)
)
)
)
Defaults.referrerRegistered = true
} catch {
if error.localizedDescription.contains("duplicate") {
// The code is not unique so we look at the description
Defaults.referrerRegistered = true
}
debugPrint(error)
DefaultLogManager.shared.log(
event: "\(ReferralProgramService.self)_register",
event: "\(ReferralProgramService.self)_setReferent",
data: error.localizedDescription,
logLevel: LogLevel.error
)
}
}

func setReferent(from: String) async {
guard from != currentUserAddress else { return }
private func sendRegisterRequest() async throws {
do {
guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet }
guard let secret = userWallet.wallet?.account.secretKey
else { throw ReferralProgramServiceError.unauthorized }
let timestamp = Int64(Date().timeIntervalSince1970)
let signed = try SetReferentSignature(
user: currentUserAddress, referent: from, timestamp: timestamp
let signed = try RegisterUserSignature(
user: currentUserAddress, referrent: nil, timestamp: timestamp
)
.sign(secretKey: secret)
let _: String? = try await jsonrpcClient.request(
baseURL: baseURL,
body: .init(
method: "set_referent",
params: SetReferentRequest(
method: "register",
params: RegisterUserRequest(
user: currentUserAddress,
referent: from,
timedSignature: ReferralTimedSignature(
timestamp: timestamp,
signature: signed.toHexString()
timestamp: timestamp, signature: signed.toHexString()
)
)
)
)
Defaults.referrerRegistered = true
} catch {
debugPrint(error)
DefaultLogManager.shared.log(
event: "\(ReferralProgramService.self)_setReferent",
event: "\(ReferralProgramService.self)_register",
data: error.localizedDescription,
logLevel: LogLevel.error
)
if error.localizedDescription.contains("duplicate key value violates unique constraint") {
// The code is not unique so we look at the description
Defaults.referrerRegistered = true
throw ReferralProgramServiceError.alreadyRegistered
}
if error.localizedDescription.contains("timed out") || (error as NSError).code == NSURLErrorTimedOut {
throw ReferralProgramServiceError.timeOut
}
throw ReferralProgramServiceError.other
}
}
}

// MARK: - Helpers

private extension Swift.Error {
var isRetryable: Bool {
switch self {
case let error as ReferralProgramServiceError:
switch error {
case .timeOut:
return true
default:
return false
}
default:
return false
}
}
}
5 changes: 5 additions & 0 deletions p2p_wallet/Injection/Resolver+registerAllServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ extension Resolver: ResolverRegistering {
.implements(PriceService.self)
.scope(.application)

register {
CoingeckoEthereumPriceService(endpoint: "https://api.coingecko.com")
}
.scope(.application)

// Prices
register {
JupiterPriceServiceImpl(client: HTTPClient(
Expand Down
1 change: 1 addition & 0 deletions p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ struct DebugMenuView: View {
NavigationLink("Socket", destination: SocketDebugView())
NavigationLink("Web3Auth", destination: OnboardingDebugView())
NavigationLink("History") { HistoryDebugView() }
NavigationLink("Flags") { FlagDebugMenuView() }
}
}

Expand Down
27 changes: 27 additions & 0 deletions p2p_wallet/Scenes/DebugMenu/View/FlagDebugMenuView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftUI

struct FlagDebugMenuView: View {
var body: some View {
List {
DebugText(title: "ETH Address", value: "\(available(.ethAddressEnabled))")
DebugText(title: "Invest Solend", value: "\(available(.investSolendFeature))")
DebugText(title: "Mocked API Gateway", value: "\(available(.mockedApiGateway))")
DebugText(title: "Mocked TKey Facade", value: "\(available(.mockedTKeyFacade))")
DebugText(title: "Onboarding Username", value: "\(available(.onboardingUsernameEnabled))")
DebugText(
title: "Onboarding Username Button Skip",
value: "\(available(.onboardingUsernameButtonSkipEnabled))"
)
DebugText(title: "PnL", value: "\(available(.pnlEnabled))")
DebugText(title: "Referral Program", value: "\(available(.referralProgramEnabled))")
DebugText(title: "Send Via Link", value: "\(available(.sendViaLinkEnabled))")
DebugText(title: "Simulated Social Error", value: "\(available(.simulatedSocialError))")
DebugText(title: "Solana ETH Address", value: "\(available(.solanaEthAddressEnabled))")
DebugText(title: "Solana Negative Status", value: "\(available(.solanaNegativeStatus))")
DebugText(title: "Solend Disable Placeholder", value: "\(available(.solendDisablePlaceholder))")
DebugText(title: "Swap Transaction Simulation", value: "\(available(.swapTransactionSimulationEnabled))")
DebugText(title: "Sell Scenario", value: "\(available(.sellScenarioEnabled))")
}
.navigationTitle("Flags")
}
}
Loading

0 comments on commit ece3f47

Please sign in to comment.