Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: thorchain streaming swap progress bar #5192

Merged
merged 6 commits into from
Sep 3, 2023
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
5 changes: 4 additions & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@
"feedbackAndSupport": "Feedback & Support",
"getSupport": "Get Support",
"viewMyWallet": "View My Wallet",
"submitFeatureRequest": "Submit a Feature Request"
"submitFeatureRequest": "Submit a Feature Request",
"learnMore": "Learn more"
},
"consentBanner": {
"body": {
Expand Down Expand Up @@ -676,6 +677,8 @@
"approvingAsset": "Approving %{symbol}",
"viewTransaction": "View Transaction",
"unlimited": "Unlimited",
"streamStatus": "Stream Status",
"swapsFailed": "%{failedSwaps} swaps failed",
"exact": "Exact",
"expectedAmount": "Expected Amount",
"beforeFees": "Before Fees",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { WarningIcon } from '@chakra-ui/icons'
import {
Alert,
AlertDescription,
Expand All @@ -12,6 +13,7 @@ import {
Flex,
Heading,
Link,
Progress,
Skeleton,
Stack,
StackDivider,
Expand All @@ -30,6 +32,7 @@ import { ReceiveSummary } from 'components/MultiHopTrade/components/TradeConfirm
import { WithBackButton } from 'components/MultiHopTrade/components/WithBackButton'
import { getMixpanelEventData } from 'components/MultiHopTrade/helpers'
import { usePriceImpact } from 'components/MultiHopTrade/hooks/quoteValidation/usePriceImpact'
import { useThorStreamingProgress } from 'components/MultiHopTrade/hooks/useThorStreamingProgress/useThorStreamingProgress'
import { useTradeExecution } from 'components/MultiHopTrade/hooks/useTradeExecution/useTradeExecution'
import { chainSupportsTxHistory } from 'components/MultiHopTrade/utils'
import { Row } from 'components/Row/Row'
Expand All @@ -44,6 +47,7 @@ import { getTxLink } from 'lib/getTxLink'
import { firstNonZeroDecimal } from 'lib/math'
import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton'
import { MixPanelEvents } from 'lib/mixpanel/types'
import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwapper/constants'
import { SwapperName } from 'lib/swapper/types'
import { assertUnreachable } from 'lib/utils'
import { selectManualReceiveAddress } from 'state/slices/swappersSlice/selectors'
Expand Down Expand Up @@ -155,6 +159,18 @@ export const TradeConfirm = () => {

const txHash = buyTxHash ?? sellTxHash

const isThorStreamingSwap = useMemo(
() => tradeQuoteStep?.source === THORCHAIN_STREAM_SWAP_SOURCE,
[tradeQuoteStep?.source],
)

const {
attemptedSwapCount,
progressProps: thorStreamingSwapProgressProps,
totalSwapCount,
failedSwaps,
} = useThorStreamingProgress(sellTxHash, isThorStreamingSwap)

const getSellTxLink = useCallback(
(sellTxHash: string) =>
getTxLink({
Expand Down Expand Up @@ -464,6 +480,39 @@ export const TradeConfirm = () => {
isSubmitting={isSubmitting}
px={4}
/>
{status !== TxStatus.Unknown && isThorStreamingSwap && (
<Stack px={4}>
<Row>
<Row.Label>{translate('trade.streamStatus')}</Row.Label>
{totalSwapCount > 0 && (
<Row.Value>{`${attemptedSwapCount} of ${totalSwapCount}`}</Row.Value>
)}
</Row>
<Row>
<Progress
width='full'
borderRadius='full'
size='sm'
{...thorStreamingSwapProgressProps}
/>
</Row>
{failedSwaps.length > 0 && (
<Row>
<Row.Value display='flex' alignItems='center' gap={1} color='text.warning'>
<WarningIcon />
{translate('trade.swapsFailed', { failedSwaps: failedSwaps.length })}
</Row.Value>
{/* TODO: provide details of streaming swap failures - needs details modal
<Row.Value>
<Button variant='link' colorScheme='blue' fontSize='sm'>
{translate('common.learnMore')}
</Button>
</Row.Value>
*/}
</Row>
)}
</Stack>
)}
<Stack px={4}>{sendReceiveSummary}</Stack>
<Stack spacing={4}>
{txLink && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type ThornodeStreamingSwapResponseSuccess = {
tx_id?: string
interval?: number
quantity?: number
count?: number
last_height?: number
trade_target?: string
deposit?: string
in?: string
out?: string
failed_swaps?: number[]
failed_swap_reasons?: string[]
}

export type ThornodeStreamingSwapResponseError = { error: string }

export type ThornodeStreamingSwapResponse =
| ThornodeStreamingSwapResponseSuccess
| ThornodeStreamingSwapResponseError
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { ProgressProps } from '@chakra-ui/progress'
import axios from 'axios'
import { getConfig } from 'config'
import { useEffect, useMemo, useRef, useState } from 'react'
import { usePoll } from 'hooks/usePoll/usePoll'

import type { ThornodeStreamingSwapResponse, ThornodeStreamingSwapResponseSuccess } from './types'
const POLL_INTERVAL_MILLISECONDS = 30_000 // 30 seconds

export const getThorchainStreamingSwap = async (
sellTxHash: string,
): Promise<ThornodeStreamingSwapResponseSuccess | undefined> => {
const thorTxHash = sellTxHash.replace(/^0x/, '')
const { data: streamingSwapData } = await axios.get<ThornodeStreamingSwapResponse>(
`${getConfig().REACT_APP_THORCHAIN_NODE_URL}/lcd/thorchain/swap/streaming/${thorTxHash}`,
)

if (!streamingSwapData) return
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
if ('error' in streamingSwapData) {
console.error('failed to fetch streaming swap data', streamingSwapData.error)
return
}

return streamingSwapData
}

type FailedSwap = {
reason: string
swapIndex: number
}

export const useThorStreamingProgress = (
txHash: string | undefined,
isThorStreamingSwap: boolean,
): {
progressProps: ProgressProps
attemptedSwapCount: number
totalSwapCount: number
failedSwaps: FailedSwap[]
} => {
// a ref is used to allow updating and reading state without creating a dependency cycle
const streamingSwapDataRef = useRef<ThornodeStreamingSwapResponseSuccess>()
const [streamingSwapData, setStreamingSwapData] = useState<ThornodeStreamingSwapResponseSuccess>()
const { poll } = usePoll<ThornodeStreamingSwapResponseSuccess | undefined>()

useEffect(() => {
// exit if not a thor trade
if (!isThorStreamingSwap) return

// don't start polling until we have a tx
if (!txHash) return

poll({
fn: async () => {
const updatedStreamingSwapData = await getThorchainStreamingSwap(txHash)

// no payload at all - must be a failed request - return
if (!updatedStreamingSwapData) return

// no valid data received so far and no valid data to update - return
if (!streamingSwapDataRef.current?.quantity && !updatedStreamingSwapData.quantity) return

// thornode returns a default empty response once the streaming is complete
// set the count to the quantity so UI can display completed status
if (streamingSwapDataRef.current?.quantity && !updatedStreamingSwapData.quantity) {
const completedStreamingSwapData = {
...streamingSwapDataRef.current,
count: streamingSwapDataRef.current.quantity,
}
streamingSwapDataRef.current = completedStreamingSwapData
setStreamingSwapData(completedStreamingSwapData)
return completedStreamingSwapData
}

// data to update - update
streamingSwapDataRef.current = updatedStreamingSwapData
setStreamingSwapData(updatedStreamingSwapData)
return updatedStreamingSwapData
},
validate: streamingSwapData => {
if (!streamingSwapData || !streamingSwapData.quantity) return false
return streamingSwapData.count === streamingSwapData.quantity
},
interval: POLL_INTERVAL_MILLISECONDS,
maxAttempts: Infinity,
})
}, [isThorStreamingSwap, poll, txHash])

const failedSwaps = useMemo(() => {
if (!streamingSwapData) return []
const { failed_swap_reasons: failedSwapReasons, failed_swaps: failedSwaps } = streamingSwapData
return failedSwapReasons?.map((reason, i) => ({ reason, swapIndex: failedSwaps![i] })) ?? []
}, [streamingSwapData])

if (!streamingSwapData)
return {
progressProps: {
isIndeterminate: true,
},
attemptedSwapCount: 0,
totalSwapCount: 0,
failedSwaps,
}

const { quantity, count } = streamingSwapData

const isComplete = count === quantity

return {
progressProps: {
min: 0,
max: quantity,
value: count,
hasStripe: true,
isAnimated: !isComplete,
colorScheme: isComplete ? 'green' : 'blue',
},
attemptedSwapCount: count ?? 0,
totalSwapCount: quantity ?? 0,
failedSwaps,
}
}