diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 5262c83..a455fd1 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -122,9 +122,15 @@ export type NotificationDisplayData = { toastDuration?: number; }; +export enum TransferNotificationTypes { + Withdrawal = 'withdrawal', + Deposit = 'deposit', +} + // Notification types export type TransferNotifcation = { txHash: string; + type?: TransferNotificationTypes; toChainId?: string; fromChainId?: string; toAmount?: number; diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index 619a072..e76ff65 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -20,6 +20,7 @@ import { type NotificationTypeConfig, NotificationType, DEFAULT_TOAST_AUTO_CLOSE_MS, + TransferNotificationTypes, } from '@/constants/notifications'; import { useSelectedNetwork, useStringGetter } from '@/hooks'; @@ -152,20 +153,20 @@ export const notificationTypes: NotificationTypeConfig[] = [ useEffect(() => { for (const transfer of transferNotifications) { - const { fromChainId, status, txHash, toAmount } = transfer; + const { fromChainId, status, txHash, toAmount, type } = transfer; const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing'; const icon = ; - const type = + const transferType = type ?? fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId - ? 'withdrawal' - : 'deposit'; + ? TransferNotificationTypes.Withdrawal + : TransferNotificationTypes.Deposit; const title = stringGetter({ key: { deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS, withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS, - }[type], + }[transferType], }); const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0; @@ -190,7 +191,7 @@ export const notificationTypes: NotificationTypeConfig[] = [ slotIcon={icon} slotTitle={title} transfer={transfer} - type={type} + type={transferType} triggeredAt={transfer.triggeredAt} notification={notification} /> diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx index cd5fa57..43c048e 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -293,12 +293,29 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo ); const sendSquidWithdraw = useCallback( - async (amount: number, payload: string) => { + async (amount: number, payload: string, isCCtp?: boolean) => { + + const cctpWithdraw = () => { + return new Promise((resolve, reject) => + abacusStateManager.cctpWithdraw((success, error, data) => { + const parsedData = JSON.parse(data); + if (success && parsedData?.code == 0) { + resolve(parsedData?.transactionHash); + } else { + reject(error); + } + }) + ) + } + if (isCCtp) { + return await cctpWithdraw(); + } + if (!subaccountClient) { return; } - - return await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload }); + const txHash = await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload }); + return `0x${Buffer.from(txHash?.hash).toString('hex')}`; }, [subaccountClient, sendSquidWithdrawFromSubaccount] ); diff --git a/src/lib/abacus/dydxChainTransactions.ts b/src/lib/abacus/dydxChainTransactions.ts index 425c16d..f93dbb3 100644 --- a/src/lib/abacus/dydxChainTransactions.ts +++ b/src/lib/abacus/dydxChainTransactions.ts @@ -2,6 +2,7 @@ import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; import Long from 'long'; import type { IndexedTx } from '@cosmjs/stargate'; import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js'; +import { EncodeObject } from '@cosmjs/proto-signing'; import { CompositeClient, @@ -380,6 +381,90 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { } } + async withdrawToNobleIBC( + params: { + subaccountNumber: number, + amount: string, + ibcPayload: string, + } + ): Promise { + if (!this.compositeClient || !this.localWallet) { + throw new Error('Missing compositeClient or localWallet'); + } + + const { subaccountNumber, amount, ibcPayload } = params ?? {}; + const parsedIbcPayload: { + msgTypeUrl: string, + msg: any, + } = ibcPayload ? JSON.parse(ibcPayload) : undefined; + + try { + const msg = this.compositeClient.withdrawFromSubaccountMessage( + new SubaccountClient(this.localWallet, subaccountNumber), + parseFloat(amount).toFixed(this.compositeClient.validatorClient.config.denoms.USDC_DECIMALS) + ); + const ibcMsg: EncodeObject = { + typeUrl: parsedIbcPayload.msgTypeUrl, + value: parsedIbcPayload.msg, + }; + + const tx = await this.compositeClient.send( + this.localWallet, + () => Promise.resolve([msg, ibcMsg]), + false + ); + + return JSON.stringify({ + txHash: `0x${Buffer.from(tx?.hash).toString('hex')}` + }); + } catch (error) { + log('DydxChainTransactions/withdrawToNobleIBC', error); + + return JSON.stringify({ + error, + }); + } + } + + async cctpWithdraw(params: { + typeUrl: string, + value: any, + }): Promise { + if (!this.nobleClient?.isConnected) { + throw new Error('Missing nobleClient or localWallet'); + } + + try { + const ibcMsg = { + typeUrl: params.typeUrl, // '/circle.cctp.v1.MsgDepositForBurn', + value: params.value, + }; + const fee = await this.nobleClient.simulateTransaction([ibcMsg]); + + // take out fee from amount before sweeping + const amount = parseInt(ibcMsg.value.amount, 10) - + Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER); + + if (amount <= 0) { + throw new Error('noble balance does not cover fees'); + } + + ibcMsg.value.amount = amount.toString(); + + const tx = await this.nobleClient.send([ibcMsg]); + + const parsedTx = this.parseToPrimitives(tx); + + return JSON.stringify(parsedTx); + } catch (error) { + log('DydxChainTransactions/cctpWithdraw', error); + + return JSON.stringify({ + error, + }); + } + } + async transaction( type: TransactionTypes, paramsInJson: Abacus.Nullable, @@ -414,6 +499,17 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { callback(result); break; } + case TransactionType.WithdrawToNobleIBC: { + const result = await this.withdrawToNobleIBC(params); + callback(result); + break; + } + case TransactionType.CctpWithdraw: { + const result = await this.cctpWithdraw(params); + callback(result); + break; + break; + } default: { break; } diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index b8ffaae..51d9343 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -85,7 +85,7 @@ class AbacusStateManager { const appConfigs = AbacusAppConfig.Companion.forWeb; if (!isMainnet || testFlags.withCCTP) - appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2DepositOnly; + appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2; this.stateManager = new AsyncAbacusStateManager( '', @@ -262,6 +262,14 @@ class AbacusStateManager { ) => void ) => this.stateManager.cancelOrder(orderId, callback); + cctpWithdraw = ( + callback: ( + success: boolean, + parsingError: Nullable, + data: string, + ) => void + ): void => this.stateManager.commitCCTPWithdraw(callback); + // ------ Utils ------ // getHistoricalPnlPeriod = (): Nullable => this.stateManager.historicalPnlPeriod; diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx index f575366..2c308e3 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx @@ -10,7 +10,7 @@ import { AlertType } from '@/constants/alerts'; import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; -import { NotificationStatus } from '@/constants/notifications'; +import { NotificationStatus, TransferNotificationTypes } from '@/constants/notifications'; import { NumberSign } from '@/constants/numbers'; import { @@ -45,6 +45,7 @@ import { getTransferInputs } from '@/state/inputsSelectors'; import abacusStateManager from '@/lib/abacus'; import { MustBigNumber } from '@/lib/numbers'; +import { getNobleChainId } from '@/lib/squid'; import { TokenSelectMenu } from './TokenSelectMenu'; import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt'; @@ -167,16 +168,16 @@ export const WithdrawForm = () => { }) ); } else { - const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data); - if (txHash?.hash) { - const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`; + const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data, isCctp); + if (txHash) { addTransferNotification({ - txHash: hash, - fromChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId, + txHash: txHash, + type: TransferNotificationTypes.Withdrawal, + fromChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(), toChainId: chainIdStr || undefined, toAmount: debouncedAmountBN.toNumber(), triggeredAt: Date.now(), - notificationStatus: NotificationStatus.Triggered, + isCctp, }); abacusStateManager.clearTransferInputValues(); setWithdrawAmount(''); diff --git a/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx b/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx index 758c60b..3cb050b 100644 --- a/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx +++ b/src/views/notifications/TransferStatusNotification/TransferStatusSteps.tsx @@ -12,10 +12,11 @@ import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; import { layoutMixins } from '@/styles/layoutMixins'; import { STRING_KEYS } from '@/constants/localization'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; +import { TransferNotificationTypes } from '@/constants/notifications'; type ElementProps = { status?: StatusResponse; - type: 'withdrawal' | 'deposit'; + type: TransferNotificationTypes; }; type StyleProps = { @@ -46,11 +47,13 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps & { label: stringGetter({ key: - type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL, + type === TransferNotificationTypes.Deposit + ? STRING_KEYS.INITIATED_DEPOSIT + : STRING_KEYS.INITIATED_WITHDRAWAL, }), step: TransferStatusStep.FromChain, - link: - type === 'deposit' + link: + type === TransferNotificationTypes.Deposit ? status?.fromChain?.transactionUrl : routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash ? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}` @@ -63,14 +66,20 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps & }, { label: stringGetter({ - key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN, + key: + type === TransferNotificationTypes.Deposit + ? STRING_KEYS.DEPOSIT_TO_CHAIN + : STRING_KEYS.WITHDRAW_TO_CHAIN, params: { - CHAIN: type === 'deposit' ? 'dYdX' : status?.toChain?.chainData?.chainName, + CHAIN: + type === TransferNotificationTypes.Deposit + ? 'dYdX' + : status?.toChain?.chainData?.chainName, }, }), step: TransferStatusStep.ToChain, link: - type === 'withdrawal' + type === TransferNotificationTypes.Withdrawal ? status?.toChain?.transactionUrl : currentStatus?.chainId === dydxChainId && currentStatus?.txHash ? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash.replace('0x', ''))}` diff --git a/src/views/notifications/TransferStatusNotification/index.tsx b/src/views/notifications/TransferStatusNotification/index.tsx index fdbbcbf..36a0bbe 100644 --- a/src/views/notifications/TransferStatusNotification/index.tsx +++ b/src/views/notifications/TransferStatusNotification/index.tsx @@ -5,7 +5,7 @@ import { useInterval, useStringGetter } from '@/hooks'; import { AlertType } from '@/constants/alerts'; import { STRING_KEYS } from '@/constants/localization'; -import { TransferNotifcation } from '@/constants/notifications'; +import { TransferNotifcation, TransferNotificationTypes } from '@/constants/notifications'; import { formatSeconds } from '@/lib/timeUtils'; @@ -22,7 +22,7 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { TransferStatusSteps } from './TransferStatusSteps'; type ElementProps = { - type: 'withdrawal' | 'deposit'; + type: TransferNotificationTypes; transfer: TransferNotifcation; triggeredAt?: number; }; @@ -55,7 +55,7 @@ export const TransferStatusNotification = ({ useInterval({ callback: updateSecondsLeft }); const inProgressStatusString = - type === 'deposit' + type === TransferNotificationTypes.Deposit ? secondsLeft > 0 ? STRING_KEYS.DEPOSIT_STATUS : STRING_KEYS.DEPOSIT_STATUS_SHORTLY @@ -64,7 +64,7 @@ export const TransferStatusNotification = ({ : STRING_KEYS.WITHDRAW_STATUS_SHORTLY; const statusString = - type === 'deposit' + type === TransferNotificationTypes.Deposit ? status?.squidTransactionStatus === 'success' ? STRING_KEYS.DEPOSIT_COMPLETE : inProgressStatusString