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

1.5.0 Hot fixes #1070

Merged
merged 5 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
2 changes: 1 addition & 1 deletion Aux/Config/Common.xcconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// MARK: - Custom flags

/// Application version shared across all targets and flavours
APP_VERSION = 1.5.0
APP_VERSION = 1.5.1

/// App Icon base name
APP_ICON = AppIcon
Expand Down
12 changes: 12 additions & 0 deletions RadixWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,7 @@
835F196D2B3581C300E0B71D /* UnknownDappComponents+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835F196C2B3581C300E0B71D /* UnknownDappComponents+View.swift */; };
836878612B4551910029C808 /* UnknownDappComponents+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836878602B4551910029C808 /* UnknownDappComponents+Reducer.swift */; };
8370FC5C2B99C780007AD882 /* NPSSurveyClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8370FC5B2B99C780007AD882 /* NPSSurveyClient+Interface.swift */; };
8381C8B02BBD2CD400A470B4 /* TokenPriceCientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8381C8AF2BBD2CD400A470B4 /* TokenPriceCientTests.swift */; };
83823EA72B722DB000827211 /* HTTPClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83823EA62B722DB000827211 /* HTTPClient+Interface.swift */; };
83823EAA2B72365000827211 /* TokenPriceClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83823EA92B72365000827211 /* TokenPriceClient+Interface.swift */; };
83856D622B0279080026452A /* VerifyMnemonic+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83856D602B0279080026452A /* VerifyMnemonic+View.swift */; };
Expand Down Expand Up @@ -2401,6 +2402,7 @@
835F196C2B3581C300E0B71D /* UnknownDappComponents+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnknownDappComponents+View.swift"; sourceTree = "<group>"; };
836878602B4551910029C808 /* UnknownDappComponents+Reducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UnknownDappComponents+Reducer.swift"; sourceTree = "<group>"; };
8370FC5B2B99C780007AD882 /* NPSSurveyClient+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NPSSurveyClient+Interface.swift"; sourceTree = "<group>"; };
8381C8AF2BBD2CD400A470B4 /* TokenPriceCientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPriceCientTests.swift; sourceTree = "<group>"; };
83823EA62B722DB000827211 /* HTTPClient+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPClient+Interface.swift"; sourceTree = "<group>"; };
83823EA92B72365000827211 /* TokenPriceClient+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TokenPriceClient+Interface.swift"; sourceTree = "<group>"; };
83856D602B0279080026452A /* VerifyMnemonic+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VerifyMnemonic+View.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6827,6 +6829,14 @@
path = NPSSurveyClient;
sourceTree = "<group>";
};
8381C8AE2BBD2CC100A470B4 /* TokenPriceCientTests */ = {
isa = PBXGroup;
children = (
8381C8AF2BBD2CD400A470B4 /* TokenPriceCientTests.swift */,
);
path = TokenPriceCientTests;
sourceTree = "<group>";
};
83823EA52B722D9000827211 /* HTTPClient */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -7448,6 +7458,7 @@
E6DBA2E02ADEBFB300A38425 /* Clients */ = {
isa = PBXGroup;
children = (
8381C8AE2BBD2CC100A470B4 /* TokenPriceCientTests */,
48FF43132AE4364000C568B9 /* KeychainClientTests */,
E6DBA2E32ADEBFB300A38425 /* OnLedgerEntitiesClientTests */,
E6DBA2EA2ADEBFB300A38425 /* ROLAClientTests */,
Expand Down Expand Up @@ -7899,6 +7910,7 @@
E6DBA3542ADEBFB300A38425 /* SLIP10PathTests.swift in Sources */,
8328806C2AE6725F0014FBF3 /* AccountPreferencesTests.swift in Sources */,
E6DBA3192ADEBFB300A38425 /* PackagesEncodingDecodingTests.swift in Sources */,
8381C8B02BBD2CD400A470B4 /* TokenPriceCientTests.swift in Sources */,
E6DBA34A2ADEBFB300A38425 /* EncryptionTests.swift in Sources */,
E6DBA3472ADEBFB300A38425 /* HexStringToDataTests.swift in Sources */,
E6DBA35A2ADEBFB300A38425 /* BIP44Tests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ extension AccountPortfoliosClient: DependencyKey {
cacheClient.removeFolder(.onLedgerEntity(.account(accountAddress.asGeneral)))
}

let account = try await onLedgerEntitiesClient.getAccount(accountAddress)
let account = try await onLedgerEntitiesClient.getAccount(accountAddress).nonEmptyVaults
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was causing users to see tokens with zero values when they did performa pull to refresh in Account details

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is done here, will that not affect all the endpoints on the client? In transaction history I need the full portfolio, including empty vaults.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right should be able to do it later, in view layer is Gustafs meaning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably will it will,
will move this at UI level

let portfolio = AccountPortfolio(account: account)

let currentResources = await state.tokenPrices.keys
Expand Down
28 changes: 18 additions & 10 deletions RadixWallet/Clients/TokenPriceClient/TokenPriceClient+Live.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ extension TokenPricesClient {
@Dependency(\.jsonDecoder) var jsonDecoder
@Dependency(\.jsonEncoder) var jsonEncoder
@Dependency(\.cacheClient) var cacheClient
#if DEBUG
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt to always use prod token price service to facilitate testing. Dev endpoint might not have all prices available or latest ones.

let rootURL = URL(string: "https://dev-token-price.extratools.works")!
#else

let rootURL = URL(string: "https://token-price-service.radixdlt.com")!
#endif

@Sendable
func getTokenPrices(_ fetchRequest: FetchPricesRequest) async throws -> TokenPrices {
Expand Down Expand Up @@ -43,12 +40,14 @@ extension TokenPricesClient {
}

extension TokenPricesClient.TokenPrices {
fileprivate init(_ tokenPricesResponse: TokensPriceResponse) {
public init(_ tokenPricesResponse: TokensPriceResponse) {
let formatter = NumberFormatter()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have a RETDecimal initializer in Sargon to handle the case of providing a Double with more than 18 decimal places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I can fix one!

formatter.locale = Locale(identifier: "en_US_POSIX") // Just ignore the users locale
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = Int(RETDecimal.maxDivisibility)
formatter.roundingMode = .down
formatter.decimalSeparator = "." // Enfore dot notation for RETDecimal
formatter.decimalSeparator = "." // Enforce dot notation for RETDecimal
formatter.usesGroupingSeparator = false // No grouping separator for RETDecimal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having this caused the prices > 1000 to be formatted with a thousand separator which then couldn't be converted to RETDecimal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is another initialiser than value: String, there is formatted: String as well. which should be able to accept strings with a thousands separator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we are rounding the Double that represents the price, by first converting the price Double to a string (using a fixed decimal separator) and then converting it back to RETDecimal?

  1. I presume that RETDecimal(value: String) is the version that takes a machine readable string, so that it ignores the user preference on decimal separator, because otherwise this wouldn't work, but even so, I don't like this, it doesn't seem nice or necessary to convert the price to a Double, then a String and the RETDecimal, just in order to round it.

  2. If we do want to round it here, can't we convert it (back) to RETDecimal and just round that, without going by way of String?

  3. Why do we want price to be a Double at all? This operation here would be even simpler if it was kept a RETDecimal until it's to be displayed.

  4. Is the behaviour actually correct? What is the purpose of clamping the number of decimals in the price according to the divisibility, isn't the price here given in a fiat currency?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is another initialiser than value: String, there is formatted: String as well. which should be able to accept strings with a thousands separator.

Presumably (hopefully), the latter respects the user's locale, and the former does not, and in this case we don't want to respect the locale, since we hard code the decimal separator.

Copy link
Contributor Author

@GhenadieVP GhenadieVP Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kugel3 note that this whole shenanigans is not about rounding, it is about truncating to RETDecimal max divisibility, otherwise we cannot initialise the RETDecimal with a double that has more decimals than 18.

The price is double as it comes from the TokenPrice service, here we do immediately convert it to RETDecimal.

Copy link
Contributor

@kugel3 kugel3 Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, then I understand your other comment about an initialiser for RETDecimal that takes a Double with arbitrary number of decimals, that would be highly preferable. Since NumberFormatter is pretty expensive to initialise, we probably don't want to make that initialiser in Swift.

But while we have this workaround, I think we should be more explicit about what is going on, so that nobody accidentally changes to using the other string based initialiser for example. Because RETDecimal(formatted: String) could not actually be used here? It does respect locale, right? @CyonAlexRDX

Perhaps change // Enforce dot notation for RETDecimal to something like // Use a period as decimal separator, for the sake of RETDecimal(formatted: String).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes RETDecimal.init(formattedStrinf:locale) is respects locale


self = tokenPricesResponse.tokens.reduce(into: [:]) { partialResult, next in
let trimmed = formatter.string(for: next.price) ?? ""
Expand All @@ -60,19 +59,28 @@ extension TokenPricesClient.TokenPrices {
}

// MARK: - TokensPriceResponse
private struct TokensPriceResponse: Decodable {
public struct TokensPriceResponse: Decodable {
public let tokens: [TokenPrice]

public init(tokens: [TokenPrice]) {
self.tokens = tokens
}
}

// MARK: TokensPriceResponse.TokenPrice
extension TokensPriceResponse {
struct TokenPrice: Decodable {
public struct TokenPrice: Decodable {
enum CodingKeys: String, CodingKey {
case resourceAddress = "resource_address"
case price = "usd_price"
}

let resourceAddress: ResourceAddress
let price: Double
public let resourceAddress: ResourceAddress
public let price: Double

public init(resourceAddress: ResourceAddress, price: Double) {
self.resourceAddress = resourceAddress
self.price = price
}
}
}
13 changes: 13 additions & 0 deletions RadixWallet/Core/FeaturePrelude/AddressView/AddressFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,16 @@ extension NonFungibleLocalId {
}
}
}

extension TXID {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha! I suspected we had a regression, because I started adding this in Sargon for transaction hashes but then tested main branch and saw we did not truncated. I should have spoken up :) @micbakos-rdx we can you add a PR implementing this in Sargon?

public func formatted(_ format: AddressFormat = .default) -> String {
let str = asStr()

switch format {
case .default:
return str.truncatedMiddle(keepFirst: 4, last: 6)
case .full, .raw:
return str
}
}
}
4 changes: 0 additions & 4 deletions RadixWallet/EngineKit/TXID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import EngineToolkit
public typealias TXID = TransactionHash

extension TXID {
public func formatted(_ format: AddressFormat = .default) -> String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GhenadieVP merge conflict imminent, but I see, it must happen, since you needed to implement this a method with this signature.

asStr()
}

public var hex: String {
bytes().hex()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@testable import Radix_Wallet_Dev
import XCTest

final class TokenPriceCientTests: XCTestCase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests! 🙂

func test_zeroPrice() {
validateDecimalPriceConversion(0, expected: .zero())
}

func test_noDecimalPlaces_1() {
validateDecimalPriceConversion(10, expected: 10)
}

func test_noDecimalPlaces_2() {
validateDecimalPriceConversion(10000, expected: 10000)
}

func test_noDecimalPlaces_3() {
validateDecimalPriceConversion(10_000_000, expected: 10_000_000)
}

func test_withDecimalPlaces_1() {
validateDecimalPriceConversion(1.99, expected: try! .init(value: "1.99"))
}

func test_withDecimalPlaces_2() {
validateDecimalPriceConversion(1.000099, expected: try! .init(value: "1.000099"))
}

func test_belowOne_1() {
validateDecimalPriceConversion(0.99, expected: 0.99)
}

func test_belowOne_2() {
validateDecimalPriceConversion(0.000099, expected: try! .init(value: "0.000099"))
}

// NOTE: All of the below values would be rounded to 14 decimal places
// As it seems that Swift number formatter for Double cannot express more decimals.
func test_closeToRETDecimalDivisibility() {
// 17 decimal places
validateDecimalPriceConversion(1.12345678901234567, expected: try! .init(value: "1.12345678901235"))
}

func test_maxRETDecimalDivisibility() {
// 18 decimal places
validateDecimalPriceConversion(1.123456789012345678, expected: try! .init(value: "1.12345678901235"))
}

func test_overMaxRETDecimalDivisibility() {
// 22 decimal places
validateDecimalPriceConversion(1.1234567890123456789012, expected: try! .init(value: "1.12345678901235"))
}

private func validateDecimalPriceConversion(
_ price: Double,
expected: RETDecimal,
file: StaticString = #filePath,
line: UInt = #line
) {
let tokenPrice = tokenWithPrice(price)
guard let decimalPrice = TokenPricesClient.TokenPrices(tokenPrice).first?.value else {
XCTFail("Could'nt convert \(tokenPrice) to RETDecimal", file: file, line: line)
return
}
XCTAssertEqual(
decimalPrice,
expected,
"expected \(expected.formattedPlain()), got \(decimalPrice.formattedPlain())",
file: file,
line: line
)
}

private func tokenWithPrice(_ price: Double) -> TokensPriceResponse {
TokensPriceResponse(tokens: [
TokensPriceResponse.TokenPrice(
resourceAddress: try! .init(validatingAddress: "resource_rdx1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxradxrd"),
price: price
),
])
}
}
Loading