Implement CCTP withdraw (#188)

* Implement CCTP withdraw

* bump packages

* address comments
This commit is contained in:
Bill 2023-12-12 10:12:37 -08:00 committed by GitHub
parent 2709d79f59
commit 0f16396c8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 195 additions and 39 deletions

View File

@ -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",

31
pnpm-lock.yaml generated
View File

@ -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'}

View File

@ -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;

View File

@ -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 = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
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}
/>

View File

@ -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<typeof useSubaccountContext>;
const SubaccountContext = createContext<SubaccountContextType>({} 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<string>((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]
);

View File

@ -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<string> {
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<string> {
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<string>,
@ -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;
}

View File

@ -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<ParsingError>,
data: string,
) => void
): void => this.stateManager.commitCCTPWithdraw(callback);
// ------ Utils ------ //
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod;

3
src/lib/hashfromTx.ts Normal file
View File

@ -0,0 +1,3 @@
export const hashFromTx = (
txHash: string | Uint8Array
): string => `0x${Buffer.from(txHash).toString('hex')}`;

View File

@ -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('');

View File

@ -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', ''))}`

View File

@ -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