From 2e14d03438bc06a22e962a23b9ca74c590f2a04d Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 1 Mar 2024 17:25:50 -0800 Subject: [PATCH 1/2] Implement CryptoAmount --- src/util/CryptoAmount.ts | 177 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 src/util/CryptoAmount.ts diff --git a/src/util/CryptoAmount.ts b/src/util/CryptoAmount.ts new file mode 100644 index 00000000000..ac53415c9ee --- /dev/null +++ b/src/util/CryptoAmount.ts @@ -0,0 +1,177 @@ +import { div, mul } from 'biggystring' +import { EdgeCurrencyConfig, EdgeDenomination } from 'edge-core-js' + +import { RootState } from '../reducers/RootReducer' +import { convertCurrency } from '../selectors/WalletSelectors' +import { asBiggystring } from './cleaners' +import { DECIMAL_PRECISION, mulToPrecision } from './utils' + +/** + * Defines base properties for the CryptoAmount asset configuration. + */ +interface AssetBaseArgs { + currencyConfig: EdgeCurrencyConfig +} + +/** + * Specifies that one of tokenId or currencyCode must be provided. To describe a + * mainnet/parent/chain currency, an explicit null must be provided to tokenId + * or currencyCode. + */ +type TokenOrCurrencyCodeArgs = + | { + tokenId: string | null + currencyCode?: never + } + | { + tokenId?: never + currencyCode: string + } + +// Specifies that one of exchangeAmount or nativeAmount must be provided. +type ExchangeOrNativeAmountArgs = + | { + exchangeAmount: number | string + nativeAmount?: never + } + | { + exchangeAmount?: never + nativeAmount: string + } + +// Combines both requirements. +type CryptoAmountConstructorArgs = AssetBaseArgs & TokenOrCurrencyCodeArgs & ExchangeOrNativeAmountArgs + +/** + * One-stop shop for any information pertaining to a crypto asset. + * Pass whatever you happen to have, get what you need. + * + * Usage: + * const ethAmountWithExchange = new CryptoAmount({ + * currencyConfig, // 'ethereum' + * tokenId: null, + * exchangeAmount: '100', + * }) + * + * const cryptoAmountWithNative = new CryptoAmount({ + * currencyConfig, + * currencyCode: 'USDC', + * nativeAmount: '12345678', + * }) + * + * cryptoAmount.getDollarValue() + */ +export class CryptoAmount { + public readonly currencyConfig: EdgeCurrencyConfig + public readonly nativeAmount: string + public readonly tokenId: string | null + + /** + * Must construct CryptoAmount with currencyConfig and one of either: tokenId or currencyCode + */ + public constructor(args: CryptoAmountConstructorArgs) { + const { currencyCode, currencyConfig, exchangeAmount, nativeAmount, tokenId } = args + this.currencyConfig = currencyConfig + + // Populate tokenId, derived from currencyCode + if (currencyCode != null) { + // Ensure currencyCode is recognized, if given as a constructor argument. + const foundTokenId = Object.keys(currencyConfig.allTokens).find(edgeToken => currencyConfig.allTokens[edgeToken].currencyCode === currencyCode) + if (foundTokenId == null) { + throw new Error(`CryptoAmount: Could not find tokenId for currencyCode: ${currencyCode}, pluginId: ${currencyConfig.currencyInfo.pluginId}.`) + } else { + this.tokenId = foundTokenId + } + } else { + this.tokenId = tokenId + } + + // Populate nativeAmount + if (exchangeAmount != null) { + try { + asBiggystring(exchangeAmount.toString()) + } catch (e) { + throw new Error(`CryptoAmount: Error instantiating with exchangeAmount: ${String(e)}\n${JSON.stringify(args)}`) + } + + this.nativeAmount = mul(this.getExchangeDenom().multiplier, exchangeAmount.toString()) + if (this.nativeAmount.includes('.')) { + this.nativeAmount = this.nativeAmount.split('.')[0] + } + } else { + this.nativeAmount = nativeAmount + } + } + + // + // Getters: + // + + /** + * Mainnet, parent, network or chain currency code. + */ + get chainCode(): string { + return this.currencyConfig.currencyInfo.currencyCode + } + + /** + * If this CryptoAmount is about a token, the currency code for that token. + * Else, the mainnet, parent, network or chain code. + */ + get currencyCode(): string { + const { currencyCode } = this.tokenId == null ? this.currencyConfig.currencyInfo : this.currencyConfig.allTokens[this.tokenId] + return currencyCode + } + + /** + * The decimal amount you would see on exchanges and pretty much anywhere + * else. Given in maximum decimal precision. + * + * If typical app precision or a specific precision is required, use + * getExchangeAmount() instead. + */ + get exchangeAmount(): string { + const { multiplier } = this.getExchangeDenom() + return div(this.nativeAmount, multiplier, DECIMAL_PRECISION) + } + + /** + * Core pluginId associated with this currency + */ + get pluginId(): string { + return this.currencyConfig.currencyInfo.pluginId + } + + // + // Utilities: + // + + /** + * Automatically uses 2 decimal/cent places if unspecified. + */ + displayDollarValue(state: RootState, precision?: number): string { + return this.displayFiatValue(state, 'iso:USD', precision) + } + + /** + * Automatically uses 2 decimal/cent places if unspecified. + */ + displayFiatValue(state: RootState, isoFiatCode: string, precision?: number) { + return parseFloat(convertCurrency(state, this.currencyCode, isoFiatCode, this.exchangeAmount)).toFixed(precision ?? 2) + } + + /** + * The amount you would see on exchanges and pretty much anywhere else. If + * precision is unset, precision is dynamically set according to the asset's + * multiplier. + */ + getExchangeAmount(precision?: number): string { + const { multiplier } = this.getExchangeDenom() + return div(this.nativeAmount, multiplier, precision ?? mulToPrecision(multiplier)) + } + + getExchangeDenom(): EdgeDenomination { + const { allTokens, currencyInfo } = this.currencyConfig + return this.tokenId == null ? currencyInfo.denominations[0] : allTokens[this.tokenId].denominations[0] + } +} From da4fa36394a3e0aa9f1da5315ab3250f8395f8db Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 1 Mar 2024 18:10:24 -0800 Subject: [PATCH 2/2] Refactor tracking props to be explicit, use CryptoAmount We call everything `pluginId,` making things ambiguous and confusing when we pass `pluginId` around. In this case it was causing problems with the logging when treating a provider's pluginId as a wallet pluginId. Also refactor reported data to be more explicit about what values mean. --- src/actions/CreateWalletActions.tsx | 4 +- src/actions/CryptoExchangeActions.tsx | 16 +- .../FioAddressRegisterSelectWalletScene.tsx | 31 ++-- .../scenes/Fio/FioAddressSettingsScene.tsx | 9 +- .../scenes/Fio/FioCreateHandleScene.tsx | 9 +- .../FioDomainRegisterSelectWalletScene.tsx | 31 ++-- .../scenes/Fio/FioDomainSettingsScene.tsx | 9 +- .../scenes/Fio/FioNameConfirmScene.tsx | 24 ++- .../edgeProvider/EdgeProviderServer.tsx | 15 +- src/plugins/gui/fiatPlugin.tsx | 16 +- src/plugins/gui/fiatPluginTypes.ts | 11 +- src/plugins/gui/providers/banxaProvider.ts | 20 ++- src/plugins/gui/providers/kadoProvider.ts | 20 ++- .../gui/providers/mtpelerinProvider.ts | 20 ++- src/plugins/gui/providers/paybisProvider.ts | 20 ++- src/util/tracking.ts | 142 ++++++++++-------- 16 files changed, 233 insertions(+), 164 deletions(-) diff --git a/src/actions/CreateWalletActions.tsx b/src/actions/CreateWalletActions.tsx index f4959726e81..e23774487e3 100644 --- a/src/actions/CreateWalletActions.tsx +++ b/src/actions/CreateWalletActions.tsx @@ -128,7 +128,7 @@ export function createAccountTransaction( // Hack. Keyboard pops up for some reason. Close it dispatch( logEvent('Activate_Wallet_Cancel', { - currencyCode: createdWalletCurrencyCode + createdWalletCurrencyCode }) ) }, @@ -141,7 +141,7 @@ export function createAccountTransaction( } else if (edgeTransaction) { dispatch( logEvent('Activate_Wallet_Done', { - currencyCode: createdWalletCurrencyCode + createdWalletCurrencyCode }) ) const edgeMetadata: EdgeMetadata = { diff --git a/src/actions/CryptoExchangeActions.tsx b/src/actions/CryptoExchangeActions.tsx index 98221be2de9..3fb2445e9f4 100644 --- a/src/actions/CryptoExchangeActions.tsx +++ b/src/actions/CryptoExchangeActions.tsx @@ -24,6 +24,7 @@ import { convertCurrency } from '../selectors/WalletSelectors' import { RootState, ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' import { GuiCurrencyInfo, GuiSwapInfo } from '../types/types' +import { CryptoAmount } from '../util/CryptoAmount' import { getCurrencyCode, getWalletTokenId } from '../util/CurrencyInfoHelpers' import { logActivity } from '../util/logger' import { bestOfPlugins } from '../util/ReferralHelpers' @@ -338,13 +339,18 @@ export function shiftCryptoCurrency(navigation: NavigationBase, quote: EdgeSwapQ await updateSwapCount(state) - const exchangeAmount = await toWallet.nativeToDenomination(toNativeAmount, toCurrencyCode) dispatch( logEvent('Exchange_Shift_Success', { - pluginId, - currencyCode: toCurrencyCode, - exchangeAmount, - orderId: result.orderId + conversionValues: { + conversionType: 'crypto', + cryptoAmount: new CryptoAmount({ + nativeAmount: toNativeAmount, + currencyCode: toCurrencyCode, + currencyConfig: toWallet.currencyConfig + }), + orderId: result.orderId, + swapProviderId: pluginId + } }) ) } catch (error: any) { diff --git a/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx b/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx index cec7d0bca47..438fc607ad6 100644 --- a/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx +++ b/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx @@ -1,4 +1,3 @@ -import { mul, toFixed } from 'biggystring' import { EdgeAccount, EdgeCurrencyConfig, EdgeCurrencyWallet, EdgeDenomination, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { Alert, Image, View } from 'react-native' @@ -6,12 +5,12 @@ import { sprintf } from 'sprintf-js' import { FIO_STR } from '../../../constants/WalletAndCurrencyConstants' import { lstrings } from '../../../locales/strings' -import { getExchangeDenomByCurrencyCode, selectDisplayDenom } from '../../../selectors/DenominationSelectors' +import { selectDisplayDenom } from '../../../selectors/DenominationSelectors' import { config } from '../../../theme/appConfig' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { EdgeAsset } from '../../../types/types' -import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' +import { CryptoAmount } from '../../../util/CryptoAmount' import { getWalletName } from '../../../util/CurrencyWalletHelpers' import { getRegInfo } from '../../../util/FioAddressUtils' import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' @@ -30,7 +29,6 @@ interface StateProps { fioPlugin?: EdgeCurrencyConfig fioWallets: EdgeCurrencyWallet[] fioDisplayDenomination: EdgeDenomination - pluginId: string isConnected: boolean } @@ -150,12 +148,16 @@ export class FioAddressRegisterSelectWallet extends React.Component ({ diff --git a/src/components/scenes/Fio/FioAddressSettingsScene.tsx b/src/components/scenes/Fio/FioAddressSettingsScene.tsx index 141dd8f038a..5c36294e47e 100644 --- a/src/components/scenes/Fio/FioAddressSettingsScene.tsx +++ b/src/components/scenes/Fio/FioAddressSettingsScene.tsx @@ -6,6 +6,7 @@ import { refreshAllFioAddresses } from '../../../actions/FioAddressActions' import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' +import { CryptoAmount } from '../../../util/CryptoAmount' import { addBundledTxs, getAddBundledTxsFee, getTransferFee } from '../../../util/FioAddressUtils' import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' @@ -104,8 +105,12 @@ export class FioAddressSettingsComponent extends React.Component { diff --git a/src/components/scenes/Fio/FioCreateHandleScene.tsx b/src/components/scenes/Fio/FioCreateHandleScene.tsx index 9616d0c8283..c7a63a25a8e 100644 --- a/src/components/scenes/Fio/FioCreateHandleScene.tsx +++ b/src/components/scenes/Fio/FioCreateHandleScene.tsx @@ -130,7 +130,14 @@ export const FioCreateHandleScene = (props: Props) => { await dispatch(refreshAllFioAddresses()) showToast(lstrings.fio_free_handle_complete) - await dispatch(logEvent('Fio_Handle_Register', { dollarValue: 3 })) + await dispatch( + logEvent('Fio_Handle_Register', { + conversionValues: { + conversionType: 'dollar', + dollarConversionValue: 3 + } + }) + ) navigation.pop() } catch (e: any) { diff --git a/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx b/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx index 123593c3cbd..84f24cb14bd 100644 --- a/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx +++ b/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx @@ -1,4 +1,3 @@ -import { mul, toFixed } from 'biggystring' import { EdgeAccount, EdgeCurrencyConfig, EdgeCurrencyWallet, EdgeDenomination, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { View } from 'react-native' @@ -7,12 +6,12 @@ import { sprintf } from 'sprintf-js' import { FIO_STR } from '../../../constants/WalletAndCurrencyConstants' import { lstrings } from '../../../locales/strings' -import { getExchangeDenomByCurrencyCode, selectDisplayDenomByCurrencyCode } from '../../../selectors/DenominationSelectors' +import { selectDisplayDenomByCurrencyCode } from '../../../selectors/DenominationSelectors' import { config } from '../../../theme/appConfig' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { EdgeAsset } from '../../../types/types' -import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' +import { CryptoAmount } from '../../../util/CryptoAmount' import { getWalletName } from '../../../util/CurrencyWalletHelpers' import { getDomainRegInfo } from '../../../util/FioAddressUtils' import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' @@ -33,7 +32,6 @@ interface StateProps { fioPlugin?: EdgeCurrencyConfig fioWallets: EdgeCurrencyWallet[] fioDisplayDenomination: EdgeDenomination - pluginId: string isConnected: boolean } @@ -121,7 +119,7 @@ class FioDomainRegisterSelectWallet extends React.PureComponent { - const { account, isConnected, navigation, route, onLogEvent: logEvent } = this.props + const { account, isConnected, navigation, route, onLogEvent } = this.props const { fioDomain, selectedWallet } = route.params const { feeValue, paymentInfo: allPaymentInfo, paymentWallet } = this.state @@ -143,11 +141,16 @@ class FioDomainRegisterSelectWallet extends React.PureComponent )).catch(err => showError(err)) - logEvent('Fio_Domain_Register', { exchangeAmount: String(feeValue), currencyCode: paymentWallet.currencyCode }) + onLogEvent('Fio_Domain_Register', { + conversionValues: { + conversionType: 'crypto', + cryptoAmount + } + }) navigation.navigate('homeTab', { screen: 'home' }) } } @@ -276,7 +284,6 @@ export const FioDomainRegisterSelectWalletScene = connect ({ diff --git a/src/components/scenes/Fio/FioDomainSettingsScene.tsx b/src/components/scenes/Fio/FioDomainSettingsScene.tsx index f26d32f2709..abde7e9c917 100644 --- a/src/components/scenes/Fio/FioDomainSettingsScene.tsx +++ b/src/components/scenes/Fio/FioDomainSettingsScene.tsx @@ -8,6 +8,7 @@ import { formatDate } from '../../../locales/intl' import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' +import { CryptoAmount } from '../../../util/CryptoAmount' import { getDomainSetVisibilityFee, getRenewalFee, getTransferFee, renewFioDomain, setDomainVisibility } from '../../../util/FioAddressUtils' import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' @@ -118,11 +119,11 @@ export class FioDomainSettingsComponent extends React.Component { await renewFioDomain(fioWallet, fioDomainName, renewalFee) - const { currencyCode, pluginId } = fioWallet.currencyInfo onLogEvent('Fio_Domain_Renew', { - nativeAmount: String(renewalFee), - currencyCode, - pluginId + conversionValues: { + conversionType: 'crypto', + cryptoAmount: new CryptoAmount({ nativeAmount: String(renewalFee), currencyConfig: fioWallet.currencyConfig, tokenId: null }) + } }) } diff --git a/src/components/scenes/Fio/FioNameConfirmScene.tsx b/src/components/scenes/Fio/FioNameConfirmScene.tsx index c9552853243..93a73c29a25 100644 --- a/src/components/scenes/Fio/FioNameConfirmScene.tsx +++ b/src/components/scenes/Fio/FioNameConfirmScene.tsx @@ -6,6 +6,7 @@ import { FIO_ADDRESS_DELIMITER } from '../../../constants/WalletAndCurrencyConst import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' +import { CryptoAmount } from '../../../util/CryptoAmount' import { fioMakeSpend, fioSignAndBroadcast } from '../../../util/FioAddressUtils' import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' @@ -111,16 +112,20 @@ class FioNameConfirm extends React.PureComponent { } } else { try { - const { currencyCode, pluginId } = paymentWallet.currencyInfo if (this.isFioAddress()) { let edgeTx = await fioMakeSpend(paymentWallet, 'registerFioAddress', { fioAddress: fioName }) edgeTx = await fioSignAndBroadcast(paymentWallet, edgeTx) await paymentWallet.saveTx(edgeTx) onLogEvent('Fio_Handle_Register', { - nativeAmount: edgeTx.nativeAmount, - currencyCode, - pluginId + conversionValues: { + conversionType: 'crypto', + cryptoAmount: new CryptoAmount({ + currencyConfig: paymentWallet.currencyConfig, + nativeAmount: edgeTx.nativeAmount, + tokenId: null + }) + } }) // @ts-expect-error @@ -136,9 +141,14 @@ class FioNameConfirm extends React.PureComponent { const expiration = edgeTx.otherParams?.broadcastResult?.expiration onLogEvent('Fio_Domain_Register', { - nativeAmount: edgeTx.nativeAmount, - currencyCode, - pluginId + conversionValues: { + conversionType: 'crypto', + cryptoAmount: new CryptoAmount({ + currencyConfig: paymentWallet.currencyConfig, + nativeAmount: edgeTx.nativeAmount, + tokenId: null + }) + } }) // @ts-expect-error diff --git a/src/controllers/edgeProvider/EdgeProviderServer.tsx b/src/controllers/edgeProvider/EdgeProviderServer.tsx index 0590c49a37b..a8f380a741c 100644 --- a/src/controllers/edgeProvider/EdgeProviderServer.tsx +++ b/src/controllers/edgeProvider/EdgeProviderServer.tsx @@ -28,6 +28,7 @@ import { Dispatch } from '../../types/reduxTypes' import { NavigationBase } from '../../types/routerTypes' import { EdgeAsset, MapObject } from '../../types/types' import { getCurrencyIconUris } from '../../util/CdnUris' +import { CryptoAmount } from '../../util/CryptoAmount' import { getTokenIdForced } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { makeCurrencyCodeTable } from '../../util/tokenIdTools' @@ -410,10 +411,16 @@ export class EdgeProviderServer implements EdgeProviderMethods { .then(exchangeAmount => { this._dispatch( logEvent('EdgeProvider_Conversion_Success', { - pluginId: this._plugin.storeId, - orderId, - currencyCode: transaction.currencyCode, - exchangeAmount: abs(exchangeAmount) + conversionValues: { + conversionType: 'crypto', + cryptoAmount: new CryptoAmount({ + currencyConfig: wallet.currencyConfig, + currencyCode: transaction.currencyCode, + exchangeAmount: abs(exchangeAmount) + }), + swapProviderId: this._plugin.storeId, + orderId + } }) ) }) diff --git a/src/plugins/gui/fiatPlugin.tsx b/src/plugins/gui/fiatPlugin.tsx index d65d2742fc1..c0aac9041cc 100644 --- a/src/plugins/gui/fiatPlugin.tsx +++ b/src/plugins/gui/fiatPlugin.tsx @@ -19,7 +19,7 @@ import { HomeAddress, SepaInfo } from '../../types/FormTypes' import { GuiPlugin } from '../../types/GuiPluginTypes' import { AppParamList, NavigationBase } from '../../types/routerTypes' import { getNavigationAbsolutePath } from '../../util/routerUtils' -import { OnLogEvent, TrackingEventName } from '../../util/tracking' +import { OnLogEvent, SellConversionValues, TrackingEventName } from '../../util/tracking' import { FiatPaymentType, FiatPluginAddressFormParams, @@ -222,19 +222,7 @@ export const executePlugin = async (params: { showToast: async (message: string, autoHideMs?: number) => { showToast(message, autoHideMs) }, - trackConversion: async ( - event: TrackingEventName, - opts: { - destCurrencyCode: string - destExchangeAmount: string - destPluginId?: string - sourceCurrencyCode: string - sourceExchangeAmount: string - sourcePluginId?: string - pluginId: string - orderId?: string - } - ) => { + trackConversion: async (event: TrackingEventName, opts: { conversionValues: SellConversionValues }) => { onLogEvent(event, opts) }, exitScene: async () => { diff --git a/src/plugins/gui/fiatPluginTypes.ts b/src/plugins/gui/fiatPluginTypes.ts index 3257efd9240..dfed5ec2a4d 100644 --- a/src/plugins/gui/fiatPluginTypes.ts +++ b/src/plugins/gui/fiatPluginTypes.ts @@ -11,7 +11,7 @@ import { HomeAddress, SepaInfo } from '../../types/FormTypes' import { GuiPlugin } from '../../types/GuiPluginTypes' import { AppParamList } from '../../types/routerTypes' import { EdgeAsset } from '../../types/types' -import { TrackingEventName } from '../../util/tracking' +import { SellConversionValues, TrackingEventName } from '../../util/tracking' import { FiatPluginOpenWebViewParams } from './scenes/FiatPluginWebView' import { RewardsCardDashboardParams } from './scenes/RewardsCardDashboardScene' import { RewardsCardWelcomeParams } from './scenes/RewardsCardWelcomeScene' @@ -146,14 +146,7 @@ export interface FiatPluginUi { trackConversion: ( event: TrackingEventName, opts: { - destCurrencyCode: string - destExchangeAmount: string - destPluginId?: string - sourceCurrencyCode: string - sourceExchangeAmount: string - sourcePluginId?: string - pluginId: string - orderId?: string + conversionValues: SellConversionValues } ) => Promise diff --git a/src/plugins/gui/providers/banxaProvider.ts b/src/plugins/gui/providers/banxaProvider.ts index 10d9fefdd4c..cd3dbab8f64 100644 --- a/src/plugins/gui/providers/banxaProvider.ts +++ b/src/plugins/gui/providers/banxaProvider.ts @@ -7,6 +7,7 @@ import URL from 'url-parse' import { SendScene2Params } from '../../../components/scenes/SendScene2' import { lstrings } from '../../../locales/strings' import { StringMap } from '../../../types/types' +import { CryptoAmount } from '../../../util/CryptoAmount' import { fetchInfo } from '../../../util/network' import { consify, makeUuid } from '../../../util/utils' import { SendErrorBackPressed, SendErrorNoTransaction } from '../fiatPlugin' @@ -645,13 +646,18 @@ export const banxaProvider: FiatProviderFactory = { interval = undefined await showUi.trackConversion('Sell_Success', { - destCurrencyCode: fiatCurrencyCode, - destExchangeAmount: priceQuote.fiat_amount, - sourceCurrencyCode: displayCurrencyCode, - sourceExchangeAmount: coinAmount.toString(), - sourcePluginId: coreWallet.currencyInfo.pluginId, - pluginId: providerId, - orderId: id + conversionValues: { + conversionType: 'sell', + destFiatCurrencyCode: fiatCurrencyCode, + destFiatAmount: priceQuote.fiat_amount, + sourceAmount: new CryptoAmount({ + currencyConfig: coreWallet.currencyConfig, + currencyCode: displayCurrencyCode, + exchangeAmount: coinAmount + }), + fiatProviderId: providerId, + orderId: id + } }) // Below is an optional step diff --git a/src/plugins/gui/providers/kadoProvider.ts b/src/plugins/gui/providers/kadoProvider.ts index f6c2358a4f6..ed2f162e87d 100644 --- a/src/plugins/gui/providers/kadoProvider.ts +++ b/src/plugins/gui/providers/kadoProvider.ts @@ -6,6 +6,7 @@ import URL from 'url-parse' import { SendScene2Params } from '../../../components/scenes/SendScene2' import { ENV } from '../../../env' import { lstrings } from '../../../locales/strings' +import { CryptoAmount } from '../../../util/CryptoAmount' import { isHex } from '../../../util/utils' import { SendErrorBackPressed, SendErrorNoTransaction } from '../fiatPlugin' import { FiatDirection, FiatPaymentType, SaveTxActionParams } from '../fiatPluginTypes' @@ -763,13 +764,18 @@ export const kadoProvider: FiatProviderFactory = { } const tx = await showUi.send(sendParams) await showUi.trackConversion('Sell_Success', { - destCurrencyCode: 'USD', - destExchangeAmount: fiatAmount, - sourceCurrencyCode: displayCurrencyCode, - sourceExchangeAmount: paymentExchangeAmount, - sourcePluginId: coreWallet.currencyInfo.pluginId, - pluginId: providerId, - orderId + conversionValues: { + conversionType: 'sell', + destFiatCurrencyCode: 'USD', + destFiatAmount: fiatAmount, + sourceAmount: new CryptoAmount({ + currencyConfig: coreWallet.currencyConfig, + currencyCode: displayCurrencyCode, + exchangeAmount: paymentExchangeAmount + }), + fiatProviderId: providerId, + orderId + } }) // Save separate metadata/action for token transaction fee diff --git a/src/plugins/gui/providers/mtpelerinProvider.ts b/src/plugins/gui/providers/mtpelerinProvider.ts index 03e3d8fca2f..95ecafae6f7 100644 --- a/src/plugins/gui/providers/mtpelerinProvider.ts +++ b/src/plugins/gui/providers/mtpelerinProvider.ts @@ -6,6 +6,7 @@ import { toUtf8Bytes } from 'ethers/lib/utils' import { SendScene2Params } from '../../../components/scenes/SendScene2' import { showError } from '../../../components/services/AirshipInstance' import { ENV } from '../../../env' +import { CryptoAmount } from '../../../util/CryptoAmount' import { hexToDecimal } from '../../../util/utils' import { FiatDirection, FiatPaymentType, SaveTxActionParams } from '../fiatPluginTypes' import { @@ -557,13 +558,18 @@ export const mtpelerinProvider: FiatProviderFactory = { } const tx = await showUi.send(sendParams) await showUi.trackConversion('Sell_Success', { - destCurrencyCode: fiatCurrencyCode, - destExchangeAmount: fiatAmount, - sourceCurrencyCode: displayCurrencyCode, - sourceExchangeAmount: exchangeAmount, - sourcePluginId: pluginId, - pluginId: providerId, - orderId + conversionValues: { + conversionType: 'sell', + destFiatCurrencyCode: fiatCurrencyCode, + destFiatAmount: fiatAmount, + sourceAmount: new CryptoAmount({ + currencyConfig: coreWallet.currencyConfig, + currencyCode: displayCurrencyCode, + exchangeAmount: exchangeAmount + }), + fiatProviderId: providerId, + orderId + } }) // Save separate metadata/action for token transaction fee diff --git a/src/plugins/gui/providers/paybisProvider.ts b/src/plugins/gui/providers/paybisProvider.ts index b7ab1588a85..c3e317ec1c3 100644 --- a/src/plugins/gui/providers/paybisProvider.ts +++ b/src/plugins/gui/providers/paybisProvider.ts @@ -7,6 +7,7 @@ import { SendScene2Params } from '../../../components/scenes/SendScene2' import { locale } from '../../../locales/intl' import { lstrings } from '../../../locales/strings' import { EdgeAsset, StringMap } from '../../../types/types' +import { CryptoAmount } from '../../../util/CryptoAmount' import { makeUuid } from '../../../util/utils' import { SendErrorNoTransaction } from '../fiatPlugin' import { FiatDirection, FiatPaymentType, SaveTxActionParams } from '../fiatPluginTypes' @@ -609,13 +610,18 @@ export const paybisProvider: FiatProviderFactory = { } const tx = await showUi.send(sendParams) await showUi.trackConversion('Sell_Success', { - destCurrencyCode: fiatCurrencyCode, - destExchangeAmount: fiatAmount, - sourceCurrencyCode: displayCurrencyCode, - sourceExchangeAmount: amount, - sourcePluginId: coreWallet.currencyInfo.pluginId, - pluginId: providerId, - orderId: invoice + conversionValues: { + conversionType: 'sell', + destFiatCurrencyCode: fiatCurrencyCode, + destFiatAmount: fiatAmount, + sourceAmount: new CryptoAmount({ + currencyConfig: coreWallet.currencyConfig, + currencyCode: displayCurrencyCode, + exchangeAmount: amount + }), + fiatProviderId: providerId, + orderId: invoice + } }) // Save separate metadata/action for token transaction fee diff --git a/src/util/tracking.ts b/src/util/tracking.ts index 7295c55b0a6..65362d25889 100644 --- a/src/util/tracking.ts +++ b/src/util/tracking.ts @@ -1,6 +1,5 @@ import Bugsnag from '@bugsnag/react-native' import analytics from '@react-native-firebase/analytics' -import { div } from 'biggystring' import { TrackingEventName as LoginTrackingEventName, TrackingValues as LoginTrackingValues } from 'edge-login-ui-rn/lib/components/publicApi/publicTypes' import PostHog from 'posthog-react-native' import { getBuildNumber, getUniqueId, getVersion } from 'react-native-device-info' @@ -9,13 +8,11 @@ import { checkNotifications } from 'react-native-permissions' import { getFirstOpenInfo } from '../actions/FirstOpenActions' import { ENV } from '../env' import { ExperimentConfig, getExperimentConfig } from '../experimentConfig' -import { getExchangeDenomByCurrencyCode } from '../selectors/DenominationSelectors' -import { convertCurrency } from '../selectors/WalletSelectors' import { ThunkAction } from '../types/reduxTypes' -import { asBiggystring } from './cleaners' +import { CryptoAmount } from './CryptoAmount' import { fetchReferral } from './network' import { makeErrorLog } from './translateError' -import { consify, mulToPrecision } from './utils' +import { consify } from './utils' export type TrackingEventName = | 'Activate_Wallet_Cancel' | 'Activate_Wallet_Done' @@ -59,22 +56,55 @@ export type TrackingEventName = export type OnLogEvent = (event: TrackingEventName, values?: TrackingValues) => void +/** + * Analytics: Known dollar amount revenue + */ +export interface DollarConversionValues { + conversionType: 'dollar' + dollarConversionValue: number +} + +/** + * Analytics: Some unknown revenue based on a send (e.g. FIO handle/domain fees) + * or swap + */ +export interface CryptoConversionValues { + conversionType: 'crypto' + cryptoAmount: CryptoAmount + + swapProviderId?: string + orderId?: string +} + +/** + * Analytics: Sell to fiat + */ +export interface SellConversionValues { + conversionType: 'sell' + + // The quoted fiat amounts resulting from this sale + destFiatAmount: string + destFiatCurrencyCode: string + + sourceAmount: CryptoAmount + + fiatProviderId: string // Fiat provider that provided the conversion + orderId?: string // Unique order identifier provided by fiat provider +} + +/** + * Culmination of defined tracking value types, including those defined in + * LoginUi. + */ export interface TrackingValues extends LoginTrackingValues { - currencyCode?: string // Wallet currency code - dollarValue?: number // Conversion amount, in USD error?: unknown | string // Any error - orderId?: string // Unique order identifier provided by plugin - pluginId?: string // Plugin that provided the conversion + + createdWalletCurrencyCode?: string numSelectedWallets?: number // Number of wallets to be created - destCurrencyCode?: string - destExchangeAmount?: string - destPluginId?: string // currency pluginId of source asset - sourceCurrencyCode?: string - sourceExchangeAmount?: string - sourcePluginId?: string // currency pluginId of dest asset numAccounts?: number // Number of full accounts saved on the device - exchangeAmount?: string - nativeAmount?: string + + // Conversion values + conversionValues?: DollarConversionValues | CryptoConversionValues | SellConversionValues } // Set up the global Firebase analytics instance at boot: @@ -141,7 +171,7 @@ export function trackError( */ export function logEvent(event: TrackingEventName, values: TrackingValues = {}): ThunkAction { return async (dispatch, getState) => { - const { currencyCode, dollarValue, pluginId, error, exchangeAmount, nativeAmount, sourceExchangeAmount, destExchangeAmount } = values + const { error, conversionValues, createdWalletCurrencyCode } = values getExperimentConfig() .then(async (experimentConfig: ExperimentConfig) => { // Persistent & Unchanged params: @@ -151,7 +181,6 @@ export function logEvent(event: TrackingEventName, values: TrackingValues = {}): // Populate referral params: const state = getState() - const { account } = state.core const { accountReferral } = state.account params.refDeviceInstallerId = state.deviceReferral.installerId params.refDeviceCurrencyCodes = state.deviceReferral.currencyCodes @@ -162,52 +191,39 @@ export function logEvent(event: TrackingEventName, values: TrackingValues = {}): params.refAccountCurrencyCodes = accountReferral.currencyCodes // Adjust params: - if (currencyCode != null) params.currency = currencyCode - if (dollarValue != null) { - // If an explicit dollarValue was given, prioritize it - params.currency = 'USD' - params.value = Number(dollarValue.toFixed(2)) - params.items = [String(event)] - } else if (currencyCode != null) { - // Else, calculate the dollar value from crypto amounts, if required props given - if (nativeAmount != null && pluginId != null) { - try { - asBiggystring(nativeAmount) - } catch (e) { - trackError('Error in tracking nativeAmount: ' + JSON.stringify({ event, values })) - } - const { multiplier } = getExchangeDenomByCurrencyCode(account.currencyConfig[pluginId], currencyCode) - params.value = div(nativeAmount, multiplier, mulToPrecision(multiplier)) - } else if (exchangeAmount != null) { - params.value = parseFloat( - convertCurrency(state, currencyCode, 'iso:USD', typeof destExchangeAmount === 'string' ? destExchangeAmount : String(destExchangeAmount)) - ) - } else if (sourceExchangeAmount != null) { - try { - asBiggystring(sourceExchangeAmount) - } catch (e) { - trackError('Error in tracking sourceExchangeAmount: ' + JSON.stringify({ event, values })) - } - params.sourceExchangeAmount = sourceExchangeAmount - } else if (destExchangeAmount != null) { - try { - asBiggystring(destExchangeAmount) - } catch (e) { - trackError('Error in tracking destExchangeAmount: ' + JSON.stringify({ event, values })) - } - params.destExchangeAmount = destExchangeAmount - try { - asBiggystring(exchangeAmount) - } catch (e) { - trackError('Error in tracking exchangeAmount: ' + JSON.stringify({ event, values })) - } - params.value = convertCurrency(state, currencyCode, 'iso:USD', exchangeAmount) - } else { - console.warn('Unable to calculate dollar value for event:', event, values) + if (createdWalletCurrencyCode != null) params.currency = createdWalletCurrencyCode + if (error != null) params.error = makeErrorLog(error) + + // Conversion values: + if (conversionValues != null) { + const { conversionType } = conversionValues + if (conversionType === 'dollar') { + params.currency = 'USD' + params.dollarConverisonValue = Number(conversionValues.dollarConversionValue.toFixed(2)) + } else if (conversionType === 'sell') { + const { sourceAmount, destFiatAmount, destFiatCurrencyCode, orderId, fiatProviderId } = conversionValues + + params.sourceDollarValue = Number(sourceAmount.displayDollarValue(state)) + params.sourceCryptoAmount = Number(sourceAmount.exchangeAmount) + params.sourceCurrencyCode = sourceAmount.currencyCode + + params.destFiatValue = Number(destFiatAmount).toFixed(2) + params.destFiatCurrencyCode = destFiatCurrencyCode + + if (orderId != null) params.orderId = orderId + if (fiatProviderId != null) params.fiatProviderId = fiatProviderId + } else if (conversionType === 'crypto') { + const { cryptoAmount, swapProviderId, orderId } = conversionValues + + params.cryptoAmount = Number(cryptoAmount.exchangeAmount) + params.currency = cryptoAmount.currencyCode + + params.dollarValue = Number(cryptoAmount.displayDollarValue(state)) + + if (orderId != null) params.orderId = orderId + if (swapProviderId != null) params.swapProviderId = swapProviderId } } - if (pluginId != null) params.plugin = pluginId - if (error != null) params.error = makeErrorLog(error) // Add all 'sticky' remote config variant values: for (const key of Object.keys(experimentConfig)) params[`svar_${key}`] = experimentConfig[key as keyof ExperimentConfig]