Skip to content

Commit

Permalink
feat: add retryable expiration notice to deposit L2 failure state (#424)
Browse files Browse the repository at this point in the history
* Dev : Enhanced the L2 deposit failure state according to design

* Dev : Fixed the text overlapping in mobile responsive mode

* Dev - made the retryable days in failed deposit card - dynamic

* Dev: minor label changes

* Dev : enhanced retryable day handling logic, updated the design to latest

* Dev : Refactored the expiry date logic to retryable util

* Dev : updated the transaction table row expiry design according to figma

* Dev : made the Retryable expiration getter method naming more apt

* fix: update to correct L2 when switching networks (#429)

* refactor: Detect corrects L1 and L2 when switching network

* Restore unused component, fix undefined check

* Properly override chainIdToDefaultL2ChainId in RegisterLocalNetwork

* feat: add unstoppable domains support (#431)

* feat: add unstoppable support

* clean up code according to review comments

* remove support for polygon UD names

* rename methods

* Dev : PR review pointers addressed

* Dev : Revised the logic, PR review pointers addressed #2

Co-authored-by: Christophe Deveaux <[email protected]>
Co-authored-by: Fionna Chan <[email protected]>
  • Loading branch information
3 people committed Sep 20, 2022
1 parent 040a374 commit 484fd36
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ function DepositRowStatus({ tx }: { tx: MergedTransaction }) {
case DepositStatus.EXPIRED:
return (
<div className="flex flex-col space-y-1">
<StatusBadge variant="green">Success</StatusBadge>
<StatusBadge variant="yellow">Expired</StatusBadge>
<StatusBadge variant="red">Failed</StatusBadge>
</div>
)

Expand Down Expand Up @@ -121,7 +120,10 @@ export function TransactionsTableDepositRow({
const { redeem, isRedeeming } = useRedeemRetryable()

const isError = useMemo(() => {
if (tx.depositStatus === DepositStatus.L1_FAILURE) {
if (
tx.depositStatus === DepositStatus.L1_FAILURE ||
tx.depositStatus === DepositStatus.EXPIRED
) {
return true
}

Expand All @@ -138,6 +140,11 @@ export function TransactionsTableDepositRow({
[tx]
)

const showRetryableExpiredText = useMemo(
() => tx.depositStatus === DepositStatus.EXPIRED,
[tx]
)

const bgClassName = isError ? 'bg-brick' : ''

return (
Expand Down Expand Up @@ -178,6 +185,21 @@ export function TransactionsTableDepositRow({
</Button>
</Tooltip>
)}

{showRetryableExpiredText && (
<Tooltip
content={
<span>
When an L2 tx fails, you have 7 days to re-execute. After that
time period, the tx is no longer recoverable.
</span>
}
>
<span className="text-md font-normal uppercase text-brick-dark">
Expired
</span>
</Tooltip>
)}
</td>
</tr>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function DepositCardContainer({
}

return (
<div className={`w-full p-6 lg:rounded-xl ${bgClassName}`}>
<div className={`w-full p-6 pb-12 sm:pb-6 lg:rounded-xl ${bgClassName}`}>
{dismissable && (
<button
className={twMerge(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { useMemo } from 'react'
/*
+ We show this card when the L1 txn is completed, but L2 txn fails for some reason.
+ In that case, we give user an option to raise a Retryable ticket request (basically, retry that transaction)
+ This action is only limited to ~7 day window from the time of failure of the transaction.
+ The text for how many days remain is updated dynamically in the card using `retryableExpiryDays` state.
+ When the ~7 day window expires, we don't show this card, and move the transaction to `EXPIRED` state.
*/

import { useEffect, useMemo, useState, useCallback } from 'react'

import { MergedTransaction } from '../../state/app/state'
import { useNetworksAndSigners } from '../../hooks/useNetworksAndSigners'
import { useRedeemRetryable } from '../../hooks/useRedeemRetryable'
import { DepositCardContainer, DepositL1TxStatus } from './DepositCard'
import { Tooltip } from '../common/Tooltip'
import { Button } from '../common/Button'
import { getRetryableTicketExpiration } from '../../util/RetryableUtils'

export function DepositCardL2Failure({ tx }: { tx: MergedTransaction }) {
const { isConnectedToArbitrum } = useNetworksAndSigners()
const [retryableExpiryDays, setRetryableExpiryDays] = useState<{
isValid: boolean // false, if the days are still loading, or ticket is expired or there was error in loading
days: number
}>({ isValid: false, days: 0 })

const {
isConnectedToArbitrum,
l1: { provider: l1Provider },
l2: { provider: l2Provider }
} = useNetworksAndSigners()

const { redeem, isRedeeming } = useRedeemRetryable()

const isRedeemButtonDisabled = useMemo(
Expand All @@ -19,40 +38,86 @@ export function DepositCardL2Failure({ tx }: { tx: MergedTransaction }) {
[isConnectedToArbitrum]
)

const updateRetryableTicketExpirationDate =
useCallback(async (): Promise<void> => {
const { daysUntilExpired, isLoading, isLoadingError, isExpired } =
await getRetryableTicketExpiration({
l1TxHash: tx.txId,
l1Provider,
l2Provider
})

// update the state to show/hide text and the card
setRetryableExpiryDays({
days: daysUntilExpired,
isValid: !(isLoading || isLoadingError || isExpired)
})
}, [tx.txId, l1Provider, l2Provider])

useEffect(() => {
updateRetryableTicketExpirationDate()
}, [updateRetryableTicketExpirationDate])

const retryableExpiryDaysText = useMemo((): string => {
const remainingDays = retryableExpiryDays.days
if (remainingDays < 1) {
return 'less than a day'
} else if (remainingDays > 1) {
return `${remainingDays} days`
} else {
// case : remainingDays === 1
return `${remainingDays} day`
}
}, [retryableExpiryDays.days])

return (
<DepositCardContainer tx={tx}>
<span className="text-4xl font-semibold text-orange-dark">
{isRedeeming ? 'Re-executing...' : 'L2 transaction failed'}
</span>
<Tooltip
show={isRedeemButtonDisabled}
content={
<span>
Please connect to the L2 network to re-execute your deposit.
<>
{/* Only show the CTA and `days remaining to retry..` modal if the remaining days are valid */}
{retryableExpiryDays?.isValid && (
<DepositCardContainer tx={tx}>
<span className="text-4xl font-semibold text-orange-dark">
{isRedeeming ? 'Re-executing...' : 'L2 transaction failed'}
</span>
}
>
<Button
variant="primary"
loading={isRedeeming}
disabled={isRedeemButtonDisabled}
onClick={() => redeem(tx)}
className="text-2xl"
>
Re-execute
</Button>
</Tooltip>

<div className="h-2" />
<div className="flex flex-col font-light">
<span className="text-lg text-orange-dark">
L1 transaction: <DepositL1TxStatus tx={tx} />
</span>
<span className="text-lg text-orange-dark">
L2 transaction:{' '}
{isRedeeming ? 'Pending...' : 'Failed. Try re-executing.'}
</span>
</div>
</DepositCardContainer>

<div className="h-1" />

<span className="text-2xl font-normal text-orange-dark">
{`No worries, we can try again. You have ${retryableExpiryDaysText} to re-execute.`}
</span>

<div className="h-1" />

<Tooltip
show={isRedeemButtonDisabled}
content={
<span>
Please connect to the L2 network to re-execute your deposit.
</span>
}
>
<Button
variant="primary"
loading={isRedeeming}
disabled={isRedeemButtonDisabled}
onClick={() => redeem(tx)}
className="text-2xl"
>
Re-execute
</Button>
</Tooltip>

<div className="h-2" />
<div className="flex flex-col font-light">
<span className="text-lg text-orange-dark">
L1 transaction: <DepositL1TxStatus tx={tx} />
</span>
<span className="text-lg text-orange-dark">
L2 transaction:{' '}
{isRedeeming ? 'Pending...' : 'Failed. Try re-executing.'}
</span>
</div>
</DepositCardContainer>
)}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function WithdrawalCardContainer({
}

return (
<div className={`w-full p-6 lg:rounded-xl ${bgClassName}`}>
<div className={`w-full p-6 pb-12 sm:pb-6 lg:rounded-xl ${bgClassName}`}>
{dismissable && (
<button
className="arb-hover absolute top-4 right-4 text-lime-dark underline"
Expand Down
59 changes: 59 additions & 0 deletions packages/arb-token-bridge-ui/src/util/RetryableUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { L1TransactionReceipt, IL1ToL2MessageWriter } from '@arbitrum/sdk'
import { Signer } from '@ethersproject/abstract-signer'
import { Provider } from '@ethersproject/abstract-provider'
import dayjs from 'dayjs'
import { JsonRpcProvider } from '@ethersproject/providers'

type GetRetryableTicketParams = {
l1TxHash: string
Expand All @@ -9,6 +11,20 @@ type GetRetryableTicketParams = {
l2Signer: Signer
}

type GetRetryableTicketExpirationParams = {
l1TxHash: string
l1Provider: JsonRpcProvider
l2Provider: JsonRpcProvider
}

type RetryableTicketExpirationResponse = {
isLoading: boolean
isLoadingError: boolean
expirationDate: number
daysUntilExpired: number
isExpired: boolean
}

export async function getRetryableTicket({
l1TxHash,
retryableCreationId,
Expand All @@ -33,3 +49,46 @@ export async function getRetryableTicket({

return retryableTicket
}

export const getRetryableTicketExpiration = async ({
l1TxHash,
l1Provider,
l2Provider
}: GetRetryableTicketExpirationParams): Promise<RetryableTicketExpirationResponse> => {
let isLoading = true,
isLoadingError = false,
isExpired = false

let daysUntilExpired = 0,
expirationDate = 0

try {
const depositTxReceipt = await l1Provider.getTransactionReceipt(l1TxHash)
const l1TxReceipt = new L1TransactionReceipt(depositTxReceipt)
const l1ToL2Msg = await l1TxReceipt.getL1ToL2Message(l2Provider)

const now = dayjs()

const expiryDateResponse = await l1ToL2Msg.getTimeout()
const expirationDate = Number(expiryDateResponse.toString()) * 1000

daysUntilExpired = dayjs(expirationDate).diff(now, 'days')

if (daysUntilExpired >= 0) isExpired = false
} catch {
isLoadingError = true
}

isLoading = false

return {
// promise loading state
isLoading,
isLoadingError,

// expiration state
expirationDate,
daysUntilExpired,
isExpired
}
}

0 comments on commit 484fd36

Please sign in to comment.