Skip to content

Commit

Permalink
feat: add mixpanel event tracking for limit orders (#8343)
Browse files Browse the repository at this point in the history
  • Loading branch information
woodenfurniture authored Dec 12, 2024
1 parent f763373 commit e169aa1
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ import { RawText, Text } from 'components/Text'
import { TransactionTypeIcon } from 'components/TransactionHistory/TransactionTypeIcon'
import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast'
import { useWallet } from 'hooks/useWallet/useWallet'
import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton'
import { MixPanelEvent } from 'lib/mixpanel/types'
import { useCancelLimitOrderMutation } from 'state/apis/limit-orders/limitOrderApi'
import { selectAssetById, selectFeeAssetById } from 'state/slices/selectors'
import { useSelectorWithArgs } from 'state/store'

import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon'
import { getMixpanelLimitOrderEventData } from '../helpers'
import type { OrderToCancel } from '../types'

const cardBorderRadius = { base: '2xl' }
Expand All @@ -45,15 +48,30 @@ export const CancelLimitOrder = ({ orderToCancel, resetOrderToCancel }: CancelLi
const wallet = useWallet().state.wallet
const { showErrorToast } = useErrorHandler()
const queryClient = useQueryClient()
const mixpanel = getMixPanel()

const [cancelLimitOrders, { error, isLoading, reset }] = useCancelLimitOrderMutation()
const [cancelLimitOrder, { error, isLoading, reset }] = useCancelLimitOrderMutation()

const sellAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.sellAssetId ?? '')
const buyAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.buyAssetId ?? '')
const feeAsset = useSelectorWithArgs(selectFeeAssetById, orderToCancel?.sellAssetId ?? '')

useEffect(() => {
if (!error) return

showErrorToast(error, 'limitOrder.cancel.cancellationFailed')
}, [error, showErrorToast])

const buyAmountCryptoPrecision = useMemo(() => {
if (!orderToCancel || !buyAsset) return '0'
return fromBaseUnit(orderToCancel.order.buyAmount, buyAsset.precision)
}, [buyAsset, orderToCancel])

const sellAmountCryptoPrecision = useMemo(() => {
if (!orderToCancel || !sellAsset) return '0'
return fromBaseUnit(orderToCancel.order.sellAmount, sellAsset.precision)
}, [orderToCancel, sellAsset])

const handleClose = useCallback(() => {
reset()
resetOrderToCancel()
Expand All @@ -64,29 +82,46 @@ export const CancelLimitOrder = ({ orderToCancel, resetOrderToCancel }: CancelLi
return
}

await cancelLimitOrders({ wallet, ...orderToCancel })
const result = await cancelLimitOrder({ wallet, ...orderToCancel })

// Exit if the request failed.
if ((result as { error: unknown }).error) return

// refetch the orders list for this account
queryClient.invalidateQueries({
queryKey: ['getLimitOrdersForAccount', orderToCancel.accountId],
})

resetOrderToCancel()
}, [orderToCancel, wallet, cancelLimitOrders, queryClient, resetOrderToCancel])

const sellAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.sellAssetId ?? '')
const buyAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.buyAssetId ?? '')
const feeAsset = useSelectorWithArgs(selectFeeAssetById, orderToCancel?.sellAssetId ?? '')
// Track event in mixpanel
const eventData = getMixpanelLimitOrderEventData({
sellAsset,
buyAsset,
sellAmountCryptoPrecision,
buyAmountCryptoPrecision,
})
if (mixpanel && eventData) {
mixpanel.track(MixPanelEvent.LimitOrderCanceled, eventData)
}
}, [
orderToCancel,
wallet,
cancelLimitOrder,
queryClient,
resetOrderToCancel,
sellAsset,
buyAsset,
sellAmountCryptoPrecision,
buyAmountCryptoPrecision,
mixpanel,
])

