Skip to content
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
117 changes: 73 additions & 44 deletions src/components/scenes/RampCreateScene.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useFocusEffect } from '@react-navigation/native'
import { useQuery } from '@tanstack/react-query'
import { div, gt, mul, round, toBns } from 'biggystring'
import type {
EdgeCurrencyWallet,
EdgeDenomination,
EdgeTokenId
} from 'edge-core-js'
import * as React from 'react'
import { useState } from 'react'
import { ActivityIndicator, Text, View } from 'react-native'
Expand All @@ -15,6 +20,7 @@ import {
} from '../../actions/SettingsActions'
import { FLAG_LOGO_URL } from '../../constants/CdnConstants'
import { COUNTRY_CODES, FIAT_COUNTRY } from '../../constants/CountryConstants'
import { getSpecialCurrencyInfo } from '../../constants/WalletAndCurrencyConstants'
import { useHandler } from '../../hooks/useHandler'
import { useRampLastCryptoSelection } from '../../hooks/useRampLastCryptoSelection'
import { useRampPlugins } from '../../hooks/useRampPlugins'
Expand Down Expand Up @@ -43,7 +49,11 @@ import type { GuiFiatType } from '../../types/types'
import { getCurrencyCode } from '../../util/CurrencyInfoHelpers'
import { getHistoricalFiatRate } from '../../util/exchangeRates'
import { logEvent } from '../../util/tracking'
import { DECIMAL_PRECISION, mulToPrecision } from '../../util/utils'
import {
convertNativeToDenomination,
DECIMAL_PRECISION,
mulToPrecision
} from '../../util/utils'
import { DropdownInputButton } from '../buttons/DropdownInputButton'
import { EdgeButton } from '../buttons/EdgeButton'
import { PillButton } from '../buttons/PillButton'
Expand Down Expand Up @@ -330,7 +340,7 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {

// Fetch quotes using the custom hook
const {
quotes: sortedQuotes,
quotes: allQuotes,
isLoading: isLoadingQuotes,
isFetching: isFetchingQuotes,
errors: quoteErrors
Expand All @@ -342,19 +352,19 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
})

// Get the best quote using .find because we want to preserve undefined in its type
const bestQuote = sortedQuotes.find((_, index) => index === 0)
const bestQuote = allQuotes.find((_, index) => index === 0)

// For Max flow, select the quote with the largest supported amount
const maxQuoteForMaxFlow = React.useMemo(() => {
if (!('max' in exchangeAmount) || sortedQuotes.length === 0) return null
if (!('max' in exchangeAmount) || allQuotes.length === 0) return null

const picked = sortedQuotes.reduce((a, b): RampQuote => {
const picked = allQuotes.reduce((a, b): RampQuote => {
const aAmount = lastUsedInput === 'crypto' ? a.cryptoAmount : a.fiatAmount
const bAmount = lastUsedInput === 'crypto' ? b.cryptoAmount : b.fiatAmount
return gt(bAmount, aAmount) ? b : a
})
return picked
}, [exchangeAmount, sortedQuotes, lastUsedInput])
}, [exchangeAmount, allQuotes, lastUsedInput])

