Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PWN-9234] Fixes for release #1514

Merged
merged 23 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,20 @@ class EthereumTokenDataCorrection {
default:
return token
}
} else {
// Native token
return fixNativeToken(token: token)
}
}

return token
private func fixNativeToken(token: EthereumToken) -> EthereumToken {
EthereumToken(
name: token.name,
symbol: token.symbol,
decimals: token.decimals,
logo: URL(string: SolanaToken.eth.logoURI ?? ""),
contractType: token.contractType
)
}

private func fixUSDT(token: EthereumToken) -> EthereumToken {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Web3

/// The repository for fetching token metadata and cache for later usage.
public final class EthereumTokensRepository {
let dataCorrection: EthereumTokenDataCorrection = .init()

/// Provider
let provider: KeyAppTokenProvider

Expand Down Expand Up @@ -67,7 +69,7 @@ public final class EthereumTokensRepository {
try? await database.write(for: "native", value: nativeToken)
try? await database.flush()

return nativeToken
return dataCorrection.correct(token: nativeToken)
}

/// Resolve ERC-20 token by address.
Expand All @@ -90,6 +92,9 @@ public final class EthereumTokensRepository {
// There is no missing addresses
if missingTokenAddresses.isEmpty {
return result
.mapValues { token in
dataCorrection.correct(token: token)
}
}

// Fetch
Expand Down Expand Up @@ -124,6 +129,9 @@ public final class EthereumTokensRepository {
try? await database.flush()

return result
.mapValues { token in
dataCorrection.correct(token: token)
}
}

public func resolve(address: EthereumAddress) async throws -> EthereumToken? {
Expand Down
59 changes: 51 additions & 8 deletions Packages/KeyAppKit/Sources/KeyAppBusiness/Price/PriceService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,29 @@ import Foundation
import KeyAppKitCore
import SolanaSwift

public struct PriceServiceOptions: OptionSet {
public let rawValue: Int

public init(rawValue: Int) {
self.rawValue = rawValue
}

public static let actualPrice = PriceServiceOptions(rawValue: 1 << 0)
}

/// Abstract class for getting exchange rate between token and fiat for any token.
public protocol PriceService: AnyObject {
func getPrice(token: AnyToken, fiat: String) async throws -> TokenPrice?
func getPrices(tokens: [AnyToken], fiat: String) async throws -> [SomeToken: TokenPrice]
func getPrice(
token: AnyToken,
fiat: String,
options: PriceServiceOptions
) async throws -> TokenPrice?

func getPrices(
tokens: [AnyToken],
fiat: String,
options: PriceServiceOptions
) async throws -> [SomeToken: TokenPrice]

/// Emit request event to fetch new price.
var onChangePublisher: AnyPublisher<Void, Never> { get }
Expand All @@ -24,6 +43,22 @@ public protocol PriceService: AnyObject {
func clear() async throws
}

public extension PriceService {
func getPrice(
token: AnyToken,
fiat: String
) async throws -> TokenPrice? {
try await getPrice(token: token, fiat: fiat, options: [])
}

func getPrices(
tokens: [AnyToken],
fiat: String
) async throws -> [SomeToken: TokenPrice] {
try await getPrices(tokens: tokens, fiat: fiat, options: [])
}
}

/// This class service allow client to get exchange rate between token and fiat.
///
/// Each rate has 15 minutes lifetime. When the lifetime is expired, the new rate will be requested.
Expand Down Expand Up @@ -70,7 +105,10 @@ public class PriceServiceImpl: PriceService {
)
}

public func getPrices(tokens: [AnyToken], fiat: String) async throws -> [SomeToken: TokenPrice] {
public func getPrices(
tokens: [AnyToken], fiat: String,
options: PriceServiceOptions
) async throws -> [SomeToken: TokenPrice] {
let fiat = fiat.lowercased()
var shouldSynchronise = false

Expand All @@ -96,8 +134,14 @@ public class PriceServiceImpl: PriceService {
for token in tokens {
let token = token.asSomeToken

if result[token] == nil {
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)
}
}
}

Expand Down Expand Up @@ -138,8 +182,8 @@ public class PriceServiceImpl: PriceService {
}
}

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

Expand Down Expand Up @@ -200,9 +244,8 @@ public class PriceServiceImpl: PriceService {
/// Adjust prices for stable coin (usdc, usdt) make it equal to 1 if not depegged more than 2%
if
case let .contract(address) = token.primaryKey,
[SolanaToken.usdc.mintAddress, SolanaToken.usdt.mintAddress].contains(address),
[PublicKey.usdcMint.base58EncodedString, PublicKey.usdtMint.base58EncodedString].contains(address),
token.network == .solana,
fiat.uppercased() == "USD",
(abs(parsedValue - 1.0) * 100) <= 2
{
parsedValue = 1.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import Combine
import Foundation

public actor SynchronizedDatabase<Key: Hashable & Codable, Value: Codable> {
public actor SynchronizedDatabase<Key: Hashable & Codable, Value: Codable & Equatable> {
private var data: [Key: Value] = [:] {
didSet {
onUpdate.send(data)
if oldValue != data {
onUpdate.send(data)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public protocol UserActionConsumer {
var persistence: UserActionPersistentStorage { get }

/// Update stream
var onUpdate: AnyPublisher<any UserAction, Never> { get }
var onUpdate: AnyPublisher<[any UserAction], Never> { get }

/// Fire action.
func start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@ public class UserActionService {

/// Thread safe array update
let accessQueue = DispatchQueue(label: "UserActionUpdateQueue", attributes: .concurrent)
#warning("REFACTOR: Remove @Published from non-observable class")
@Published public var actions: [any UserAction] = []

private let actionsSubject: CurrentValueSubject<[String: any UserAction], Never> = .init([:])

public var actions: AnyPublisher<[any UserAction], Never> {
actionsSubject
.map { Array($0.values) }
.eraseToAnyPublisher()
}

public init(consumers: [any UserActionConsumer]) {
self.consumers = consumers

for consumer in consumers {
consumer.onUpdate.sink { [weak self] userAction in
self?.update(action: userAction)
consumer.onUpdate.sink { [weak self] userActions in
self?.update(actions: userActions)
}
.store(in: &subscriptions)

Expand All @@ -46,24 +52,24 @@ public class UserActionService {
}

/// Internal method for updating action. The consumer will emits value and pass to this method.
func update(action: any UserAction) {
func update(actions: [any UserAction]) {
accessQueue.async(flags: .barrier) { [weak self] in
guard let self else { return }

let idx = self.actions.firstIndex { $0.id == action.id }
if let idx {
self.actions[idx] = action
} else {
self.actions.append(action)
var value = self.actionsSubject.value
for action in actions {
value[action.id] = action
}

self.actionsSubject.value = value
}
}

/// Observer user action.
public func observer<Action: UserAction>(action: Action) -> AnyPublisher<Action, Never> {
$actions
actionsSubject
.map { actions -> Action? in
let action = actions.first { $0.id == action.id }
let action = actions[action.id]
guard let action = action as? Action else { return nil }
return action
}
Expand All @@ -73,11 +79,9 @@ public class UserActionService {
}

public func observer(id: String) -> AnyPublisher<any UserAction, Never> {
$actions
actionsSubject
.map { actions -> (any UserAction)? in
let action = actions.first { $0.id == id }
guard let action else { return nil }
return action
actions[id]
}
.compactMap { $0 }
.eraseToAnyPublisher()
Expand All @@ -87,7 +91,7 @@ public class UserActionService {
public func getActions() async -> [any UserAction] {
await withCheckedContinuation { continuation in
accessQueue.sync {
continuation.resume(returning: actions)
continuation.resume(returning: Array(actionsSubject.value.values))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ public class WormholeSendUserActionConsumer: UserActionConsumer {

public let persistence: UserActionPersistentStorage

public var onUpdate: AnyPublisher<any UserAction, Never> {
public var onUpdate: AnyPublisher<[any UserAction], Never> {
database
.onUpdate
.flatMap { data in
Publishers.Sequence(sequence: Array(data.values))
.map { data in
Array(data.values)
}
.eraseToAnyPublisher()
}
Expand Down Expand Up @@ -92,7 +92,7 @@ public class WormholeSendUserActionConsumer: UserActionConsumer {
switch event {
case let .track(sendStatus):
Task { [weak self] in
guard let self = self else { return }
guard let self else { return }

let userAction = try? await WormholeSendUserAction(
sendStatus: sendStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ public class WormholeClaimUserActionConsumer: UserActionConsumer {
public let persistence: UserActionPersistentStorage

/// Stream of updating user action.
public var onUpdate: AnyPublisher<any UserAction, Never> {
public var onUpdate: AnyPublisher<[any UserAction], Never> {
database
.onUpdate
.flatMap { data in Publishers.Sequence(sequence: Array(data.values)) }
.map { data in Array(data.values) }
.eraseToAnyPublisher()
}

Expand Down Expand Up @@ -149,7 +149,10 @@ public class WormholeClaimUserActionConsumer: UserActionConsumer {

guard case var .pending(rawBundle) = action.internalState else {
let error = Error.claimFailure
self?.errorObserver.handleError(error, userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action])
self?.errorObserver.handleError(
error,
userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action]
)
self?.handleInternalEvent(
event: .claimFailure(
bundleID: action.bundleID, reason: error
Expand All @@ -163,7 +166,10 @@ public class WormholeClaimUserActionConsumer: UserActionConsumer {
do {
try rawBundle.signBundle(with: keyPair)
} catch {
self?.errorObserver.handleError(Error.claimFailure, userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action])
self?.errorObserver.handleError(
Error.claimFailure,
userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action]
)
self?.handleInternalEvent(event: .claimFailure(bundleID: action.bundleID, reason: .signingFailure))
}

Expand All @@ -172,11 +178,17 @@ public class WormholeClaimUserActionConsumer: UserActionConsumer {
try await self?.wormholeAPI.sendEthereumBundle(bundle: rawBundle)
self?.handleInternalEvent(event: .claimInProgress(bundleID: action.bundleID))
} catch {
self?.errorObserver.handleError(error, userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action])
self?.errorObserver.handleError(
error,
userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action]
)

let error = Error.submitError

self?.errorObserver.handleError(error, userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action])
self?.errorObserver.handleError(
error,
userInfo: [WormholeClaimUserActionError.UserInfoKey.action.rawValue: action]
)
self?.handleInternalEvent(event: .claimFailure(bundleID: action.bundleID, reason: error))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "a2e487b77f17edbce9a65f2b7415f2f479dc8e48",
"version" : "2.57.0"
"revision" : "324bc65a28323660fad8a36a7a37f0c2c78eeb9a",
"version" : "2.55.0"
}
},
{
Expand Down
Loading
Loading