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

MOB-2004 - Handle insufficient parent token balance on the UI #550

Merged
merged 4 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct CryptoSender: CryptoSenderProtocol {
}

func computeGasFeeFrom(maxCrypto: CryptoSendingSpec, on chain: ChainSpec, toAddress: HexAddress) async throws -> EVMCoinAmount {
.init(wei: 12300)
.init(units: 0.11)
}

func fetchGasPrices(on chain: ChainSpec) async throws -> EstimatedGasPrices {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,39 @@ struct BalanceTokenUIDescription: Hashable, Identifiable {
let balanceUsd: Double
var marketUsd: Double?
var marketPctChange24Hr: Double?
var parentSymbol: String?
var logoURL: URL?
var parentLogoURL: URL?
var parentMarketUSD: Double?
var parent: ParentDetails?
private(set) var isSkeleton: Bool = false

struct ParentDetails: Hashable {
var symbol: String
var balance: Double
var marketUsd: Double?
var logoURL: URL?

init(symbol: String, balance: Double, marketUsd: Double? = nil, logoURL: URL? = nil) {
self.symbol = symbol
self.balance = balance
self.marketUsd = marketUsd
self.logoURL = logoURL
}

init(token: WalletTokenPortfolio) {
symbol = token.symbol
balance = token.balanceAmt
marketUsd = token.value.marketUsdAmt
logoURL = URL(string: token.logoUrl ?? "")
}
}

static let iconSize: InitialsView.InitialsSize = .default
static let iconStyle: InitialsView.Style = .gray

var isERC20Token: Bool { parent != nil }

var balanceSymbol: String {
// For 'parent' token we show gas currency (which is ETH for Base token)
if parentSymbol == nil {
if parent == nil {
return gasCurrency
}
return symbol
Expand All @@ -53,7 +74,8 @@ struct BalanceTokenUIDescription: Hashable, Identifiable {
balance: Double,
balanceUsd: Double,
marketUsd: Double? = nil,
marketPctChange24Hr: Double? = nil) {
marketPctChange24Hr: Double? = nil,
parent: ParentDetails? = nil) {
self.chain = chain
self.symbol = symbol
self.gasCurrency = symbol
Expand All @@ -62,13 +84,12 @@ struct BalanceTokenUIDescription: Hashable, Identifiable {
self.balanceUsd = balanceUsd
self.marketUsd = marketUsd
self.marketPctChange24Hr = marketPctChange24Hr
self.parent = parent
}

init(chain: String,
walletToken: WalletTokenPortfolio.Token,
parentSymbol: String,
parentMarketUSD: Double?,
parentLogoURL: URL?) {
parent: ParentDetails) {
self.chain = chain
self.symbol = walletToken.symbol
self.gasCurrency = walletToken.gasCurrency
Expand All @@ -77,23 +98,17 @@ struct BalanceTokenUIDescription: Hashable, Identifiable {
self.balanceUsd = walletToken.value?.walletUsdAmt ?? 0
self.marketUsd = walletToken.value?.marketUsdAmt ?? 0
self.marketPctChange24Hr = walletToken.value?.marketPctChange24Hr
self.parentSymbol = parentSymbol
self.parentMarketUSD = parentMarketUSD
self.parentLogoURL = parentLogoURL
self.parent = parent
self.logoURL = URL(string: walletToken.logoUrl ?? "")
}

static func extractFrom(walletBalance: WalletTokenPortfolio) -> [BalanceTokenUIDescription] {
let tokenDescription = BalanceTokenUIDescription(walletBalance: walletBalance)
let parentSymbol = walletBalance.symbol
let parentMarketUSD = walletBalance.value.marketUsdAmt
let parentLogoURL = URL(string: walletBalance.logoUrl ?? "")
let parent = ParentDetails(token: walletBalance)
let chainSymbol = walletBalance.symbol
let subTokenDescriptions = walletBalance.tokens?.map({ BalanceTokenUIDescription(chain: chainSymbol,
walletToken: $0,
parentSymbol: parentSymbol,
parentMarketUSD: parentMarketUSD,
parentLogoURL: parentLogoURL) })
parent: parent) })
.filter({ $0.balanceUsd >= 1 }) ?? []

return [tokenDescription] + subTokenDescriptions
Expand Down Expand Up @@ -122,8 +137,8 @@ extension BalanceTokenUIDescription {
}

func loadParentIcon(iconUpdated: @escaping (UIImage?)->()) {
if let parentSymbol {
BalanceTokenUIDescription.loadIconFor(ticker: parentSymbol, logoURL: parentLogoURL, iconUpdated: iconUpdated)
if let parentSymbol = parent?.symbol {
BalanceTokenUIDescription.loadIconFor(ticker: parentSymbol, logoURL: parent?.logoURL, iconUpdated: iconUpdated)
} else {
iconUpdated(nil)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,24 @@ extension MockEntitiesFabric {
mockEthToken()
}

static func mockUSDTToken(parent: BlockchainType = .Ethereum) -> BalanceTokenUIDescription {
BalanceTokenUIDescription(chain: "ETH",
symbol: "USDT",
name: "USDT",
balance: 10,
balanceUsd: 10,
marketUsd: 1,
parent: .init(symbol: parent.rawValue,
balance: 0,
marketUsd: 3900))
}

static func mockEthToken() -> BalanceTokenUIDescription {
BalanceTokenUIDescription(chain: "ETH",
symbol: "ETH",
name: "Ethereum",
balance: 1,
balanceUsd: 1,
balanceUsd: 3900.34,
marketUsd: 3900.34)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ extension String {
static let balance = "BALANCE"
static let estimatedFee = "ESTIMATED_FEE"
static let insufficientBalance = "INSUFFICIENT_BALANCE"
static let insufficientToken = "INSUFFICIENT_TOKEN"
static let network = "NETWORK"

static let walletVerifiedInfoTitle = "WALLET_VERIFIED_INFO_TITLE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct BalanceTokenIconsView: View {
.skeletonable()
.clipShape(Circle())

if token.parentSymbol != nil {
if token.parent != nil {
Image(uiImage: parentIcon ?? .init())
.resizable()
.squareFrame(20)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,31 @@ private extension ConfirmSendAssetReviewInfoView {
.foregroundStyle(Color.foregroundSecondary)
Spacer()
}
.frame(width: geom.size.width * 0.38)
.frame(width: geom.size.width * 0.38)
HStack(spacing: 8) {
UIImageBridgeView(image: info.icon,
tintColor: info.iconColor)
.squareFrame(24)
.clipShape(Circle())
Text(info.value)
.font(.currentFont(size: 16, weight: .medium))
.foregroundStyle(info.valueColor)
if let subValue = info.subValue {
Text(subValue)
.font(.currentFont(size: 16, weight: .medium))
.foregroundStyle(Color.foregroundSecondary)
VStack(alignment: .leading,
spacing: -4) {
HStack(spacing: 8) {
Text(info.value)
.font(.currentFont(size: 16, weight: .medium))
.frame(height: 24)
.foregroundStyle(info.valueColor)
if let subValue = info.subValue {
Text(subValue)
.font(.currentFont(size: 16, weight: .medium))
.foregroundStyle(Color.foregroundSecondary)
}
}
if let errorMessage = info.errorMessage {
Text(errorMessage)
.font(.currentFont(size: 15, weight: .medium))
.foregroundStyle(Color.foregroundDanger)
.frame(height: 24)
}
}
}
Spacer()
Expand Down Expand Up @@ -157,6 +169,7 @@ private extension ConfirmSendAssetReviewInfoView {
let value: String
var valueColor: Color = .foregroundDefault
var subValue: String? = nil
var errorMessage: String? = nil
var actions: [InfoActionDescription] = []
var analyticName: Analytics.Button? = nil
}
Expand Down Expand Up @@ -198,7 +211,9 @@ private extension ConfirmSendAssetReviewInfoView {
switch asset {
case .token(let dataModel):
getSectionsForToken(selectedTxSpeed: dataModel.txSpeed,
gasUsd: dataModel.gasFeeUsd)
gasUsd: dataModel.gasFeeUsd,
gasFee: dataModel.gasFee,
token: dataModel.token)
case .domain:
getSectionsForDomain()
}
Expand Down Expand Up @@ -245,7 +260,9 @@ private extension ConfirmSendAssetReviewInfoView {
}

func getSectionsForToken(selectedTxSpeed: SendCryptoAsset.TransactionSpeed,
gasUsd: Double?) -> [SectionType] {
gasUsd: Double?,
gasFee: Double?,
token: BalanceTokenUIDescription) -> [SectionType] {
[getFromWalletInfoSection(),
getChainInfoSection(),
.infoValue(.init(title: String.Constants.speed.localized(),
Expand All @@ -255,12 +272,33 @@ private extension ConfirmSendAssetReviewInfoView {
valueColor: Color(uiColor: tintColorFor(txSpeed: selectedTxSpeed)),
actions: getTransactionSpeedActions(),
analyticName: .transactionSpeedSelection)),
.infoValue(.init(title: String.Constants.feeEstimate.localized(),
icon: .tildaIcon,
value: gasUsdTitleFor(gasUsd: gasUsd))),
getGasFeeSection(gasUsd: gasUsd,
gasFee: gasFee,
token: token),
.info(String.Constants.sendCryptoReviewPromptMessage.localized())]
}

func getGasFeeErrorMessage(gasFee: Double?,
token: BalanceTokenUIDescription) -> String? {
guard let gasFee else { return nil }

if let parent = token.parent,
parent.balance <= gasFee {
return String.Constants.insufficientToken.localized(parent.symbol)
}
return nil
}

func getGasFeeSection(gasUsd: Double?,
gasFee: Double?,
token: BalanceTokenUIDescription) -> SectionType {
.infoValue(.init(title: String.Constants.feeEstimate.localized(),
icon: .tildaIcon,
value: gasUsdTitleFor(gasUsd: gasUsd),
errorMessage: getGasFeeErrorMessage(gasFee: gasFee,
token: token)))
}

func getFromWalletInfoSection() -> SectionType {
.infoValue(.init(title: String.Constants.from.localized(),
icon: fromUserAvatar ?? sourceWallet.displayInfo.source.listIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ final class ConfirmSendTokenDataModel: ObservableObject {
var amount: SendCryptoAsset.TokenAssetAmountInput { data.amount }
var gasFeeUsd: Double? {
if let gasFee,
let marketUsd = data.token.parentMarketUSD ?? data.token.marketUsd {
let marketUsd = data.token.parent?.marketUsd ?? data.token.marketUsd {
return gasFee * marketUsd
}
return nil
}

init(data: SendCryptoAsset.SendTokenAssetData) {
self.data = data
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,27 @@ private extension ConfirmSendTokenView {
guard let gasFee = dataModel.gasFee else { return true }

let sendData = dataModel.data
let balance = sendData.token.balance
if sendData.isSendingAllTokens() {
return balance >= gasFee
}
let token = sendData.token

let tokenAmountToSend = sendData.getTokenAmountValueToSend()
return balance > tokenAmountToSend + gasFee
if let parent = token.parent { // ERC20 Token
let parentBalance = parent.balance
return parentBalance >= gasFee
} else {
let balance = token.balance
if sendData.isSendingAllTokens() {
return balance >= gasFee
}

let tokenAmountToSend = sendData.getTokenAmountValueToSend()
return balance > tokenAmountToSend + gasFee
}
}

@ViewBuilder
func confirmButton() -> some View {
VStack(spacing: isIPSE ? 6 : 16) {
if !hasSufficientFunds {
if !hasSufficientFunds,
!dataModel.token.isERC20Token {
insufficientFundsLabel()
} else {
totalValueLabel()
Expand Down Expand Up @@ -223,7 +231,8 @@ private extension ConfirmSendTokenView {
func totalValue(gasFee: Double) -> Double {
let sendData = dataModel.data
var amountToSpend = sendData.amount.valueOf(type: .tokenAmount, for: token)
if !sendData.isSendingAllTokens() {
if !sendData.isSendingAllTokens(),
!token.isERC20Token {
amountToSpend += gasFee
}
return amountToSpend
Expand All @@ -233,7 +242,13 @@ private extension ConfirmSendTokenView {
guard let marketUsd = token.marketUsd else { return "" }

let amountToSpend = totalValue(gasFee: gasFee)
let amountInUSD = amountToSpend * marketUsd
var amountInUSD = amountToSpend * marketUsd

if let parent = token.parent, // ERC20Token
let parentMarketUsd = parent.marketUsd {
amountInUSD += gasFee * parentMarketUsd
}

let formattedUSDAmount = formatCartPrice(amountInUSD)
return formattedUSDAmount
}
Expand All @@ -250,7 +265,11 @@ private extension ConfirmSendTokenView {
HStack(alignment: .top) {
Text(String.Constants.totalEstimate.localized())
Spacer()
Text("\(totalValueInUSD(gasFee: gasFee)) \(totalValueFormatted(gasFee: gasFee))")
if token.isERC20Token {
Text("\(totalValueInUSD(gasFee: gasFee))")
} else {
Text("\(totalValueInUSD(gasFee: gasFee)) \(totalValueFormatted(gasFee: gasFee))")
}
}
.font(.currentFont(size: 16))
.foregroundStyle(Color.foregroundSecondary)
Expand All @@ -274,8 +293,8 @@ private extension ConfirmSendTokenView {
PresentAsModalPreviewView {
NavigationStack {
ConfirmSendTokenView(data: .init(receiver: MockEntitiesFabric.SendCrypto.mockReceiver(),
token: MockEntitiesFabric.Tokens.mockUIToken(),
amount: .usdAmount(3998234.3),
token: MockEntitiesFabric.Tokens.mockUSDTToken(),
amount: .tokenAmount(0.999),
receiverAddress: "0x1234567890"))
.navigationBarTitleDisplayMode(.inline)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ private extension SelectTokenAssetAmountToSendView {
func maxButtonPressed() {
guard !isUsingMax else { return }

pullUp = .default(.maxCryptoSendInfoPullUp(token: token))
if !token.isERC20Token {
pullUp = .default(.maxCryptoSendInfoPullUp(token: token))
}
setMaxInputValue()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ private extension WalletsDataService {
if pendingTransactions.filterPending(extraCondition: { $0.operation == .transferDomain }).first(where: { $0.domainName == domain.name }) != nil {
domainState = .transfer
} else if pendingTransactions.containMintingInProgress(domain) {
domainState = .minting
domainState = .transfer
} else if pendingTransactions.containReverseResolutionOperationProgress(domain) {
domainState = .updatingReverseResolution
} else if pendingTransactions.containPending(domain) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@
"BALANCE" = "Balance";
"ESTIMATED_FEE" = "Estimated fee";
"INSUFFICIENT_BALANCE" = "Insufficient balance";
"INSUFFICIENT_TOKEN" = "Insufficient %@";
"NETWORK" = "Network";

"WALLET_VERIFIED_INFO_TITLE" = "Verified";
Expand Down