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
Original file line number Diff line number Diff line change
Expand Up @@ -742,15 +742,6 @@ exports[`TransactionListTop should render (with ENABLE_VISA_PROGRAM) 1`] = `
>
<ActivityIndicator
color="#00f1a2"
style={
{
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "center",
"minWidth": 67,
"paddingRight": 22,
}
}
/>
</View>
</View>
Expand Down Expand Up @@ -1805,15 +1796,6 @@ exports[`TransactionListTop should render 1`] = `
>
<ActivityIndicator
color="#00f1a2"
style={
{
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "center",
"minWidth": 67,
"paddingRight": 22,
}
}
/>
</View>
</View>
Expand Down
108 changes: 52 additions & 56 deletions src/components/charts/SwipeChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CursorProps, GradientProps, SlideAreaChart, ToolTipProps, ToolTipTextRenderersInput, YAxisProps } from '@connectedcars/react-native-slide-charts'
import { asArray, asEither, asNumber, asObject, asString, asTuple } from 'cleaners'
import { asArray, asMaybe, asNumber, asObject, asString, asTuple } from 'cleaners'
import * as React from 'react'
import { Dimensions, LayoutChangeEvent, Platform, View } from 'react-native'
import { cacheStyles } from 'react-native-patina'
Expand All @@ -14,9 +14,9 @@ import { formatFiatString } from '../../hooks/useFiatText'
import { useHandler } from '../../hooks/useHandler'
import { formatDate } from '../../locales/intl'
import { lstrings } from '../../locales/strings'
import { snooze } from '../../util/utils'
import { MinimalButton } from '../buttons/MinimalButton'
import { FillLoader } from '../progress-indicators/FillLoader'
import { showWarning } from '../services/AirshipInstance'
import { Theme, useTheme } from '../services/ThemeContext'
import { ReText } from '../text/ReText'
import { EdgeText } from '../themed/EdgeText'
Expand Down Expand Up @@ -53,8 +53,6 @@ const asCoinGeckoMarketChartRange = asObject<CoinGeckoMarketChartRange>({
total_volumes: asArray(asCoinGeckoDataPair)
})

const asCoinGeckoMarketApi = asEither(asCoinGeckoMarketChartRange, asCoinGeckoError)

