diff --git a/package.json b/package.json index bd4c75c..7e31863 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0", - "@dydxprotocol/v4-abacus": "^1.1.16", - "@dydxprotocol/v4-client-js": "^1.0.6", + "@dydxprotocol/v4-abacus": "^1.1.22", + "@dydxprotocol/v4-client-js": "^1.0.11", "@dydxprotocol/v4-localization": "^1.0.18", "@ethersproject/providers": "^5.7.2", "@js-joda/core": "^5.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d3a46c..f1f9f60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,11 +27,11 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/v4-abacus': - specifier: ^1.1.16 - version: 1.1.16 + specifier: ^1.1.22 + version: 1.1.22 '@dydxprotocol/v4-client-js': - specifier: ^1.0.6 - version: 1.0.6 + specifier: ^1.0.11 + version: 1.0.11 '@dydxprotocol/v4-localization': specifier: ^1.0.18 version: 1.0.18 @@ -988,12 +988,12 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/v4-abacus@1.1.16: - resolution: {integrity: sha512-R9ROWx70f0a4lHxAPP/iJd9qnLhzL1SG8vIowfNg2VvWLk6f+f1HwsgMhDd07H328RqY016GNrRmnigeyVcz/g==} + /@dydxprotocol/v4-abacus@1.1.22: + resolution: {integrity: sha512-txinqnKGblCEcn1FjMj5nW9Tv2446fbkRaquodwQSk0GTP92/78xl4VSOwMngc0f4heiVGbirlCKyHmtZUADmA==} dev: false - /@dydxprotocol/v4-client-js@1.0.6: - resolution: {integrity: sha512-xiWH+kbix+zhI6EsAnd+NDvkjBgxWtGwQmvpd0PjljWNYSFgUtNe5M+piDdRbl2nhy6YWbxAGTwwS3K/ih5qSw==} + /@dydxprotocol/v4-client-js@1.0.11: + resolution: {integrity: sha512-rJNFGTO+HU1GQppXeRLKXNk4HWx/h+DjU2sUCieEv5mnRIMQpLw8dH/L+Uyhxx2oovccY1MaKB4Fr9IngdrF0A==} dependencies: '@cosmjs/amino': 0.30.1 '@cosmjs/encoding': 0.31.1 @@ -1013,7 +1013,7 @@ packages: ethers: 6.6.1 long: 4.0.0 protobufjs: 6.11.4 - ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) + ws: 8.15.0 transitivePeerDependencies: - bufferutil - debug @@ -14037,6 +14037,19 @@ packages: utf-8-validate: 5.0.10 dev: false + /ws@8.15.0: + resolution: {integrity: sha512-H/Z3H55mrcrgjFwI+5jKavgXvwQLtfPCUEp6pi35VhoB0pfcHnSoyuTzkBEZpzq49g1193CUEwIvmsjcotenYw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.5.0: resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} engines: {node: '>=10.0.0'} 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..b44b2ac 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -31,6 +31,7 @@ import { log } from '@/lib/telemetry'; import { useAccounts } from './useAccounts'; import { useTokenConfigs } from './useTokenConfigs'; import { useDydxClient } from './useDydxClient'; +import { hashFromTx } from '@/lib/hashfromTx'; type SubaccountContextType = ReturnType; const SubaccountContext = createContext({} as SubaccountContextType); @@ -293,12 +294,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 tx = await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload }); + return hashFromTx(tx?.hash); }, [subaccountClient, sendSquidWithdrawFromSubaccount] ); diff --git a/src/lib/abacus/dydxChainTransactions.ts b/src/lib/abacus/dydxChainTransactions.ts index 425c16d..dd0620d 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, @@ -41,6 +42,7 @@ import { openDialog } from '@/state/dialogs'; import { StatefulOrderError } from '../errors'; import { bytesToBigInt } from '../numbers'; import { log } from '../telemetry'; +import { hashFromTx } from '../hashfromTx'; class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { private compositeClient: CompositeClient | undefined; @@ -380,6 +382,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: hashFromTx(tx?.hash) + }); + } 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 +500,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/lib/hashfromTx.ts b/src/lib/hashfromTx.ts new file mode 100644 index 0000000..98696e6 --- /dev/null +++ b/src/lib/hashfromTx.ts @@ -0,0 +1,3 @@ +export const hashFromTx = ( + txHash: string | Uint8Array +): string => `0x${Buffer.from(txHash).toString('hex')}`; 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