diff --git a/src/shared/analytics/analytics.background.ts b/src/shared/analytics/analytics.background.ts index c5a358e7e..6f20a75e4 100644 --- a/src/shared/analytics/analytics.background.ts +++ b/src/shared/analytics/analytics.background.ts @@ -28,7 +28,7 @@ import { getProviderForMetabase, getProviderNameFromGroup, } from './shared/getProviderNameFromGroup'; -import { addressActionToAnalytics } from './shared/addressActionToAnalytics'; +import { transactionContextToAnalytics } from './shared/transactionContextToAnalytics'; import { mixpanelTrack, mixpanelIdentify, mixpanelReset } from './mixpanel'; import { getChainBreakdown, @@ -135,60 +135,56 @@ function trackAppEvents({ account }: { account: Account }) { sendToMetabase('client_error', params); }); - emitter.on( - 'transactionSent', - async ({ + emitter.on('transactionSent', async (contextParams) => { + const { transaction, initiator, feeValueCommon, - addressAction, quote, clientScope, chain, - }) => { - const initiatorURL = new URL(initiator); - const { origin, pathname } = initiatorURL; - const isInternalOrigin = globalThis.location.origin === origin; - const initiatorName = isInternalOrigin ? 'Extension' : 'External Dapp'; - const addressActionAnalytics = addressActionToAnalytics({ - addressAction, - quote, - }); - const preferences = await account - .getCurrentWallet() - .getPreferences({ context: INTERNAL_SYMBOL_CONTEXT }); + } = contextParams; - const params = createParams({ - request_name: 'signed_transaction', - screen_name: origin === initiator ? 'Transaction Request' : pathname, - wallet_address: transaction.from, - /* @deprecated */ - context: initiatorName, - /* @deprecated */ - type: 'Sign', - client_scope: clientScope ?? initiatorName, - action_type: addressActionAnalytics?.action_type ?? 'Execute', - dapp_domain: isInternalOrigin ? null : origin, - chain, - gas: transaction.gasLimit.toString(), - hash: transaction.hash, - asset_amount_sent: [], // TODO - gas_price: null, // TODO - network_fee: null, // TODO - network_fee_value: feeValueCommon, - contract_type: quote?.contract_metadata?.name ?? null, - hold_sign_button: Boolean(preferences.enableHoldToSignButton), - ...addressActionAnalytics, - }); - sendToMetabase('signed_transaction', params); - const mixpanelParams = omit(params, [ - 'request_name', - 'hash', - 'wallet_address', - ]); - mixpanelTrack(account, 'Transaction: Signed Transaction', mixpanelParams); - } - ); + const initiatorURL = new URL(initiator); + const { origin, pathname } = initiatorURL; + const isInternalOrigin = globalThis.location.origin === origin; + const initiatorName = isInternalOrigin ? 'Extension' : 'External Dapp'; + + const analyticsData = transactionContextToAnalytics(contextParams); + const preferences = await account + .getCurrentWallet() + .getPreferences({ context: INTERNAL_SYMBOL_CONTEXT }); + + const params = createParams({ + request_name: 'signed_transaction', + screen_name: origin === initiator ? 'Transaction Request' : pathname, + wallet_address: transaction.from, + /* @deprecated */ + context: initiatorName, + /* @deprecated */ + type: 'Sign', + client_scope: clientScope ?? initiatorName, + action_type: analyticsData?.action_type ?? 'Execute', + dapp_domain: isInternalOrigin ? null : origin, + chain, + gas: transaction.gasLimit.toString(), + hash: transaction.hash, + asset_amount_sent: [], // TODO + gas_price: null, // TODO + network_fee: null, // TODO + network_fee_value: feeValueCommon, + contract_type: quote?.contract_metadata?.name ?? null, + hold_sign_button: Boolean(preferences.enableHoldToSignButton), + ...analyticsData, + }); + sendToMetabase('signed_transaction', params); + const mixpanelParams = omit(params, [ + 'request_name', + 'hash', + 'wallet_address', + ]); + mixpanelTrack(account, 'Transaction: Signed Transaction', mixpanelParams); + }); async function handleSign({ type, diff --git a/src/shared/analytics/shared/addressActionToAnalytics.ts b/src/shared/analytics/shared/transactionContextToAnalytics.ts similarity index 89% rename from src/shared/analytics/shared/addressActionToAnalytics.ts rename to src/shared/analytics/shared/transactionContextToAnalytics.ts index e064cc0ec..38e023935 100644 --- a/src/shared/analytics/shared/addressActionToAnalytics.ts +++ b/src/shared/analytics/shared/transactionContextToAnalytics.ts @@ -1,11 +1,10 @@ import type { ActionAsset } from 'defi-sdk'; import { isTruthy } from 'is-truthy-ts'; import { getFungibleAsset } from 'src/modules/ethereum/transactions/actionAsset'; -import type { AnyAddressAction } from 'src/modules/ethereum/transactions/addressAction'; import type { Chain } from 'src/modules/networks/Chain'; import { createChain } from 'src/modules/networks/Chain'; import { getDecimals } from 'src/modules/networks/asset'; -import type { Quote } from 'src/shared/types/Quote'; +import type { TransactionContextParams } from 'src/shared/types/SignatureContextParams'; import { baseToCommon } from 'src/shared/units/convert'; interface AnalyticsTransactionData { @@ -21,6 +20,8 @@ interface AnalyticsTransactionData { zerion_fee_percentage?: number; zerion_fee_usd_amount?: number; output_chain?: string; + warning_was_shown: TransactionContextParams['warningWasShown']; + output_amount_color: TransactionContextParams['outputAmountColor']; } interface AssetQuantity { @@ -79,13 +80,12 @@ function toMaybeArr( return arr?.length ? arr.filter(isTruthy) : undefined; } -export function addressActionToAnalytics({ +export function transactionContextToAnalytics({ addressAction, quote, -}: { - addressAction: AnyAddressAction | null; - quote?: Quote; -}): AnalyticsTransactionData | null { + warningWasShown, + outputAmountColor, +}: TransactionContextParams): AnalyticsTransactionData | null { if (!addressAction) { return null; } @@ -106,7 +106,10 @@ export function addressActionToAnalytics({ asset_name_received: toMaybeArr(incoming?.map(getAssetName)), asset_address_sent: toMaybeArr(outgoing?.map(getAssetAddress)), asset_address_received: toMaybeArr(incoming?.map(getAssetAddress)), + warning_was_shown: warningWasShown, + output_amount_color: outputAmountColor, }; + if (quote) { const zerion_fee_percentage = quote.protocol_fee; const feeAmount = quote.protocol_fee_amount; diff --git a/src/shared/types/SignatureContextParams.ts b/src/shared/types/SignatureContextParams.ts index cd52dae91..adc10b88c 100644 --- a/src/shared/types/SignatureContextParams.ts +++ b/src/shared/types/SignatureContextParams.ts @@ -16,6 +16,8 @@ export interface TransactionContextParams { clientScope: ClientScope | null; addressAction: AnyAddressAction | null; quote?: Quote; + warningWasShown?: boolean; + outputAmountColor?: 'grey' | 'red'; } export interface MessageContextParams { diff --git a/src/ui/components/FiatInputValue/FiatInputValue.tsx b/src/ui/components/FiatInputValue/FiatInputValue.tsx index 6c0905cd4..1ad50a0b2 100644 --- a/src/ui/components/FiatInputValue/FiatInputValue.tsx +++ b/src/ui/components/FiatInputValue/FiatInputValue.tsx @@ -7,7 +7,10 @@ import { useCurrency } from 'src/modules/currency/useCurrency'; import type { Asset } from 'defi-sdk'; import { HStack } from 'src/ui/ui-kit/HStack'; import type { PriceImpact } from 'src/ui/pages/SwapForm/shared/price-impact'; -import { getPriceImpactPercentage } from 'src/ui/pages/SwapForm/shared/price-impact'; +import { + getPriceImpactPercentage, + isSignificantValueLoss, +} from 'src/ui/pages/SwapForm/shared/price-impact'; import { formatPercent } from 'src/shared/units/formatPercent'; export function FiatInputValue({ @@ -88,9 +91,9 @@ export function ReceiveFiatInputValue({ }: { priceImpact: PriceImpact | null; } & FieldInputValueProps) { - const isSignificantLoss = - priceImpact?.kind === 'loss' && - (priceImpact.level === 'medium' || priceImpact.level === 'high'); + const isSignificantLoss = Boolean( + priceImpact && isSignificantValueLoss(priceImpact) + ); const priceImpactPercentage = priceImpact ? getPriceImpactPercentage(priceImpact) @@ -104,27 +107,26 @@ export function ReceiveFiatInputValue({ [priceImpactPercentage] ); - const showPercentageChange = + const pecentageChangeVisible = Boolean(percentageChange) && (priceImpact?.kind === 'zero' || priceImpact?.kind === 'loss'); + const color = isSignificantLoss + ? 'var(--negative-500)' + : 'var(--neutral-600)'; + return ( + pecentageChangeVisible && percentageChange ? ( + {`(${percentageChange})`} ) : null } - color={isSignificantLoss ? 'var(--negative-500)' : 'var(--neutral-600)'} + color={color} style={isSignificantLoss ? { cursor: 'help' } : undefined} title={ isSignificantLoss diff --git a/src/ui/pages/BridgeForm/BridgeForm.tsx b/src/ui/pages/BridgeForm/BridgeForm.tsx index 0e3276adc..168535b32 100644 --- a/src/ui/pages/BridgeForm/BridgeForm.tsx +++ b/src/ui/pages/BridgeForm/BridgeForm.tsx @@ -94,6 +94,7 @@ import { TransactionConfiguration } from '../SendTransaction/TransactionConfigur import { ApproveHintLine } from '../SwapForm/ApproveHintLine'; import { txErrorToMessage } from '../SendTransaction/shared/transactionErrorToMessage'; import { getQuotesErrorMessage } from '../SwapForm/Quotes/getQuotesErrorMessage'; +import { calculatePriceImpact } from '../SwapForm/shared/price-impact'; import type { BridgeFormState } from './shared/types'; import { useBridgeTokens } from './useBridgeTokens'; import { getAvailablePositions } from './getAvailablePositions'; @@ -444,6 +445,15 @@ function BridgeFormComponent() { [selectedQuote] ); + const priceImpact = useMemo(() => { + return calculatePriceImpact({ + inputValue: spendInput || null, + outputValue: receiveInput || null, + inputAsset: spendAsset, + outputAsset: receiveAsset, + }); + }, [receiveAsset, receiveInput, spendAsset, spendInput]); + const reverseChains = useCallback( () => setUserFormState((state) => ({ @@ -665,6 +675,11 @@ function BridgeFormComponent() { resetApproveMutation, ]); + const isApproveMode = + approveMutation.isLoading || + (quotesData.done && !enough_allowance) || + approveTxStatus === 'pending'; + const { mutate: sendTransaction, data: transactionHash, @@ -709,6 +724,8 @@ function BridgeFormComponent() { chain: spendChain, }), quote: selectedQuote, + warningWasShown: false, + outputAmountColor: 'grey', }); return txResponse.hash; }, @@ -786,11 +803,6 @@ function BridgeFormComponent() { ); } - const isApproveMode = - approveMutation.isLoading || - (quotesData.done && !enough_allowance) || - approveTxStatus === 'pending'; - const showApproveHintLine = (quotesData.done && !enough_allowance) || !approveMutation.isIdle; @@ -981,6 +993,7 @@ function BridgeFormComponent() { } spendInput={spendInput} spendAsset={spendAsset} + priceImpact={priceImpact} onChangeAmount={(value) => handleChange('receiveInput', value)} onChangeToken={(value) => handleChange('receiveTokenInput', value) diff --git a/src/ui/pages/BridgeForm/fieldsets/ReceiveTokenField/ReceiveTokenField.tsx b/src/ui/pages/BridgeForm/fieldsets/ReceiveTokenField/ReceiveTokenField.tsx index 57512738a..86b5a40a8 100644 --- a/src/ui/pages/BridgeForm/fieldsets/ReceiveTokenField/ReceiveTokenField.tsx +++ b/src/ui/pages/BridgeForm/fieldsets/ReceiveTokenField/ReceiveTokenField.tsx @@ -1,21 +1,21 @@ -import React, { useEffect, useId, useMemo, useRef } from 'react'; -import { getPositionBalance } from 'src/ui/components/Positions/helpers'; +import type { EmptyAddressPosition } from '@zeriontech/transactions'; +import type { AddressPosition, Asset } from 'defi-sdk'; +import React, { useEffect, useId, useRef } from 'react'; +import type { Chain } from 'src/modules/networks/Chain'; import { formatTokenValue, roundTokenValue, } from 'src/shared/units/formatTokenValue'; +import { ReceiveFiatInputValue } from 'src/ui/components/FiatInputValue/FiatInputValue'; +import { getPositionBalance } from 'src/ui/components/Positions/helpers'; +import { MarketAssetSelect } from 'src/ui/pages/SwapForm/fieldsets/ReceiveTokenField/MarketAssetSelect'; +import type { PriceImpact } from 'src/ui/pages/SwapForm/shared/price-impact'; +import { FLOAT_INPUT_PATTERN } from 'src/ui/shared/forms/inputs'; +import { NBSP } from 'src/ui/shared/typography'; +import { FormFieldset } from 'src/ui/ui-kit/FormFieldset'; import type { InputHandle } from 'src/ui/ui-kit/Input/DebouncedInput'; import { DebouncedInput } from 'src/ui/ui-kit/Input/DebouncedInput'; -import { FormFieldset } from 'src/ui/ui-kit/FormFieldset'; import { UnstyledInput } from 'src/ui/ui-kit/UnstyledInput'; -import type { Chain } from 'src/modules/networks/Chain'; -import { NBSP } from 'src/ui/shared/typography'; -import { FLOAT_INPUT_PATTERN } from 'src/ui/shared/forms/inputs'; -import type { AddressPosition, Asset } from 'defi-sdk'; -import { MarketAssetSelect } from 'src/ui/pages/SwapForm/fieldsets/ReceiveTokenField/MarketAssetSelect'; -import type { EmptyAddressPosition } from '@zeriontech/transactions'; -import { ReceiveFiatInputValue } from 'src/ui/components/FiatInputValue/FiatInputValue'; -import { calculatePriceImpact } from 'src/ui/pages/SwapForm/shared/price-impact'; export function ReceiveTokenField({ receiveInput, @@ -25,6 +25,7 @@ export function ReceiveTokenField({ availableReceivePositions, spendInput, spendAsset, + priceImpact, onChangeAmount, onChangeToken, }: { @@ -35,6 +36,7 @@ export function ReceiveTokenField({ availableReceivePositions: AddressPosition[]; spendInput?: string; spendAsset: Asset | null; + priceImpact: PriceImpact | null; onChangeAmount: (value: string) => void; onChangeToken: (value: string) => void; }) { @@ -55,17 +57,6 @@ export function ReceiveTokenField({ } }, [receiveInput]); - const priceImpact = useMemo( - () => - calculatePriceImpact({ - inputValue: spendInput ?? null, - outputValue: receiveInput ?? null, - inputAsset: spendAsset, - outputAsset: receiveAsset, - }), - [receiveAsset, receiveInput, spendAsset, spendInput] - ); - const inputId = useId(); const inputRef = useRef(null); diff --git a/src/ui/pages/SendForm/SendForm.tsx b/src/ui/pages/SendForm/SendForm.tsx index 2567b22b4..9bc38214f 100644 --- a/src/ui/pages/SendForm/SendForm.tsx +++ b/src/ui/pages/SendForm/SendForm.tsx @@ -381,6 +381,8 @@ function SendFormComponent() { quantity: amount, chain, }), + warningWasShown: false, + outputAmountColor: 'grey', }); if (preferences) { setPreferences({ diff --git a/src/ui/pages/SwapForm/SwapForm.tsx b/src/ui/pages/SwapForm/SwapForm.tsx index bce423d72..9a483fc4c 100644 --- a/src/ui/pages/SwapForm/SwapForm.tsx +++ b/src/ui/pages/SwapForm/SwapForm.tsx @@ -108,8 +108,12 @@ import { getQuotesErrorMessage } from './Quotes/getQuotesErrorMessage'; import { SlippageLine } from './SlippageSettings/SlippageLine'; import { getPopularTokens } from './shared/getPopularTokens'; import type { PriceImpact } from './shared/price-impact'; -import { calculatePriceImpact } from './shared/price-impact'; -import { PriceImpactLine } from './shared/PriceImpactLine'; +import { + calculatePriceImpact, + isHighValueLoss, + isSignificantValueLoss, +} from './shared/price-impact'; +import { PriceImpactWarningLine } from './shared/PriceImpactWarningLine'; const rootNode = getRootDomNode(); @@ -139,9 +143,9 @@ function FormHint({ : null; const exceedsBalance = Number(spendInput) > Number(positionBalanceCommon); - const showPriceImpactWarning = - priceImpact?.kind === 'loss' && - (priceImpact.level === 'medium' || priceImpact.level === 'high'); + const priceImpactWarningIconVisible = Boolean( + priceImpact && isSignificantValueLoss(priceImpact) + ); let hint: React.ReactNode | null = null; if (exceedsBalance) { @@ -152,7 +156,7 @@ function FormHint({ hint = 'Incorrect amount'; } else if (quotesData.error) { hint = getQuotesErrorMessage(quotesData); - } else if (showPriceImpactWarning) { + } else if (priceImpactWarningIconVisible) { hint = ( { - return calculatePriceImpact({ - inputValue: spendInput || null, - outputValue: receiveInput || null, - inputAsset: spendAsset, - outputAsset: receiveAsset, - }); - }, [receiveAsset, receiveInput, spendAsset, spendInput]); - const snapshotRef = useRef(null); const onBeforeSubmit = () => { snapshotRef.current = swapView.store.getState(); @@ -468,6 +461,8 @@ export function SwapFormComponent() { quantity: selectedQuote.input_amount_estimation, chain, }), + warningWasShown: false, + outputAmountColor: 'grey', }); return txResponse.hash; }, @@ -475,6 +470,29 @@ export function SwapFormComponent() { }); const approveTxStatus = useTransactionStatus(approveHash ?? null); + const isApproveMode = + approveMutation.isLoading || + (quotesData.done && !enough_allowance) || + approveTxStatus === 'pending'; + + const { receiveAsset, spendAsset } = swapView; + + const priceImpact = useMemo(() => { + return calculatePriceImpact({ + inputValue: spendInput || null, + outputValue: receiveInput || null, + inputAsset: spendAsset, + outputAsset: receiveAsset, + }); + }, [receiveAsset, receiveInput, spendAsset, spendInput]); + + const priceImpactWarningVisible = Boolean( + quotesData.done && + !isApproveMode && + priceImpact && + isHighValueLoss(priceImpact) + ); + useEffect(() => { if (approveTxStatus === 'confirmed') { refetchAllowanceQuery(); @@ -491,6 +509,9 @@ export function SwapFormComponent() { resetApproveMutation, ]); + const outputAmountColor = + priceImpact && isSignificantValueLoss(priceImpact) ? 'red' : 'grey'; + const { mutate: sendTransaction, data: transactionHash, @@ -534,6 +555,8 @@ export function SwapFormComponent() { chain, }), quote: selectedQuote, + warningWasShown: priceImpactWarningVisible, + outputAmountColor, }); return txResponse.hash; }, @@ -618,10 +641,6 @@ export function SwapFormComponent() { ); } - const isApproveMode = - approveMutation.isLoading || - (quotesData.done && !enough_allowance) || - approveTxStatus === 'pending'; const showApproveHintLine = (quotesData.done && !enough_allowance) || !approveMutation.isIdle; @@ -896,8 +915,8 @@ export function SwapFormComponent() { {selectedQuote ? : null} - {quotesData.done && priceImpact && !isApproveMode ? ( - + {priceImpactWarningVisible && priceImpact ? ( + ) : null} diff --git a/src/ui/pages/SwapForm/shared/PriceImpactLine/index.ts b/src/ui/pages/SwapForm/shared/PriceImpactLine/index.ts deleted file mode 100644 index 70280f33d..000000000 --- a/src/ui/pages/SwapForm/shared/PriceImpactLine/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PriceImpactLine } from './PriceImpactLine'; diff --git a/src/ui/pages/SwapForm/shared/PriceImpactLine/PriceImpactLine.tsx b/src/ui/pages/SwapForm/shared/PriceImpactWarningLine/PriceImpactWarningLine.tsx similarity index 82% rename from src/ui/pages/SwapForm/shared/PriceImpactLine/PriceImpactLine.tsx rename to src/ui/pages/SwapForm/shared/PriceImpactWarningLine/PriceImpactWarningLine.tsx index 4aba2863c..3319598a6 100644 --- a/src/ui/pages/SwapForm/shared/PriceImpactLine/PriceImpactLine.tsx +++ b/src/ui/pages/SwapForm/shared/PriceImpactWarningLine/PriceImpactWarningLine.tsx @@ -5,10 +5,11 @@ import { UIText } from 'src/ui/ui-kit/UIText'; import { formatPercent } from 'src/shared/units/formatPercent'; import { getPriceImpactPercentage, type PriceImpact } from '../price-impact'; -export function PriceImpactLine({ priceImpact }: { priceImpact: PriceImpact }) { - const isHighValueLoss = - priceImpact.kind === 'loss' && priceImpact.level === 'high'; - +export function PriceImpactWarningLine({ + priceImpact, +}: { + priceImpact: PriceImpact; +}) { const priceImpactPercentage = priceImpact ? getPriceImpactPercentage(priceImpact) : null; @@ -21,7 +22,7 @@ export function PriceImpactLine({ priceImpact }: { priceImpact: PriceImpact }) { [priceImpactPercentage] ); - return isHighValueLoss ? ( + return ( High Price Impact @@ -32,5 +33,5 @@ export function PriceImpactLine({ priceImpact }: { priceImpact: PriceImpact }) { ) : null} - ) : null; + ); } diff --git a/src/ui/pages/SwapForm/shared/PriceImpactWarningLine/index.ts b/src/ui/pages/SwapForm/shared/PriceImpactWarningLine/index.ts new file mode 100644 index 000000000..21506741d --- /dev/null +++ b/src/ui/pages/SwapForm/shared/PriceImpactWarningLine/index.ts @@ -0,0 +1 @@ +export { PriceImpactWarningLine } from './PriceImpactWarningLine'; diff --git a/src/ui/pages/SwapForm/shared/price-impact.ts b/src/ui/pages/SwapForm/shared/price-impact.ts index e9e590500..d1bf3ae7d 100644 --- a/src/ui/pages/SwapForm/shared/price-impact.ts +++ b/src/ui/pages/SwapForm/shared/price-impact.ts @@ -87,3 +87,14 @@ export function getPriceImpactPercentage(priceImpact: PriceImpact) { return null; } } + +export function isHighValueLoss(priceImpact: PriceImpact) { + return priceImpact.kind === 'loss' && priceImpact.level === 'high'; +} + +export function isSignificantValueLoss(priceImpact: PriceImpact) { + return ( + priceImpact.kind === 'loss' && + (priceImpact.level === 'medium' || priceImpact.level === 'high') + ); +}