diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index 6c751ce2f0..1b0882b858 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -793,12 +793,36 @@ "subHeading": "Confirm the transaction request to complete this transaction" }, "executing": { - "heading": "Processing" + "heading": "Processing", + "subHeadingDuration": "This should take {{duration}}", + "subHeading": "Go to Axelarscan for transaction details" }, "executed": { - "heading": "Funds added successfully", - "subHeadingGoTo": "Go to", - "subHeadingTransactionDetails": "for transaction details" + "heading": "Tokens Added", + "subHeading": "Go to Axelarscan for transaction details", + "primaryButtonText": "Done" + }, + "executedZkEVM": { + "heading": "Tokens Added", + "subHeading": "Go to Immutable zkEVM explorer for transaction details", + "primaryButtonText": "Done" + }, + "needsGas": { + "heading": "Gas has surged", + "subHeading": "You’ll need to pay more gas to complete this transaction. You can do this via Axelarscan.", + "primaryButtonText": "Go to Axelarscan", + "secondaryButtonText": "Dismiss" + }, + "partialSuccess": { + "heading": "You’ve received axlUSDC", + "subHeading": "Your transaction was subject to slippage and you’ve received axlUSDC. To receive the token you intended to add, you may need to complete a swap via Squid. ", + "primaryButtonText": "Go to Squid", + "secondaryButtonText": "Dismiss" + }, + "statusFailed": { + "heading": "Unable to complete transaction", + "subHeading": "Something went wrong, and we were unable to complete this transaction. Visit Axelarscan for details.", + "secondaryButtonText": "Dismiss" } }, "onboarding": { diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index bf0f5a3971..361fc8b034 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -776,12 +776,36 @@ "subHeading": "取引リクエストを確認してこの取引を完了してください" }, "executing": { - "heading": "処理中" + "heading": "処理中", + "subHeadingDuration": "この処理には{{duration}}かかります", + "subHeading": "Axelarscanでトランザクションの詳細をご確認ください" }, "executed": { - "heading": "資金が正常に追加されました", - "subHeadingGoTo": "移動", - "subHeadingTransactionDetails": "取引詳細はこちら" + "heading": "トークンが追加されました", + "subHeading": "Axelarscanでトランザクションの詳細をご確認ください", + "primaryButtonText": "完了" + }, + "executedZkEVM": { + "heading": "トークンが追加されました", + "subHeading": "Immutable zkEVM explorerでトランザクションの詳細をご確認ください", + "primaryButtonText": "完了" + }, + "needsGas": { + "heading": "ガス料金が急騰しました", + "subHeading": "このトランザクションを完了するためには、さらにガス料金を支払う必要があります。Axelarscanで手続きを行えます。", + "primaryButtonText": "Axelarscanに移動", + "secondaryButtonText": "閉じる" + }, + "partialSuccess": { + "heading": "axlUSDCを受け取りました", + "subHeading": "トランザクションはスリッページの影響を受け、axlUSDCを受け取りました。意図したトークンを受け取るには、Squidでスワップを完了する必要があります。", + "primaryButtonText": "Squidに移動", + "secondaryButtonText": "閉じる" + }, + "statusFailed": { + "heading": "トランザクションを完了できませんでした", + "subHeading": "問題が発生し、トランザクションを完了できませんでした。詳細はAxelarscanをご確認ください。", + "secondaryButtonText": "閉じる" } }, "onboarding": { diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index eca668a4bd..31169ba489 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -773,12 +773,36 @@ "subHeading": "거래 요청을 확인하여 이 거래를 완료하세요" }, "executing": { - "heading": "처리 중" + "heading": "처리 중", + "subHeadingDuration": "이 작업은 {{duration}} 소요됩니다", + "subHeading": "트랜잭션 세부 정보는 Axelarscan에서 확인하세요" }, "executed": { - "heading": "자금이 성공적으로 추가되었습니다", - "subHeadingGoTo": "이동", - "subHeadingTransactionDetails": "거래 세부 정보 보기" + "heading": "토큰이 추가되었습니다", + "subHeading": "트랜잭션 세부 정보는 Axelarscan에서 확인하세요", + "primaryButtonText": "완료" + }, + "executedZkEVM": { + "heading": "토큰이 추가되었습니다", + "subHeading": "트랜잭션 세부 정보는 Immutable zkEVM explorer에서 확인하세요", + "primaryButtonText": "완료" + }, + "needsGas": { + "heading": "가스 요금 급등", + "subHeading": "이 트랜잭션을 완료하려면 더 높은 가스 요금을 지불해야 합니다. Axelarscan에서 진행할 수 있습니다.", + "primaryButtonText": "Axelarscan으로 이동", + "secondaryButtonText": "닫기" + }, + "partialSuccess": { + "heading": "axlUSDC를 받았습니다", + "subHeading": "트랜잭션이 슬리피지에 영향을 받아 axlUSDC를 받았습니다. 의도한 토큰을 받으려면 Squid에서 스왑을 완료해야 할 수 있습니다.", + "primaryButtonText": "Squid로 이동", + "secondaryButtonText": "닫기" + }, + "statusFailed": { + "heading": "트랜잭션을 완료할 수 없습니다", + "subHeading": "문제가 발생하여 트랜잭션을 완료할 수 없습니다. 자세한 내용은 Axelarscan에서 확인하세요.", + "secondaryButtonText": "닫기" } }, "onboarding": { diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index d7c0cd1080..ca6c1a8821 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -773,12 +773,36 @@ "subHeading": "确认交易请求以完成此交易" }, "executing": { - "heading": "处理中" + "heading": "处理中", + "subHeadingDuration": "预计耗时{{duration}}", + "subHeading": "您可以前往Axelarscan查看交易详情" }, "executed": { - "heading": "资金添加成功", - "subHeadingGoTo": "前往", - "subHeadingTransactionDetails": "查看交易详情" + "heading": "代币已添加", + "subHeading": "您可以前往Axelarscan查看交易详情", + "primaryButtonText": "完成" + }, + "executedZkEVM": { + "heading": "代币已添加", + "subHeading": "您可以前往Immutable zkEVM explorer查看交易详情", + "primaryButtonText": "完成" + }, + "needsGas": { + "heading": "Gas费用激增", + "subHeading": "完成此交易需要支付更高的Gas费用。您可以通过Axelarscan完成。", + "primaryButtonText": "前往Axelarscan", + "secondaryButtonText": "关闭" + }, + "partialSuccess": { + "heading": "您已收到axlUSDC", + "subHeading": "交易受到滑点影响,您已收到axlUSDC。如需获得目标代币,可能需要通过Squid完成兑换。", + "primaryButtonText": "前往Squid", + "secondaryButtonText": "关闭" + }, + "statusFailed": { + "heading": "交易未能完成", + "subHeading": "交易发生错误,未能完成。详情请前往Axelarscan查看。", + "secondaryButtonText": "关闭" } }, "onboarding": { diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useExecute.ts b/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useExecute.ts index 0606cc4160..8d3d50b704 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useExecute.ts +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useExecute.ts @@ -4,6 +4,8 @@ import { RouteResponse } from '@0xsquid/squid-types'; import { Squid } from '@0xsquid/sdk'; import { ethers } from 'ethers'; import { Environment } from '@imtbl/config'; + +import { StatusResponse } from '@0xsquid/sdk/dist/types'; import { isSquidNativeToken } from '../functions/isSquidNativeToken'; import { useError } from './useError'; import { AddTokensError, AddTokensErrorTypes } from '../types'; @@ -13,6 +15,8 @@ import { retry } from '../../../lib/retry'; import { withMetricsAsync } from '../../../lib/metrics'; import { UserJourney } from '../../../context/analytics-provider/SegmentAnalyticsProvider'; +const TRANSACTION_NOT_COMPLETED = 'transaction not completed'; + export const useExecute = (contextId: string, environment: Environment) => { const { showErrorHandover } = useError(environment); const { @@ -165,7 +169,7 @@ export const useExecute = (contextId: string, environment: Environment) => { const callApprove = async ( provider: Web3Provider, routeResponse: RouteResponse, - ):Promise => { + ): Promise => { const erc20Abi = [ 'function approve(address spender, uint256 amount) public returns (bool)', ]; @@ -212,7 +216,7 @@ export const useExecute = (contextId: string, environment: Environment) => { squid: Squid, provider: Web3Provider, routeResponse: RouteResponse, - ):Promise => { + ): Promise => { const tx = (await squid.executeRoute({ signer: provider.getSigner(), route: routeResponse.route, @@ -239,10 +243,53 @@ export const useExecute = (contextId: string, environment: Environment) => { } }; + const getStatus = async ( + squid: Squid, + transactionHash: string, + ): Promise => { + const completedTransactionStatus = [ + 'success', + 'partial_success', + 'needs_gas', + 'not_found', + ]; + try { + return await retry( + async () => { + const result = await squid.getStatus({ + transactionId: transactionHash, + }); + if ( + completedTransactionStatus.includes( + result.squidTransactionStatus ?? '', + ) + ) { + return result; + } + return Promise.reject(TRANSACTION_NOT_COMPLETED); + }, + { + retries: 240, + retryIntervalMs: 5000, + nonRetryable: (err) => { + if (err.response) { + return err.response.status === 400 || err.response.status === 500; + } + return err !== TRANSACTION_NOT_COMPLETED; + }, + }, + ); + } catch (error) { + handleTransactionError(error); + return undefined; + } + }; + return { checkProviderChain, getAllowance, approve, execute, + getStatus, }; }; diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts b/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts index 93ee26afe0..e6d18f9152 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts @@ -8,4 +8,8 @@ export const APPROVE_TXN_ANIMATION = '/access_coins.riv'; export const EXECUTE_TXN_ANIMATION = '/swapping_coins.riv'; +export const BLOCK_TXN_ANIMATION = '/blocked.riv'; + +export const ERROR_TXN_ANIMATION = '/error.riv'; + export const TOKEN_PRIORITY_ORDER = ['IMX', 'USDC', 'ETH']; diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx index 1e891392d7..1fde35da82 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx @@ -24,6 +24,7 @@ import { } from '@biom3/react'; import { RouteResponse } from '@0xsquid/squid-types'; import { t } from 'i18next'; +import { Trans } from 'react-i18next'; import { Environment } from '@imtbl/config'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { AddTokensContext } from '../context/AddTokensContext'; @@ -65,11 +66,14 @@ import { getFormattedNumber, getFormattedNumberWithDecimalPlaces, } from '../functions/getFormattedNumber'; -import { convertToNetworkChangeableProvider } from '../functions/convertToNetworkChangeableProvider'; import { SquidFooter } from '../components/SquidFooter'; import { useError } from '../hooks/useError'; -import { sendAddTokensSuccessEvent } from '../AddTokensWidgetEvents'; +import { + sendAddTokensCloseEvent, + sendAddTokensSuccessEvent, +} from '../AddTokensWidgetEvents'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; +import { convertToNetworkChangeableProvider } from '../functions/convertToNetworkChangeableProvider'; interface ReviewProps { data: AddTokensReviewData; @@ -108,7 +112,9 @@ export function Review({ }, } = useProvidersContext(); - const { eventTargetState: { eventTarget } } = useContext(EventTargetContext); + const { + eventTargetState: { eventTarget }, + } = useContext(EventTargetContext); const [route, setRoute] = useState(); const [proceedDisabled, setProceedDisabled] = useState(true); @@ -122,10 +128,7 @@ export function Review({ const { showErrorHandover } = useError(checkout.config.environment); const { - checkProviderChain, - getAllowance, - approve, - execute, + checkProviderChain, getAllowance, approve, execute, getStatus, } = useExecute(id, checkout?.config.environment || Environment.SANDBOX); useEffect(() => { @@ -202,7 +205,9 @@ export function Review({ }} > {t('views.ADD_TOKENS.fees.includedFees')} - {` ${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${getFormattedAmounts(totalFeesUsd)}`} + {` ${t( + 'views.ADD_TOKENS.fees.fiatPricePrefix', + )} $${getFormattedAmounts(totalFeesUsd)}`} void; + secondaryButtonText?: string; + onSecondaryButtonClick?: () => void; + duration?: number; + } + const showHandover = useCallback( - ( - animationPath: string, - state: RiveStateMachineInput, - headingText: string, - subheadingText?: ReactNode, - duration?: number, - ) => { + ({ + animationPath, + state, + headingText, + subheadingText, + primaryButtonText, + onPrimaryButtonClick, + secondaryButtonText, + onSecondaryButtonClick, + duration, + }: HandoverProps) => { addHandover({ animationUrl: getRemoteRive( checkout?.config.environment, @@ -244,6 +265,10 @@ export function Review({ ), }); @@ -278,7 +303,10 @@ export function Review({ try { currentFromAddress = await fromProvider.getSigner().getAddress(); } catch (error) { - showErrorHandover(AddTokensErrorTypes.PROVIDER_ERROR, { contextId: id, error }); + showErrorHandover(AddTokensErrorTypes.PROVIDER_ERROR, { + contextId: id, + error, + }); return; } @@ -290,11 +318,11 @@ export function Review({ clearInterval(getRouteIntervalIdRef.current); setProceedDisabled(true); - showHandover( - APPROVE_TXN_ANIMATION, - RiveStateMachineInput.START, - t('views.ADD_TOKENS.handover.preparing.heading'), - ); + showHandover({ + animationPath: APPROVE_TXN_ANIMATION, + state: RiveStateMachineInput.START, + headingText: t('views.ADD_TOKENS.handover.preparing.heading'), + }); const changeableProvider = await convertToNetworkChangeableProvider( fromProvider, @@ -313,37 +341,38 @@ export function Review({ const { fromAmount } = route.route.params; if (allowance?.lt(fromAmount)) { - showHandover( - APPROVE_TXN_ANIMATION, - RiveStateMachineInput.WAITING, - t('views.ADD_TOKENS.handover.requestingApproval.heading'), - t('views.ADD_TOKENS.handover.requestingApproval.subHeading'), - ); + showHandover({ + animationPath: APPROVE_TXN_ANIMATION, + state: RiveStateMachineInput.WAITING, + headingText: t('views.ADD_TOKENS.handover.requestingApproval.heading'), + subheadingText: t( + 'views.ADD_TOKENS.handover.requestingApproval.subHeading', + ), + }); const approveTxnReceipt = await approve(changeableProvider, route); if (!approveTxnReceipt) { return; } - - showHandover( - APPROVE_TXN_ANIMATION, - RiveStateMachineInput.COMPLETED, - t('views.ADD_TOKENS.handover.approved.heading'), - '', - FIXED_HANDOVER_DURATION, - ); + showHandover({ + animationPath: APPROVE_TXN_ANIMATION, + state: RiveStateMachineInput.COMPLETED, + headingText: t('views.ADD_TOKENS.handover.approved.heading'), + duration: FIXED_HANDOVER_DURATION, + }); } - showHandover( - EXECUTE_TXN_ANIMATION, - RiveStateMachineInput.WAITING, - t('views.ADD_TOKENS.handover.requestingExecution.heading'), - t('views.ADD_TOKENS.handover.requestingExecution.subHeading'), - ); + showHandover({ + animationPath: EXECUTE_TXN_ANIMATION, + state: RiveStateMachineInput.WAITING, + headingText: t('views.ADD_TOKENS.handover.requestingExecution.heading'), + subheadingText: t( + 'views.ADD_TOKENS.handover.requestingExecution.subHeading', + ), + }); const executeTxnReceipt = await execute(squid, changeableProvider, route); - if (executeTxnReceipt) { track({ userJourney: UserJourney.ADD_TOKENS, @@ -357,37 +386,220 @@ export function Review({ sendAddTokensSuccessEvent(eventTarget, executeTxnReceipt.transactionHash); - showHandover( - EXECUTE_TXN_ANIMATION, - RiveStateMachineInput.PROCESSING, - t('views.ADD_TOKENS.handover.executing.heading'), - '', - FIXED_HANDOVER_DURATION, - ); + if (toChain === fromChain) { + showHandover({ + animationPath: EXECUTE_TXN_ANIMATION, + state: RiveStateMachineInput.COMPLETED, + headingText: t('views.ADD_TOKENS.handover.executedZkEVM.heading'), + subheadingText: ( + + )} + /> + ), + }} + /> + ), + primaryButtonText: t( + 'views.ADD_TOKENS.handover.executed.primaryButtonText', + ), + onPrimaryButtonClick: () => { + sendAddTokensCloseEvent(eventTarget); + }, + }); + return; + } - showHandover( - EXECUTE_TXN_ANIMATION, - RiveStateMachineInput.COMPLETED, - t('views.ADD_TOKENS.handover.executed.heading'), - <> - {t('views.ADD_TOKENS.handover.executed.subHeadingGoTo')} - {' '} - - )} - > - Axelarscan - - {' '} - {t('views.ADD_TOKENS.handover.executed.subHeadingTransactionDetails')} - , - ); + showHandover({ + animationPath: EXECUTE_TXN_ANIMATION, + state: RiveStateMachineInput.PROCESSING, + headingText: t('views.ADD_TOKENS.handover.executing.heading'), + subheadingText: ( + <> + {t('views.ADD_TOKENS.handover.executing.subHeadingDuration', { + duration: getDurationFormatted( + route.route.estimate.estimatedRouteDuration, + t('views.ADD_TOKENS.routeSelection.minutesText'), + t('views.ADD_TOKENS.routeSelection.minuteText'), + t('views.ADD_TOKENS.routeSelection.secondsText'), + ), + })} +
+ + )} + /> + ), + }} + /> + + ), + }); + + const status = await getStatus(squid, executeTxnReceipt.transactionHash); + const axelarscanUrl = `https://axelarscan.io/gmp/${executeTxnReceipt?.transactionHash}`; + + if (status?.squidTransactionStatus === 'success') { + showHandover({ + animationPath: EXECUTE_TXN_ANIMATION, + state: RiveStateMachineInput.COMPLETED, + headingText: t('views.ADD_TOKENS.handover.executed.heading'), + subheadingText: ( + + )} + /> + ), + }} + /> + ), + primaryButtonText: t( + 'views.ADD_TOKENS.handover.executed.primaryButtonText', + ), + onPrimaryButtonClick: () => { + sendAddTokensCloseEvent(eventTarget); + }, + }); + } else if (status?.squidTransactionStatus === 'needs_gas') { + showHandover({ + animationPath: APPROVE_TXN_ANIMATION, + state: RiveStateMachineInput.COMPLETED, + headingText: t('views.ADD_TOKENS.handover.needsGas.heading'), + subheadingText: ( + + )} + /> + ), + }} + /> + ), + primaryButtonText: t( + 'views.ADD_TOKENS.handover.needsGas.primaryButtonText', + ), + onPrimaryButtonClick: () => { + window.open(axelarscanUrl, '_blank', 'noreferrer'); + }, + secondaryButtonText: t( + 'views.ADD_TOKENS.handover.needsGas.secondaryButtonText', + ), + onSecondaryButtonClick: () => { + sendAddTokensCloseEvent(eventTarget); + }, + }); + } else if (status?.squidTransactionStatus === 'partial_success') { + showHandover({ + animationPath: APPROVE_TXN_ANIMATION, + state: RiveStateMachineInput.COMPLETED, + headingText: t('views.ADD_TOKENS.handover.partialSuccess.heading'), + subheadingText: ( + + )} + /> + ), + }} + /> + ), + primaryButtonText: t( + 'views.ADD_TOKENS.handover.partialSuccess.primaryButtonText', + ), + onPrimaryButtonClick: () => { + window.open( + 'https://toolkit.immutable.com/squid-bridge/', + '_blank', + 'noreferrer', + ); + }, + secondaryButtonText: t( + 'views.ADD_TOKENS.handover.partialSuccess.secondaryButtonText', + ), + onSecondaryButtonClick: () => { + sendAddTokensCloseEvent(eventTarget); + }, + }); + } else { + showHandover({ + animationPath: APPROVE_TXN_ANIMATION, + state: RiveStateMachineInput.COMPLETED, + headingText: t('views.ADD_TOKENS.handover.statusFailed.heading'), + subheadingText: ( + + )} + /> + ), + }} + /> + ), + secondaryButtonText: t( + 'views.ADD_TOKENS.handover.statusFailed.secondaryButtonText', + ), + onSecondaryButtonClick: () => { + sendAddTokensCloseEvent(eventTarget); + }, + }); + } } }, [ route, @@ -515,7 +727,9 @@ export function Review({ sx={{ flexShrink: 0, alignSelf: 'flex-start' }} > - {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${route?.route.estimate.fromAmountUSD ?? ''}`} + {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${ + route?.route.estimate.fromAmountUSD ?? '' + }`} @@ -569,12 +783,13 @@ export function Review({ {t('views.ADD_TOKENS.review.poweredBySquid')}
1 - {' '} {route.route.estimate.fromToken.symbol} {' '} = {' '} - {getFormattedNumberWithDecimalPlaces(route.route.estimate.exchangeRate)} + {getFormattedNumberWithDecimalPlaces( + route.route.estimate.exchangeRate, + )} {' '} {route.route.estimate.toToken.name} @@ -652,7 +867,9 @@ export function Review({ sx={{ flexShrink: 0, alignSelf: 'flex-start' }} > - {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${route?.route.estimate.toAmountUSD ?? ''}`} + {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${ + route?.route.estimate.toAmountUSD ?? '' + }`} @@ -710,7 +927,8 @@ export function Review({ disabled={proceedDisabled} sx={{ mx: 'base.spacing.x3' }} > - {proceedDisabled ? t('views.ADD_TOKENS.review.processingButtonText') + {proceedDisabled + ? t('views.ADD_TOKENS.review.processingButtonText') : t('views.ADD_TOKENS.review.proceedButtonText')} @@ -719,10 +937,10 @@ export function Review({ )} {!route && !showAddressMissmatchDrawer && ( - + )}