From 1608a27e3e9086cb90c79efd4cc7849d1b7994ee Mon Sep 17 00:00:00 2001 From: Bill He Date: Tue, 5 Sep 2023 10:57:52 -0700 Subject: [PATCH] address comments --- package.json | 2 +- pnpm-lock.yaml | 8 +-- src/{constants/abi.ts => abi/erc20.json} | 4 +- src/hooks/useLocalNotifications.tsx | 12 ++-- src/hooks/useSquid.tsx | 12 ++-- src/hooks/useWalletConnection.ts | 5 +- src/views/TransferStatus.tsx | 2 +- src/views/TransferStatusSteps.tsx | 69 +++++++++---------- .../AccountManagementForms/DepositForm.tsx | 52 ++++++++++++-- 9 files changed, 104 insertions(+), 62 deletions(-) rename src/{constants/abi.ts => abi/erc20.json} (99%) diff --git a/package.json b/package.json index 315be66..31fe544 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0", - "@dydxprotocol/abacus": "^0.4.6", + "@dydxprotocol/abacus": "^0.4.7", "@dydxprotocol/v4-client-js": "^0.32.0", "@dydxprotocol/v4-localization": "^0.0.25", "@ethersproject/providers": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c9bff6..4129834 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/abacus': - specifier: ^0.4.6 - version: 0.4.6 + specifier: ^0.4.7 + version: 0.4.7 '@dydxprotocol/v4-client-js': specifier: ^0.32.0 version: 0.32.0 @@ -1145,8 +1145,8 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/abacus@0.4.6: - resolution: {integrity: sha512-nvF9B1JuWzsnrPtQ/mKo06XWuqFc9fy6TEFcFwlIGGbxKortod7fSDj9sEZGKGdRyIj5wvl7H/Rgh+dlywO+aQ==} + /@dydxprotocol/abacus@0.4.7: + resolution: {integrity: sha512-3c7Dx5H+KeXZYDpAOJs6fqxSNerKqNSD4tuMNn5F7RXAPWUOU8ADQHAFPnrP1PgTxPjfZBZQzLIziKpyXww4NQ==} dev: false /@dydxprotocol/dydxjs@0.3.0: diff --git a/src/constants/abi.ts b/src/abi/erc20.json similarity index 99% rename from src/constants/abi.ts rename to src/abi/erc20.json index 8cf651f..3b0ab2f 100644 --- a/src/constants/abi.ts +++ b/src/abi/erc20.json @@ -1,4 +1,4 @@ -export const erc20 = [ +[ { "constant": true, "inputs": [], @@ -219,4 +219,4 @@ export const erc20 = [ "name": "Transfer", "type": "event" } -] as const; +] \ No newline at end of file diff --git a/src/hooks/useLocalNotifications.tsx b/src/hooks/useLocalNotifications.tsx index 881aaf1..cd81aa7 100644 --- a/src/hooks/useLocalNotifications.tsx +++ b/src/hooks/useLocalNotifications.tsx @@ -65,10 +65,14 @@ const useLocalNotificationsContext = () => { fromChainId, status: currentStatus, } of transferNotifications) { - if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue; - - const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }); - if (status) statuses[txHash] = status; + try { + if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue; + + const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }); + if (status) statuses[txHash] = status; + } catch (error) { + console.error(error); + } } return statuses; }, diff --git a/src/hooks/useSquid.tsx b/src/hooks/useSquid.tsx index 0266fc7..8fed15a 100644 --- a/src/hooks/useSquid.tsx +++ b/src/hooks/useSquid.tsx @@ -1,17 +1,13 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; +import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js'; import { Squid } from '@0xsquid/sdk'; -import { DydxV4Network, isDydxV4Network } from '@/constants/networks'; +import { CLIENT_NETWORK_CONFIGS, DydxV4Network, isDydxV4Network } from '@/constants/networks'; import { getSelectedNetwork } from '@/state/appSelectors'; -const SQUID_BASE_URL: Record = { - [DydxV4Network.V4Testnet2]: 'https://squid-api-git-feat-cosmos-maintestnet-0xsquid.vercel.app', - [DydxV4Network.V4Staging]: undefined, - [DydxV4Network.V4Local]: undefined, - [DydxV4Network.V4Mainnet]: 'https://api.0xsquid.com', -}; +export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const useSquidContext = () => { const selectedNetwork = useSelector(getSelectedNetwork); @@ -27,7 +23,7 @@ const useSquidContext = () => { const squid = useMemo( () => isDydxV4Network(selectedNetwork) - ? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] }) + ? new Squid({ baseUrl: CLIENT_NETWORK_CONFIGS[selectedNetwork]?.endpoints['0xsquid'] }) : undefined, [selectedNetwork] ); diff --git a/src/hooks/useWalletConnection.ts b/src/hooks/useWalletConnection.ts index c8cbfb4..3269388 100644 --- a/src/hooks/useWalletConnection.ts +++ b/src/hooks/useWalletConnection.ts @@ -17,6 +17,7 @@ import { useConnect as useConnectWagmi, useAccount as useAccountWagmi, useDisconnect as useDisconnectWagmi, + usePublicClient as usePublicClientWagmi, useWalletClient as useWalletClientWagmi, } from 'wagmi'; import { @@ -42,6 +43,7 @@ export const useWalletConnection = () => { defaultValue: undefined, }); const { address: evmAddressWagmi, isConnected: isConnectedWagmi } = useAccountWagmi(); + const publicClientWagmi = usePublicClientWagmi(); const { data: signerWagmi } = useWalletClientWagmi(); const { disconnectAsync: disconnectWagmi } = useDisconnectWagmi(); @@ -214,7 +216,8 @@ export const useWalletConnection = () => { evmAddress, evmAddressWagmi, signerWagmi, - + publicClientWagmi, + // Wallet connection (Cosmos) dydxAddress, dydxAddressGraz, diff --git a/src/views/TransferStatus.tsx b/src/views/TransferStatus.tsx index 2f792c3..37fcc00 100644 --- a/src/views/TransferStatus.tsx +++ b/src/views/TransferStatus.tsx @@ -34,7 +34,7 @@ export const TransferStatusToast = ({ const [secondsLeft, setSecondsLeft] = useState(); const type = useMemo( - () => (status?.toChain?.chainData.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'), + () => (status?.toChain?.chainData?.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'), [status] ); diff --git a/src/views/TransferStatusSteps.tsx b/src/views/TransferStatusSteps.tsx index 0a55ddc..1d01aed 100644 --- a/src/views/TransferStatusSteps.tsx +++ b/src/views/TransferStatusSteps.tsx @@ -27,38 +27,17 @@ enum TransferStatusStep { export const TransferStatusSteps = ({ status }: ElementProps) => { const stringGetter = useStringGetter(); - const type = useMemo( - () => (status?.toChain?.chainData.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'), - [status] - ); - - const currentStep = useMemo(() => { + const { currentStep, steps, type } = useMemo(() => { const routeStatus = status?.routeStatus; - const fromChain = status?.fromChain?.chainData.chainId; - const toChain = status?.toChain?.chainData.chainId; + const fromChain = status?.fromChain?.chainData?.chainId; + const toChain = status?.toChain?.chainData?.chainId; + const type = toChain === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'; - if (!routeStatus?.length) return TransferStatusStep.FromChain; - - const currentStatus = routeStatus[routeStatus?.length - 1]; - - if (currentStatus.chainId === toChain) { - return currentStatus.status !== 'success' - ? TransferStatusStep.ToChain - : TransferStatusStep.Complete; - } - - if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') { - return TransferStatusStep.FromChain; - } - - return TransferStatusStep.Bridge; - }, [status]); - - const steps = useMemo( - () => [ + const steps = [ { label: stringGetter({ - key: type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL, + key: + type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL, }), step: TransferStatusStep.FromChain, link: status?.fromChain?.transactionUrl, @@ -72,15 +51,35 @@ export const TransferStatusSteps = ({ status }: ElementProps) => { label: stringGetter({ key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN, params: { - CHAIN: status?.toChain?.chainData.chainName, - } + CHAIN: status?.toChain?.chainData?.chainName, + }, }), step: TransferStatusStep.ToChain, link: status?.toChain?.transactionUrl, }, - ], - [status] - ); + ]; + + const currentStatus = routeStatus[routeStatus?.length - 1]; + + let currentStep = TransferStatusStep.Bridge; + + if (!routeStatus?.length) { + currentStep = TransferStatusStep.FromChain; + } else if (currentStatus.chainId === toChain) { + currentStep = + currentStatus.status !== 'success' + ? TransferStatusStep.ToChain + : TransferStatusStep.Complete; + } else if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') { + currentStep = TransferStatusStep.FromChain; + } + + return { + currentStep, + steps, + type, + }; + }, [status, stringGetter]); if (!status) return ; @@ -108,9 +107,7 @@ export const TransferStatusSteps = ({ status }: ElementProps) => { ) : ( - = step.step}> - {step.label} - + = step.step}>{step.label} )} diff --git a/src/views/forms/AccountManagementForms/DepositForm.tsx b/src/views/forms/AccountManagementForms/DepositForm.tsx index 143b327..3f36f26 100644 --- a/src/views/forms/AccountManagementForms/DepositForm.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm.tsx @@ -3,8 +3,8 @@ import styled, { type AnyStyledComponent } from 'styled-components'; import { type NumberFormatValues } from 'react-number-format'; import { shallowEqual, useSelector } from 'react-redux'; import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js'; -import { ethers } from 'ethers'; +import erc20 from '@/abi/erc20.json'; import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus'; import { AlertType } from '@/constants/alerts'; import { ButtonSize } from '@/constants/buttons'; @@ -14,6 +14,8 @@ import { NumberSign } from '@/constants/numbers'; import { useAccounts, useDebounce, useStringGetter } from '@/hooks'; import { useAccountBalance } from '@/hooks/useAccountBalance'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; +import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid'; +import { useWalletConnection } from '@/hooks/useWalletConnection'; import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; @@ -49,7 +51,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - const { signerWagmi } = useAccounts(); + const { evmAddress, signerWagmi } = useAccounts(); + const { publicClientWagmi } = useWalletConnection(); const { addTransferNotification } = useLocalNotifications(); @@ -68,6 +71,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { [token, resources] ); + const sourceChain = useMemo( + () => (chainIdStr ? resources?.chainResources?.get(chainIdStr) : undefined), + [chainId, resources] + ); + const [fromAmount, setFromAmount] = useState(''); const [slippage, setSlippage] = useState(0.01); // 1% slippage const debouncedAmount = useDebounce(fromAmount, 500); @@ -159,6 +167,36 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { } }, [balance, setFromAmount]); + const validateTokenApproval = useCallback(async () => { + if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer'); + if (!sourceToken?.address) throw new Error('Missing source token address'); + if (!sourceChain?.rpc) throw new Error('Missing source chain rpc'); + if (!requestPayload?.targetAddress) throw new Error('Missing target address'); + if (!requestPayload?.value) throw new Error('Missing transaction value'); + if (sourceToken?.address === NATIVE_TOKEN_ADDRESS) return; + + const allowance = await publicClientWagmi.readContract({ + address: sourceToken.address as `0x${string}`, + abi: erc20, + functionName: 'allowance', + args: [evmAddress as `0x${string}`, requestPayload.targetAddress as `0x${string}`] + }); + + const sourceAmountBN = BigInt(requestPayload.value) + + if (sourceAmountBN > (allowance as bigint)) { + const { request } = await publicClientWagmi.simulateContract({ + account: evmAddress, + address: sourceToken.address as `0x${string}`, + abi: erc20, + functionName: 'approve', + args: [requestPayload.targetAddress as `0x${string}`, sourceAmountBN], + }) + + await signerWagmi.writeContract(request); + } + }, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]); + const onSubmit = useCallback( async (e: FormEvent) => { try { @@ -179,20 +217,23 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { } setIsLoading(true); + + validateTokenApproval(); let tx = { to: requestPayload.targetAddress as `0x${string}`, data: requestPayload.data as `0x${string}`, - gasLimit: ethers.toBigInt(requestPayload.gasLimit), + gasLimit: BigInt(requestPayload.gasLimit), value: - requestPayload.routeType !== 'SEND' ? ethers.toBigInt(requestPayload.value) : undefined, + requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined, }; const txHash = await signerWagmi.sendTransaction(tx); + onDeposit?.(); if (txHash) { addTransferNotification({ - txHash, + txHash: txHash, toChainId: TESTNET_CHAIN_ID, fromChainId: chainIdStr || undefined, toAmount: summary?.usdcSize || undefined, @@ -202,6 +243,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { setFromAmount(''); } } catch (error) { + console.error(error); setError(error); } finally { setIsLoading(false);