const COINGECKO_URL = 'https://api.coingecko.com'
const COINGECKO_URL_PRO = 'https://pro-api.coingecko.com'
const MARKET_CHART_ENDPOINT_4S = '/api/v3/coins/%1$s/market_chart/range?vs_currency=%2$s&from=%3$s&to=%4$s'
Expand Down Expand Up @@ -130,7 +128,7 @@ const reduceChartData = (chartData: ChartDataPoint[], timespan: Timespan): Chart
const SwipeChartComponent = (params: Props) => {
const theme = useTheme()
const styles = getStyles(theme)
const { assetId, currencyCode, fiatCurrencyCode } = params
const { assetId, fiatCurrencyCode } = params

// #region Chart setup

Expand Down Expand Up @@ -190,59 +188,57 @@ const SwipeChartComponent = (params: Props) => {
sMinMaxOpacity.value = withDelay(ANIMATION_DURATION.maxMinFadeInDelay, withTiming(1, { duration: ANIMATION_DURATION.maxMinFadeIn }))
}

try {
if (cachedChartData != null) {
// The chart price line animation is slow when transitioning directly
// between datasets.
// Add a delay so the component can get re-mounted with fresh data
// instead.
setTimeout(() => {
setChartData(cachedChartData)
setIsLoading(false)
delayShowMinMaxLabels()
}, 10)
} else {
const unixNow = Math.trunc(new Date().getTime() / 1000)
const fromParam = unixNow - queryFromTimeOffset
const fetchPath = sprintf(MARKET_CHART_ENDPOINT_4S, assetId, fiatCurrencyCode, fromParam, unixNow)
// Start with the free base URL
let fetchUrl = `${COINGECKO_URL}${fetchPath}`
do {
// Construct the dataset query
const response = await fetch(fetchUrl)
const result = await response.json()
const marketChartRange = asCoinGeckoMarketApi(result)
if ('status' in marketChartRange) {
if (marketChartRange.status.error_code === 429) {
// Rate limit error, use our API key as a fallback
if (!fetchUrl.includes('x_cg_pro_api_key')) {
fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}`
continue
}
} else {
throw new Error(JSON.stringify(marketChartRange))
if (cachedChartData != null) {
// The chart price line animation is slow when transitioning directly
// between datasets.
// Add a delay so the component can get re-mounted with fresh data
// instead.
setTimeout(() => {
setChartData(cachedChartData)
setIsLoading(false)
delayShowMinMaxLabels()
}, 10)
} else {
const unixNow = Math.trunc(new Date().getTime() / 1000)
const fromParam = unixNow - queryFromTimeOffset
const fetchPath = sprintf(MARKET_CHART_ENDPOINT_4S, assetId, fiatCurrencyCode, fromParam, unixNow)
// Start with the free base URL
let fetchUrl = `${COINGECKO_URL}${fetchPath}`
do {
// Construct the dataset query
const response = await fetch(fetchUrl)
const result = await response.json()
const apiError = asMaybe(asCoinGeckoError)(result)
if (apiError != null) {
if (apiError.status.error_code === 429) {
// Rate limit error, use our API key as a fallback
if (!fetchUrl.includes('x_cg_pro_api_key') && ENV.COINGECKO_API_KEY !== '') {
fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}`
}
} else {
const rawChartData = marketChartRange.prices.map(rawDataPoint => {
return {
x: new Date(rawDataPoint[0]),
y: rawDataPoint[1]
}
})
const reducedChartData = reduceChartData(rawChartData, selectedTimespan)

setChartData(reducedChartData)
cachedTimespanChartData.set(selectedTimespan, reducedChartData)
setCachedChartData(cachedTimespanChartData)
setIsLoading(false)
delayShowMinMaxLabels()
break
// Wait 2 second before retrying. It typically takes 1 minute
// before rate limiting is relieved, so even 2 seconds is hasty.
await snooze(2000)
continue
}
throw new Error(`Failed to fetch market data: ${apiError.status.error_code} ${apiError.status.error_message}`)
}

const marketChartRange = asCoinGeckoMarketChartRange(result)
const rawChartData = marketChartRange.prices.map(rawDataPoint => {
return {
x: new Date(rawDataPoint[0]),
y: rawDataPoint[1]
}
} while (true)
}
} catch (e: any) {
showWarning(`Failed to retrieve market data for ${currencyCode}.`)
console.error(JSON.stringify(e))
})
const reducedChartData = reduceChartData(rawChartData, selectedTimespan)

setChartData(reducedChartData)
cachedTimespanChartData.set(selectedTimespan, reducedChartData)
setCachedChartData(cachedTimespanChartData)
setIsLoading(false)
delayShowMinMaxLabels()
break
} while (true)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
4 changes: 2 additions & 2 deletions src/components/scenes/CoinRankingDetailsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const CoinRankingDetailsSceneComponent = (props: Props) => {
*/
const stakingWallets = matchingWallets.filter(wallet => {
return (
isStakingSupported(wallet.currencyInfo.pluginId, currencyCode) &&
isStakingSupported(wallet.currencyInfo.pluginId) &&
walletStakingStateMap[wallet.id] != null &&
filterStakePolicies(
Object.values(walletStakingStateMap[wallet.id].stakePolicies).map(stakePolicy => stakePolicy),
Expand All @@ -201,7 +201,7 @@ const CoinRankingDetailsSceneComponent = (props: Props) => {
if (coinRankingData != null && matchingWallets.length > 0) {
// Start with a looser filter that does not include the stake policy,
// because it has not yet been initialized
const uninitializedStakingWallets = matchingWallets.filter(wallet => isStakingSupported(wallet.currencyInfo.pluginId, currencyCode))
const uninitializedStakingWallets = matchingWallets.filter(wallet => isStakingSupported(wallet.currencyInfo.pluginId))

for (const wallet of uninitializedStakingWallets) {
if (walletStakingStateMap[wallet.id] != null && Object.keys(walletStakingStateMap[wallet.id].stakePolicies).length > 0) continue
Expand Down
46 changes: 18 additions & 28 deletions src/components/themed/TransactionListTop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ export class TransactionListTopComponent extends React.PureComponent<Props, Stat
const styles = getStyles(theme)
const { countryCode } = this.state

const showStaking = this.isStakingAvailable()
const hideStaking = !isStakingSupported(this.props.wallet.currencyInfo.pluginId)
const bestApyText = getBestApyText(Object.values(walletStakingState.stakePolicies))

return (
Expand All @@ -500,32 +500,29 @@ export class TransactionListTopComponent extends React.PureComponent<Props, Stat
<IconButton label={lstrings.fragment_send_subtitle} onPress={this.handleSend}>
<Ionicons name="arrow-up" size={theme.rem(2)} color={theme.primaryText} />
</IconButton>
<IconButton
disabled={this.props.walletStakingState.isLoading}
label={getUkCompliantString(countryCode, 'stake_earn_button_label')}
onPress={this.handleStakePress}
superscriptLabel={bestApyText}
>
{this.props.walletStakingState.isLoading ? (
<ActivityIndicator color={theme.textLink} style={styles.stakingButton} />
) : !showStaking ? null : (
<Feather name="percent" size={theme.rem(1.75)} color={theme.primaryText} />
)}
</IconButton>
{hideStaking ? null : (
<IconButton
disabled={this.props.walletStakingState.isLoading}
label={getUkCompliantString(countryCode, 'stake_earn_button_label')}
onPress={this.handleStakePress}
superscriptLabel={bestApyText}
>
{this.props.walletStakingState.isLoading ? (
<ActivityIndicator color={theme.textLink} />
) : (
<Feather name="percent" size={theme.rem(1.75)} color={theme.primaryText} />
)}
</IconButton>
)}
<IconButton label={lstrings.trade_currency} onPress={this.handleTrade}>
<Ionicons name="swap-horizontal" size={theme.rem(2)} color={theme.primaryText} />
</IconButton>
</View>
)
}

isStakingAvailable = (): boolean => {
const { currencyCode, wallet } = this.props
const { pluginId } = wallet.currencyInfo

const isStakingPolicyAvailable = Object.keys(this.props.walletStakingState.stakePolicies).length > 0

return isStakingPolicyAvailable && isStakingSupported(pluginId, currencyCode)
isStakingPolicyAvailable = (): boolean => {
return Object.keys(this.props.walletStakingState.stakePolicies).length > 0
}

/** Return the best APY found, defaulting to 1 decimal place, rounding to the
Expand Down Expand Up @@ -601,7 +598,7 @@ export class TransactionListTopComponent extends React.PureComponent<Props, Stat

render() {
const { wallet, isEmpty, searching, theme, tokenId, navigation } = this.props
const showStakedBalance = this.isStakingAvailable()
const showStakedBalance = this.isStakingPolicyAvailable()
const styles = getStyles(theme)

return (
Expand Down Expand Up @@ -732,13 +729,6 @@ const getStyles = cacheStyles((theme: Theme) => ({
maxWidth: '70%',
fontSize: theme.rem(1)
},
stakingButton: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minWidth: theme.rem(3),
paddingRight: theme.rem(1)
},

// TODO: Fix SceneHeader to be UI4 compatible
// This negative margin will cause the SceneHeader's divider-line to touch
Expand Down
2 changes: 1 addition & 1 deletion src/envConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const asEnvConfig = asObject({
EDGE_API_KEY: asOptional(asString, ''),
EDGE_API_SECRET: asOptional(asBase16),

COINGECKO_API_KEY: asOptional(asString, 'a0000000000000000000000000000000'),
COINGECKO_API_KEY: asOptional(asString, ''),
IP_API_KEY: asOptional(asString, ''),
SENTRY_DSN_URL: asOptional(asString, 'SENTRY_DSN_URL'),
SENTRY_MAP_UPLOAD_URL: asOptional(asString, 'SENTRY_MAP_UPLOAD_URL'),
Expand Down
6 changes: 2 additions & 4 deletions src/util/stakeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ export const getBestApyText = (stakePolicies?: StakePolicy[]): string | undefine
/**
* Returns true if staking is supported for the given currency code and
* pluginId.
* NOTE: currencyCode is ONLY checked against 'FIO'!
*/
export const isStakingSupported = (pluginId: string, currencyCode: string): boolean => {
// Special case for FIO because it uses it's own staking plugin
return currencyCode.toUpperCase() === 'FIO' || SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true
export const isStakingSupported = (pluginId: string): boolean => {
return SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true
}
Loading