const limitPrice = useMemo(() => {
if (!orderToCancel || !sellAsset || !buyAsset) return
const buyAmountCryptoPrecision = fromBaseUnit(orderToCancel.order.buyAmount, buyAsset.precision)
const sellAmountCryptoPrecision = fromBaseUnit(
orderToCancel.order.sellAmount,
sellAsset.precision,
)
if (bnOrZero(sellAmountCryptoPrecision).isZero() || bnOrZero(buyAmountCryptoPrecision).isZero())
return
return bn(buyAmountCryptoPrecision).div(sellAmountCryptoPrecision).toFixed()
}, [buyAsset, orderToCancel, sellAsset])
}, [buyAmountCryptoPrecision, sellAmountCryptoPrecision])

const expiryText = useMemo(() => {
if (!orderToCancel) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { TransactionDate } from 'components/TransactionHistoryRows/TransactionDa
import { useActions } from 'hooks/useActions'
import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast'
import { useWallet } from 'hooks/useWallet/useWallet'
import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton'
import { MixPanelEvent } from 'lib/mixpanel/types'
import { usePlaceLimitOrderMutation } from 'state/apis/limit-orders/limitOrderApi'
import { limitOrderSlice } from 'state/slices/limitOrderSlice/limitOrderSlice'
import {
Expand All @@ -43,6 +45,7 @@ import { useAppSelector } from 'state/store'

import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon'
import { WithBackButton } from '../../WithBackButton'
import { getMixpanelLimitOrderEventData } from '../helpers'
import { LimitOrderRoutePaths } from '../types'

const cardBorderRadius = { base: '2xl' }
Expand All @@ -54,6 +57,7 @@ export const LimitOrderConfirm = () => {
const { confirmSubmit, setLimitOrderInitialized } = useActions(limitOrderSlice.actions)
const { showErrorToast } = useErrorHandler()
const queryClient = useQueryClient()
const mixpanel = getMixPanel()

const activeQuote = useAppSelector(selectActiveQuote)
const sellAsset = useAppSelector(selectActiveQuoteSellAsset)
Expand Down Expand Up @@ -99,18 +103,38 @@ export const LimitOrderConfirm = () => {
// TEMP: Bypass allowance approvals and jump straight to placing the order
setLimitOrderInitialized(quoteId)
confirmSubmit(quoteId)
await placeLimitOrder({ quoteId, wallet })
const result = await placeLimitOrder({ quoteId, wallet })

// Exit if the request failed.
if ((result as { error: unknown }).error) return

// refetch the orders list for this account
queryClient.invalidateQueries({
queryKey: ['getLimitOrdersForAccount', accountId],
refetchType: 'all',
})

// Track event in mixpanel
const eventData = getMixpanelLimitOrderEventData({
sellAsset,
buyAsset,
sellAmountCryptoPrecision,
buyAmountCryptoPrecision,
})
if (mixpanel && eventData) {
mixpanel.track(MixPanelEvent.LimitOrderPlaced, eventData)
}
}, [
activeQuote?.params.accountId,
activeQuote?.response.id,
buyAmountCryptoPrecision,
buyAsset,
confirmSubmit,
mixpanel,
placeLimitOrder,
queryClient,
sellAmountCryptoPrecision,
sellAsset,
setLimitOrderInitialized,
wallet,
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { Text } from 'components/Text'
import { WithBackButton } from '../../WithBackButton'
import { useGetLimitOrdersQuery } from '../hooks/useGetLimitOrdersForAccountQuery'
import type { OrderToCancel } from '../types'
import { CancelLimitOrder } from './CancelLimtOrder'
import { CancelLimitOrder } from './CancelLimitOrder'
import { LimitOrderCard } from './LimitOrderCard'

const textSelectedProps = {
Expand Down
64 changes: 63 additions & 1 deletion src/components/MultiHopTrade/components/LimitOrder/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import type { SerializedError } from '@reduxjs/toolkit'
import type { CowSwapError } from '@shapeshiftoss/types'
import type { Asset, CowSwapError } from '@shapeshiftoss/types'
import { OrderError } from '@shapeshiftoss/types'
import { bn } from '@shapeshiftoss/utils'
import type { InterpolationOptions } from 'node-polyglot'
import { getMaybeCompositeAssetSymbol } from 'lib/mixpanel/helpers'
import { assertUnreachable } from 'lib/utils'
import { selectCalculatedFees } from 'state/apis/snapshot/selectors'
import type { ReduxState } from 'state/reducer'
import {
selectAssets,
selectFeeAssetById,
selectMarketDataUsd,
selectUserCurrencyToUsdRate,
} from 'state/slices/selectors'
import { store } from 'state/store'

export const isCowSwapError = (
maybeCowSwapError: CowSwapError | SerializedError | undefined,
Expand Down Expand Up @@ -66,3 +77,54 @@ export const getCowSwapErrorTranslation = (
assertUnreachable(errorType)
}
}

export const getMixpanelLimitOrderEventData = ({
sellAsset,
buyAsset,
sellAmountCryptoPrecision,
buyAmountCryptoPrecision,
}: {
sellAsset: Asset | undefined
buyAsset: Asset | undefined
sellAmountCryptoPrecision: string
buyAmountCryptoPrecision: string
}) => {
// mixpanel paranoia seeing impossibly high values
if (!sellAsset?.precision) return
if (!buyAsset?.precision) return

const state = store.getState() as ReduxState

const buyAssetFeeAsset = selectFeeAssetById(state, buyAsset.assetId)
const sellAssetFeeAsset = selectFeeAssetById(state, sellAsset.assetId)
const userCurrencyToUsdRate = selectUserCurrencyToUsdRate(state)
const marketDataUsd = selectMarketDataUsd(state)
const assets = selectAssets(state)

const sellAmountBeforeFeesUsd = bn(sellAmountCryptoPrecision)
.times(marketDataUsd[sellAsset.assetId]?.price ?? 0)
.toString()
const sellAmountBeforeFeesUserCurrency = bn(sellAmountBeforeFeesUsd)
.times(userCurrencyToUsdRate)
.toString()

const feeParams = { feeModel: 'SWAPPER' as const, inputAmountUsd: sellAmountBeforeFeesUsd }
const { feeUsd: shapeshiftFeeUsd } = selectCalculatedFees(state, feeParams)
const shapeShiftFeeUserCurrency = shapeshiftFeeUsd.times(userCurrencyToUsdRate).toString()

const compositeBuyAsset = getMaybeCompositeAssetSymbol(buyAsset.assetId, assets)
const compositeSellAsset = getMaybeCompositeAssetSymbol(sellAsset.assetId, assets)

return {
buyAsset: compositeBuyAsset,
sellAsset: compositeSellAsset,
buyAssetChain: buyAssetFeeAsset?.networkName,
sellAssetChain: sellAssetFeeAsset?.networkName,
amountUsd: sellAmountBeforeFeesUsd,
amountUserCurrency: sellAmountBeforeFeesUserCurrency,
shapeShiftFeeUserCurrency,
shapeshiftFeeUsd: shapeshiftFeeUsd.toString(),
[compositeBuyAsset]: buyAmountCryptoPrecision,
[compositeSellAsset]: sellAmountCryptoPrecision,
}
}
2 changes: 2 additions & 0 deletions src/lib/mixpanel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export enum MixPanelEvent {
LpIncompleteWithdrawConfirm = 'LP Incomplete Withdraw Confirm',
CustomAssetAdded = 'Custom Asset Added',
ToggleWatchAsset = 'Toggle Watch Asset',
LimitOrderPlaced = 'Limit Order Placed',
LimitOrderCanceled = 'Limit Order Canceled',
}

export type TrackOpportunityProps = {
Expand Down
1 change: 1 addition & 0 deletions src/state/apis/limit-orders/limitOrderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const limitOrderApi = createApi({
`${baseUrl}/${network}/api/v1/orders/`,
limitOrder,
)

const orderId = result.data
return { data: orderId }
} catch (e) {
Expand Down

0 comments on commit e169aa1

Please sign in to comment.