// Calculate exchange rate from best quote
const quoteExchangeRate = React.useMemo(() => {
Expand Down Expand Up @@ -387,10 +397,7 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
if ('empty' in exchangeAmount) return ''

if ('max' in exchangeAmount) {
if (maxQuoteForMaxFlow != null) {
return maxQuoteForMaxFlow.fiatAmount ?? ''
}
return ''
return maxQuoteForMaxFlow?.fiatAmount ?? ''
}

if (lastUsedInput === 'fiat') {
Expand Down Expand Up @@ -422,10 +429,10 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
if ('empty' in exchangeAmount || lastUsedInput === null) return ''

if ('max' in exchangeAmount) {
if (maxQuoteForMaxFlow != null) {
return maxQuoteForMaxFlow.cryptoAmount ?? ''
}
return ''
return (
maxQuoteForMaxFlow?.cryptoAmount ??
(typeof exchangeAmount.max === 'string' ? exchangeAmount.max : '')
)
}

if (lastUsedInput === 'crypto') {
Expand Down Expand Up @@ -561,20 +568,36 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
setLastUsedInput('crypto')
})

const handleMaxPress = useHandler(() => {
const handleMaxPress = useHandler(async () => {
// Preconditions to submit a max request
if (
selectedWallet == null ||
countryCode === '' ||
denomination == null ||
selectedCrypto == null ||
selectedCryptoCurrencyCode == null ||
countryCode === ''
selectedWallet == null
) {
return
}

// Trigger a transient max flow: request quotes with {max:true} and auto-navigate when ready
setPendingMaxNav(true)
setLastUsedInput('fiat')
setExchangeAmount({ max: true })
setLastUsedInput(direction === 'buy' ? 'fiat' : 'crypto')

if (direction === 'sell') {
const maxSpendExchangeAmount = await getMaxSpendExchangeAmount(
selectedWallet,
selectedCrypto.tokenId,
denomination
)
setExchangeAmount({
max: maxSpendExchangeAmount
})
} else {
setExchangeAmount({
max: true
})
}
})

// Auto-navigate once a best quote arrives for the transient max flow
Expand All @@ -588,21 +611,6 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
maxQuoteForMaxFlow != null &&
!isLoadingQuotes
) {
// Persist the chosen amount so it remains after returning
if (
!amountTypeSupport.onlyCrypto &&
maxQuoteForMaxFlow.fiatAmount != null
) {
setLastUsedInput('fiat')
setExchangeAmount({ amount: maxQuoteForMaxFlow.fiatAmount })
} else if (
!amountTypeSupport.onlyFiat &&
maxQuoteForMaxFlow.cryptoAmount != null
) {
setLastUsedInput('crypto')
setExchangeAmount({ amount: maxQuoteForMaxFlow.cryptoAmount })
}

navigation.navigate('rampSelectOption', {
rampQuoteRequest
})
Expand Down Expand Up @@ -637,13 +645,9 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
)
}

const fiatInputDisabled =
('max' in exchangeAmount && sortedQuotes.length > 0) ||
amountTypeSupport.onlyCrypto
const fiatInputDisabled = amountTypeSupport.onlyCrypto
const cryptoInputDisabled =
isLoadingPersistedCryptoSelection ||
('max' in exchangeAmount && sortedQuotes.length > 0) ||
amountTypeSupport.onlyFiat
isLoadingPersistedCryptoSelection || amountTypeSupport.onlyFiat

// Render trade form view
return (
Expand Down Expand Up @@ -778,7 +782,7 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
denomination == null ||
'empty' in exchangeAmount ||
lastUsedInput == null ||
(!isLoadingQuotes && sortedQuotes.length === 0) ? null : (
(!isLoadingQuotes && allQuotes.length === 0) ? null : (
<>
<EdgeText style={styles.exchangeRateTitle}>
{lstrings.trade_create_exchange_rate}
Expand All @@ -799,7 +803,7 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
// Nothing is loading
!isResultLoading &&
// Nothing was returned
sortedQuotes.length === 0 &&
allQuotes.length === 0 &&
quoteErrors.length === 0 &&
// User has queried
!('empty' in exchangeAmount) &&
Expand All @@ -825,7 +829,7 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
}

{!isResultLoading &&
sortedQuotes.length === 0 &&
allQuotes.length === 0 &&
supportedPlugins.length > 0 &&
!('empty' in exchangeAmount) ? (
supportedPluginsError != null ? (
Expand Down Expand Up @@ -860,7 +864,7 @@ export const RampCreateScene: React.FC<Props> = (props: Props) => {
'empty' in exchangeAmount ||
lastUsedInput === null ||
supportedPlugins.length === 0 ||
sortedQuotes.length === 0 ||
allQuotes.length === 0 ||
(lastUsedInput === 'fiat' && amountTypeSupport.onlyCrypto) ||
(lastUsedInput === 'crypto' && amountTypeSupport.onlyFiat)
}
Expand Down Expand Up @@ -998,3 +1002,28 @@ function getRoundedFiatEquivalent(fiatAmount: string, rate: string): string {
usdAmount = round(usdAmount, usdAmount.length - 1)
return usdAmount
}

async function getMaxSpendExchangeAmount(
wallet: EdgeCurrencyWallet,
tokenId: EdgeTokenId,
denomination: EdgeDenomination
): Promise<string> {
async function getDummyAddress(): Promise<string> {
const pluginId = wallet.currencyInfo.pluginId
const dummyPublicAddress =
getSpecialCurrencyInfo(pluginId).dummyPublicAddress
if (dummyPublicAddress != null) {
return dummyPublicAddress
}
const addresses = await wallet.getAddresses({ tokenId: null })
return addresses.length > 0 ? addresses[0].publicAddress : ''
}
const maxSpendNativeAmount = await wallet.getMaxSpendable({
tokenId,
spendTargets: [{ publicAddress: await getDummyAddress() }]
})
const maxSpendExchangeAmount = convertNativeToDenomination(
denomination.multiplier
)(maxSpendNativeAmount)
return maxSpendExchangeAmount
}
15 changes: 13 additions & 2 deletions src/plugins/ramps/banxa/banxaRampPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,10 +890,14 @@ export const banxaRampPlugin: RampPluginFactory = (
} = request
const currencyPluginId = request.wallet.currencyInfo.pluginId

const isMaxAmount =
'max' in request.exchangeAmount && request.exchangeAmount.max
const isMaxAmount = 'max' in request.exchangeAmount
const exchangeAmount =
'amount' in request.exchangeAmount ? request.exchangeAmount.amount : ''
const maxAmountLimit =
'max' in request.exchangeAmount &&
typeof request.exchangeAmount.max === 'string'
? request.exchangeAmount.max
: undefined

// Fetch provider configuration (cached or fresh)
const config = await fetchProviderConfig()
Expand Down Expand Up @@ -1059,6 +1063,13 @@ export const banxaRampPlugin: RampPluginFactory = (
continue
}
maxAmountString = maxPriceRow.coin_amount

if (
maxAmountLimit != null &&
gt(maxAmountString, maxAmountLimit)
) {
maxAmountString = maxAmountLimit
}
}
}

Expand Down
12 changes: 10 additions & 2 deletions src/plugins/ramps/bity/bityRampPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,10 +685,14 @@ export const bityRampPlugin = (pluginConfig: RampPluginConfig): RampPlugin => {
const currencyPluginId = request.wallet.currencyInfo.pluginId
const isBuy = direction === 'buy'

const isMaxAmount =
'max' in request.exchangeAmount && request.exchangeAmount.max
const isMaxAmount = 'max' in request.exchangeAmount
const exchangeAmount =
'amount' in request.exchangeAmount ? request.exchangeAmount.amount : ''
const maxAmountLimit =
'max' in request.exchangeAmount &&
typeof request.exchangeAmount.max === 'string'
? request.exchangeAmount.max
: undefined

// Validate region using helper function
if (!isRegionSupported(regionCode)) {
Expand Down Expand Up @@ -782,6 +786,10 @@ export const bityRampPlugin = (pluginConfig: RampPluginConfig): RampPlugin => {
return []
}
}

if (maxAmountLimit != null && gt(amount, maxAmountLimit)) {
amount = maxAmountLimit
}
} else {
amount = toFixed(exchangeAmount, amountPrecision)
}
Expand Down
16 changes: 14 additions & 2 deletions src/plugins/ramps/moonpay/moonpayRampPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,10 +517,14 @@ export const moonpayRampPlugin: RampPluginFactory = (
const { direction, regionCode, displayCurrencyCode, tokenId } = request
const fiatCurrencyCode = ensureIsoPrefix(request.fiatCurrencyCode)

const isMaxAmount =
'max' in request.exchangeAmount && request.exchangeAmount.max
const isMaxAmount = 'max' in request.exchangeAmount
const exchangeAmountString =
'amount' in request.exchangeAmount ? request.exchangeAmount.amount : ''
const maxAmountLimitString =
'max' in request.exchangeAmount &&
typeof request.exchangeAmount.max === 'string'
? request.exchangeAmount.max
: undefined

// Fetch provider configuration (with caching)
const config = await fetchProviderConfig()
Expand Down Expand Up @@ -648,9 +652,17 @@ export const moonpayRampPlugin: RampPluginFactory = (
Infinity
}

const maxAmountLimit =
maxAmountLimitString != null
? parseFloat(maxAmountLimitString)
: undefined

let exchangeAmount: number
if (isMaxAmount) {
exchangeAmount = request.amountType === 'fiat' ? maxFiat : maxCrypto
if (maxAmountLimit != null && isFinite(maxAmountLimit)) {
exchangeAmount = Math.min(exchangeAmount, maxAmountLimit)
}
} else {
exchangeAmount = parseFloat(exchangeAmountString)
}
Expand Down
18 changes: 16 additions & 2 deletions src/plugins/ramps/paybis/paybisRampPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,10 +747,14 @@ export const paybisRampPlugin: RampPluginFactory = (
} = request
const currencyPluginId = request.wallet.currencyInfo.pluginId

const isMaxAmount =
'max' in request.exchangeAmount && request.exchangeAmount.max
const isMaxAmount = 'max' in request.exchangeAmount
const exchangeAmount =
'amount' in request.exchangeAmount ? request.exchangeAmount.amount : ''
const maxAmountLimit =
'max' in request.exchangeAmount &&
typeof request.exchangeAmount.max === 'string'
? request.exchangeAmount.max
: undefined

// Validate region restrictions
if (regionCode != null) {
Expand Down Expand Up @@ -838,6 +842,16 @@ export const paybisRampPlugin: RampPluginFactory = (
if (isMaxAmount) {
// Use default max amounts
amount = amountType === 'fiat' ? '10000' : '10'

if (maxAmountLimit != null) {
const maxCapNumber = parseFloat(maxAmountLimit)
const amountNumber = parseFloat(amount)
if (!Number.isNaN(maxCapNumber) && !Number.isNaN(amountNumber)) {
if (amountNumber > maxCapNumber) {
amount = maxAmountLimit
}
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: Crypto Limits Applied Incorrectly to Fiat

The maxAmountLimit, which represents a crypto amount, is incorrectly applied to fiat amounts within the fetchQuotes function for both Moonpay and Paybis. This causes fiat transactions to be limited by crypto maximums, even when the amountType is 'fiat'.

Additional Locations (1)

Fix in Cursor Fix in Web

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 is a corner-cut. We're assuming this API is only used for sell quotes for now (which it is). Maybe some TODO comments are warranted, but time is the constaint.

} else {
amount = exchangeAmount
}
Expand Down
9 changes: 8 additions & 1 deletion src/plugins/ramps/rampPluginTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ export interface RampSupportResult {

export type RampExchangeAmount =
| {
max: true
/**
* Requests a quote for the maximum amount that the provider supports.
* If a string amount is provided (in units of the amountType), then
* the quote amount must not exceed this amount.
* If `true` is provided then the maximum is the amount that the provider
* supports.
* */
max: string | true
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to add a comment explaining the difference between the two.

}
| { amount: string }

Expand Down
Loading
Loading