[feat] native USDC bridging (#38)

* cctp bridging part 1: deposits

* small optimization

* part 2: finalize deposits

* add erc20spender

* fix build

* show correct statuses for deposits

* withdrawal functionality

* change copy

* rebase

* update copy, add modals, complete ux

* dry
This commit is contained in:
Lukas
2023-10-19 16:33:42 -04:00
committed by GitHub
parent c5eb616dc6
commit b93c375dd8
46 changed files with 2517 additions and 491 deletions

View File

@@ -1,4 +1,4 @@
ASSETS=eth,cbeth,comp
ASSETS=eth,cbeth,comp,usdc
L1_CHAIN_ID=5
L1_EXPLORER_URL=https://goerli.etherscan.io
L1_EXPLORER_API_URL=https://api-goerli.etherscan.io/api
@@ -10,6 +10,12 @@ L1_OPTIMISM_PORTAL_PROXY_ADDRESS=0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA
L2_L1_MESSAGE_PASSER_ADDRESS=0x4200000000000000000000000000000000000016
L2_STANDARD_BRIDGE=0x4200000000000000000000000000000000000010
L2_OUTPUT_ORACLE_PROXY_ADDRESS=0x2A35891ff30313CcFa6CE88dcf3858bb075A2298
L1_CCTP_MESSAGE_TRANSMITTER_ADDRESS=0x26413e8157cd32011e726065a5462e97dd4d03d9
L1_CCTP_TOKEN_MESSENGER_ADDRESS=0xd0c3da58f55358142b8d3e06c1c30c5c6114efe8
L2_CCTP_MESSAGE_TRANSMITTER_ADDRESS=0x9ff9a4da6f2157A9c82CE756f8fD7E0d75be8895
L2_CCTP_TOKEN_MESSENGER_ADDRESS=0x877b8e8c9e2383077809787ED6F279ce01CB4cc8
L1_CCTP_DOMAIN=0
L2_CCTP_DOMAIN=6
MARKETING_URL=https://base.org
DOCS_URL=https://docs.base.org
BLOG_URL=https://base.mirror.xyz
@@ -25,3 +31,5 @@ TOS_VERSION=0x01
MAINNET_GA_LAUNCH_FLAG=true
WALLET_CONNECT_PROJECT_ID=
COMPLIANCE_API_URL=https://bridge-api.base.org
CCTP_ATTESTATIONS_API_URL=https://iris-api-sandbox.circle.com
CCTP_ENABLED=true

View File

@@ -13,5 +13,6 @@ module.exports = {
rules: {
// Does not work with `:` aliases
'import/extensions': 'off',
'react/prop-types': 'off',
},
};

View File

@@ -10,6 +10,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/eth.svg',
L2icon: '/icons/currency/eth.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'cbETH',
@@ -22,6 +23,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/cbeth.svg',
L2icon: '/icons/currency/cbeth.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'DAI',
@@ -34,6 +36,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/dai.svg',
L2icon: '/icons/currency/dai.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'USDC',
@@ -46,6 +49,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/usdc.svg',
L2icon: '/icons/currency/usdbc.svg',
decimals: 6,
protocol: 'OP',
},
{
L1symbol: 'COMP',
@@ -58,6 +62,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/comp.svg',
L2icon: '/icons/currency/comp.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'BAL',
@@ -70,6 +75,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/balancer.svg',
L2icon: '/icons/currency/balancer.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'RPL',
@@ -82,6 +88,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/rocket-pool.png',
L2icon: '/icons/currency/rocket-pool.png',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'rETH',
@@ -94,6 +101,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/rocket-pool-eth.svg',
L2icon: '/icons/currency/rocket-pool-eth.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'SOFI',
@@ -106,6 +114,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/sofi.png',
L2icon: '/icons/currency/sofi.png',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'ZRX',
@@ -118,6 +127,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/0x.svg',
L2icon: '/icons/currency/0x.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'SUSHI',
@@ -130,6 +140,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/sushi.svg',
L2icon: '/icons/currency/sushi.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'CRV',
@@ -142,6 +153,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/curve.svg',
L2icon: '/icons/currency/curve.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: '1INCH',
@@ -154,6 +166,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/1inch.svg',
L2icon: '/icons/currency/1inch.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'WAMPL',
@@ -166,6 +179,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/wampl.svg',
L2icon: '/icons/currency/wampl.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'KNC',
@@ -178,6 +192,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/knc.svg',
L2icon: '/icons/currency/knc.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'ETH',
@@ -188,6 +203,20 @@ const assets: Asset[] = [
L1icon: '/icons/currency/eth.svg',
L2icon: '/icons/currency/eth.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'USDC',
L2symbol: 'USDbC',
L1chainId: 5,
L2chainId: 84531,
L1contract: '0x07865c6E87B9F70255377e024ace6630C1Eaa37F',
L2contract: '0x853154e2A5604E5C74a2546E2871Ad44932eB92C',
apiId: 'usd-coin',
L1icon: '/icons/currency/usdc.svg',
L2icon: '/icons/currency/usdbc.svg',
decimals: 6,
protocol: 'OP',
},
{
L1symbol: 'cbETH',
@@ -200,6 +229,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/cbeth.svg',
L2icon: '/icons/currency/cbeth.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'COMP',
@@ -212,6 +242,20 @@ const assets: Asset[] = [
L1icon: '/icons/currency/comp.svg',
L2icon: '/icons/currency/comp.svg',
decimals: 18,
protocol: 'OP',
},
{
L1symbol: 'USDC',
L2symbol: 'USDC',
L1chainId: 5,
L2chainId: 84531,
L1contract: '0x07865c6e87b9f70255377e024ace6630c1eaa37f',
L2contract: '0xf175520c52418dfe19c8098071a252da48cd1c19',
apiId: 'usd-coin',
L1icon: '/icons/currency/usdc.svg',
L2icon: '/icons/currency/usdc.svg',
decimals: 6,
protocol: 'CCTP',
},
{
L1symbol: 'ETH',
@@ -222,6 +266,7 @@ const assets: Asset[] = [
L1icon: '/icons/currency/eth.svg',
L2icon: '/icons/currency/eth.svg',
decimals: 18,
protocol: 'OP',
},
];

View File

@@ -14,6 +14,12 @@ declare module 'next/config' {
l2L1MessagePasserAddress: `0x${string}`;
L2StandardBridge: `0x${string}`;
l2OutputOracleProxyAddress: `0x${string}`;
l1CCTPMessageTransmitterAddress: `0x${string}`;
l1CCTPTokenMessengerAddress: `0x${string}`;
l2CCTPMessageTransmitterAddress: `0x${string}`;
l2CCTPTokenMessengerAddress: `0x${string}`;
l1CCTPDomain: string;
l2CCTPDomain: string;
marketingURL: string;
docsURL: string;
blogURL: string;
@@ -33,6 +39,8 @@ declare module 'next/config' {
appStage: string;
complianceApiURL: string;
sepoliaBridgeURL: string;
cctpAttestationsAPIURL: string;
cctpEnabled: string;
};
};

View File

@@ -75,6 +75,8 @@ const contentSecurityPolicy = {
'https://sepolia.etherscan.io', // Sepolia Etherscan
'https://api-sepolia.etherscan.io/api', // Sepolia Etherscan API
'https://base-sepolia.blockscout.com', // Sepolia Blockscout
'https://base-goerli.blockscout.com/api', // Blockscout
'https://iris-api-sandbox.circle.com/attestations/', // Circle
],
'img-src': ["'self'", 'data:', 'https://*.walletconnect.com/'], // WalletConnect,
};
@@ -161,6 +163,12 @@ module.exports = extendBaseConfig({
l2L1MessagePasserAddress: process.env.L2_L1_MESSAGE_PASSER_ADDRESS,
L2StandardBridge: process.env.L2_STANDARD_BRIDGE,
l2OutputOracleProxyAddress: process.env.L2_OUTPUT_ORACLE_PROXY_ADDRESS,
l1CCTPMessageTransmitterAddress: process.env.L1_CCTP_MESSAGE_TRANSMITTER_ADDRESS,
l1CCTPTokenMessengerAddress: process.env.L1_CCTP_TOKEN_MESSENGER_ADDRESS,
l2CCTPMessageTransmitterAddress: process.env.L2_CCTP_MESSAGE_TRANSMITTER_ADDRESS,
l2CCTPTokenMessengerAddress: process.env.L2_CCTP_TOKEN_MESSENGER_ADDRESS,
l1CCTPDomain: process.env.L1_CCTP_DOMAIN,
l2CCTPDomain: process.env.L2_CCTP_DOMAIN,
marketingURL: process.env.MARKETING_URL,
docsURL: process.env.DOCS_URL,
twitterURL: process.env.TWITTER_URL,
@@ -181,6 +189,8 @@ module.exports = extendBaseConfig({
appStage: process.env.APP_STAGE,
complianceApiURL: process.env.COMPLIANCE_API_URL,
sepoliaBridgeURL: process.env.SEPOLIA_BRIDGE_URL,
cctpAttestationsAPIURL: process.env.CCTP_ATTESTATIONS_API_URL,
cctpEnabled: process.env.CCTP_ENABLED,
},
...baseConfig,
async headers() {

View File

@@ -28,7 +28,7 @@
"react-query": "^3.39.3",
"typescript": "next",
"viem": "latest",
"wagmi": "^1.4.3",
"wagmi": "^1.4.4",
"webpack-bugsnag-plugins": "^1.8.0"
},
"devDependencies": {

View File

@@ -16,6 +16,7 @@ import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
import { useBlockNumberOfLatestL2OutputProposal } from 'apps/bridge/src/utils/hooks/useBlockNumberOfLatestL2OutputProposal';
import Head from 'next/head';
import Image from 'next/image';
import { FinalizeCCTPBridgeModal } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeModal';
const COLUMNS = ['Time', 'Type', 'Amount', 'Phase', 'Status'];
@@ -27,7 +28,10 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
const blockNumberOfLatestL2OutputProposal = useBlockNumberOfLatestL2OutputProposal();
const [modalProveTxHash, setModalProveTxHash] = useState<`0x${string}` | undefined>(undefined);
const [modalFinalizeTxHash, setModalFinalizeTxHash] = useState<`0x${string}` | undefined>(
const [modalFinalizeOPTxHash, setModalFinalizeOPTxHash] = useState<`0x${string}` | undefined>(
undefined,
);
const [modalFinalizeCCTPTxHash, setModalFinalizeCCTPTxHash] = useState<`0x${string}` | undefined>(
undefined,
);
@@ -41,6 +45,11 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
onOpen: onOpenFinalizeWithdrawalModal,
onClose: onCloseFinalizeWithdrawalModal,
} = useDisclosure();
const {
isOpen: isFinalizeCCTPBridgeModalOpen,
onOpen: onOpenFinalizeCCTPBridgeModal,
onClose: onCloseFinalizeCCTPBridgeModal,
} = useDisclosure();
return (
<div className="h-screen w-full overflow-auto">
@@ -52,13 +61,26 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
<FinalizeWithdrawalModal
isOpen={isFinalizeWithdrawalModalOpen}
onClose={onCloseFinalizeWithdrawalModal}
finalizeTxHash={modalFinalizeTxHash}
finalizeTxHash={modalFinalizeOPTxHash}
/>
<FinalizeCCTPBridgeModal
isOpen={isFinalizeCCTPBridgeModalOpen}
onClose={onCloseFinalizeCCTPBridgeModal}
finalizeTxHash={modalFinalizeCCTPTxHash}
/>
<Table
head={COLUMNS}
rows={transactions.map((transaction) => {
if (transaction.type === 'Deposit') {
return <DepositRow key={transaction.hash} transaction={transaction} />;
return (
<DepositRow
key={transaction.hash}
transaction={transaction}
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
/>
);
}
return (
<WithdrawalRow
@@ -69,8 +91,11 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
setModalProveTxHash={setModalProveTxHash}
setModalFinalizeTxHash={setModalFinalizeTxHash}
setModalFinalizeOPTxHash={setModalFinalizeOPTxHash}
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
/>
);
})}

View File

@@ -87,7 +87,7 @@ export function BridgeInput({
return () => {
onClose();
setSelectedAsset(asset);
setAmount('0');
setAmount('');
};
},
[onClose, setAmount, setSelectedAsset],
@@ -154,7 +154,7 @@ export function BridgeInput({
isDesktop
? {
minWidth: 40,
width: amount.length * 37,
width: amount.length * 37 + 20,
maxWidth: 16 * 31,
}
: {

View File

@@ -0,0 +1,83 @@
import { ReactNode } from 'react';
import Link from 'next/link';
type BadgeStatus = 'NOT_STARTED' | 'STARTED' | 'DONE';
type StepBadgeProps = {
num: string;
label: string;
status: BadgeStatus;
};
const BadgeClassNames = {
NOT_STARTED: '',
STARTED: 'border-white text-white',
DONE: 'border-white bg-white text-black font-bold',
};
const LabelClassNames = {
NOT_STARTED: '',
STARTED: 'text-white',
DONE: 'text-white',
};
function StepBadge({ num, label, status }: StepBadgeProps) {
const badgeClassNames = BadgeClassNames[status];
const labelClassNames = LabelClassNames[status];
return (
<div className="flex flex-col items-center gap-3">
<div className={`h-6 w-6 rounded-full border-2 leading-6 ${badgeClassNames}`}>
<span className="align-super text-sm">{status === 'DONE' ? '✓' : num}</span>
</div>
<span className={`font-sans text-base font-medium ${labelClassNames}`}>{label}</span>
</div>
);
}
type BarStatus = 'REQUEST_SENT' | 'VERIFIED';
type CCTPBridgeProgressBarProps = {
status: BarStatus;
};
const BarStatusToBadgeStatuses: Record<BarStatus, BadgeStatus[]> = {
REQUEST_SENT: ['STARTED', 'NOT_STARTED'],
VERIFIED: ['DONE', 'STARTED'],
};
const DisclaimerContent: Record<BarStatus, ReactNode> = {
REQUEST_SENT: (
<>
USDC deposits and withdrawals use Circle&apos;s CCTP. After you initiate a deposit or
withdrawal, you must complete the bridge in order to access your funds, on{' '}
<Link href="/transactions" className="underline">
the transactions page
</Link>
.
</>
),
VERIFIED:
'It takes a few minutes for the transaction to complete onchain. After this period, you can access your funds.',
};
export function CCTPBridgeProgressBar({ status }: CCTPBridgeProgressBarProps) {
const badgeStatuses = BarStatusToBadgeStatuses[status];
return (
<div className="flex flex-col gap-10">
<div className="mt-8 flex flex-row justify-around gap-8">
<div className="flex flex-col items-center">
<StepBadge num="1" status={badgeStatuses[0]} label="Send request" />
<span>Takes a few minutes</span>
</div>
<div className="flex flex-col items-center">
<StepBadge num="2" status={badgeStatuses[1]} label="Complete" />
<span>Takes a few minutes</span>
</div>
</div>
<span className="font-base">{DisclaimerContent[status]}</span>
<span className="text-white underline">
<Link href="https://developers.circle.com/stablecoin/docs/cctp-getting-started">
Learn more
</Link>
</span>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BridgeButton } from 'apps/bridge/src/components/BridgeButton/BridgeButton';
import { BridgeInput } from 'apps/bridge/src/components/BridgeInput/BridgeInput';
import { BridgeToInput } from 'apps/bridge/src/components/BridgeToInput/BridgeToInput';
@@ -8,7 +8,6 @@ import { FaqSidebar } from 'apps/bridge/src/components/Faq/FaqSidebar';
import { BaseButton } from 'apps/bridge/src/components/SwitchNetworkButton/SwitchNetworkButton';
import { TransactionSummary } from 'apps/bridge/src/components/TransactionSummary/TransactionSummary';
import { Asset } from 'apps/bridge/src/types/Asset';
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
import { useApproveContract } from 'apps/bridge/src/utils/hooks/useApproveContract';
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
@@ -20,29 +19,36 @@ import { usePrepareERC20Deposit } from 'apps/bridge/src/utils/hooks/usePrepareER
import { usePrepareERC20DepositTo } from 'apps/bridge/src/utils/hooks/usePrepareERC20DepositTo';
import { usePrepareETHDeposit } from 'apps/bridge/src/utils/hooks/usePrepareETHDeposit';
import { isAddress, parseUnits } from 'viem';
import { waitForTransaction } from 'wagmi/actions';
import getConfig from 'next/config';
import { useAccount, useBalance, useContractWrite } from 'wagmi';
import { useAccount, useBalance, useContractWrite, usePublicClient, useSwitchNetwork } from 'wagmi';
import { writeContract } from 'wagmi/actions';
import { useIsPermittedToBridgeTo } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridgeTo';
import { getL1NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL1NetworkForChainEnv';
import { getL2NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL2NetworkForChainEnv';
const assetList = getAssetListForChainEnv();
import { getDepositAssetsForChainEnv } from 'apps/bridge/src/utils/assets/getDepositAssetsForChainEnv';
import { usePrepareInitiateCCTPBridge } from 'apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge';
const { publicRuntimeConfig } = getConfig();
const activeAssets = getDepositAssetsForChainEnv();
const chainId = parseInt(publicRuntimeConfig.l1ChainID);
export function DepositContainer() {
const [depositAmount, setDepositAmount] = useState('0');
const [depositAmount, setDepositAmount] = useState('');
const [L1ApproveTxHash, setL1ApproveTxHash] = useState<`0x${string}` | undefined>(undefined);
const [L1DepositTxHash, setL1DepositTxHash] = useState<`0x${string}` | undefined>(undefined);
const [depositTo, setDepositTo] = useState('');
const [isApprovalTx, setIsApprovalTx] = useState(false);
const isWalletConnected = useIsWalletConnected();
const [selectedAsset, setSelectedAsset] = useState<Asset>(assetList[0]);
const activeAssets = assetList.filter((asset) =>
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
);
const [selectedAsset, setSelectedAsset] = useState<Asset>(activeAssets[0]);
const publicClient = usePublicClient({ chainId });
const { switchNetwork } = useSwitchNetwork();
useEffect(() => {
switchNetwork?.(chainId);
}, [switchNetwork]);
const { address } = useAccount();
const codeAtAddress = useGetCode(chainId, address);
const isSmartContractWallet = !!codeAtAddress && codeAtAddress !== '0x';
@@ -53,9 +59,16 @@ export function DepositContainer() {
chainId: parseInt(publicRuntimeConfig.l1ChainID),
});
const erc20Spender =
selectedAsset.protocol === 'CCTP'
? publicRuntimeConfig.l1CCTPTokenMessengerAddress
: publicRuntimeConfig.l1BridgeProxyAddress;
const { data: readERC20Approval, error: readERC20ApprovalError } = useIsContractApproved({
contactAddress: selectedAsset.L1contract,
address,
spender: erc20Spender,
bridgeDirection: 'deposit',
});
const readApprovalResult = useMemo(() => {
@@ -75,15 +88,18 @@ export function DepositContainer() {
const handleCloseDepositModal = useCallback(() => {
onCloseDepositModal();
setL1DepositTxHash(undefined);
setL1ApproveTxHash(undefined);
setIsApprovalTx(false);
}, [onCloseDepositModal]);
// approve erc20
const approveConfig = useApproveContract({
contractAddress: selectedAsset.L1contract,
spender: erc20Spender,
approveAmount: depositAmount,
decimals: selectedAsset.decimals,
bridgeDirection: 'deposit',
});
const { writeAsync: approveWrite } = useContractWrite(approveConfig);
const chainEnv = useChainEnv();
@@ -102,7 +118,6 @@ export function DepositContainer() {
isPermittedToBridge,
includeTosVersionByte,
});
const { writeAsync: depositETHWrite } = useContractWrite(depositETHConfig);
// deposit erc20
@@ -121,10 +136,21 @@ export function DepositContainer() {
isPermittedToBridge,
includeTosVersionByte,
});
const { writeAsync: depositERC20Write } = useContractWrite(depositERC20Config);
const { writeAsync: depositERC20ToWrite } = useContractWrite(depositERC20ToConfig);
// deposit using CCTP (eg USDC)
const depositCCTPAssetConfig = usePrepareInitiateCCTPBridge({
mintRecipient: isSmartContractWallet ? (depositTo as `0x${string}`) : address,
asset: selectedAsset,
amount: depositAmount,
destinationDomain: parseInt(publicRuntimeConfig.l2CCTPDomain),
isPermittedToBridge,
includeTosVersionByte,
bridgeDirection: 'deposit',
});
const { writeAsync: depositCCTPAssetWrite } = useContractWrite(depositCCTPAssetConfig);
const initiateApproval = useCallback(() => {
void (async () => {
setIsApprovalTx(true);
@@ -134,19 +160,27 @@ export function DepositContainer() {
if (approveResult?.hash) {
const approveTxHash: `0x${string}` = approveResult.hash;
setL1ApproveTxHash(approveTxHash);
// wait for confirmations
await waitForTransaction({ hash: approveResult?.hash });
}
// next, call the transfer function
setIsApprovalTx(false);
const depositResult = await (isSmartContractWallet
? depositERC20ToWrite?.()
: depositERC20Write?.());
if (depositResult?.hash) {
const depositTxHash = depositResult.hash;
setL1DepositTxHash(depositTxHash);
setDepositAmount('0');
// wait for confirmations
await publicClient.waitForTransactionReceipt({ hash: approveResult.hash });
// next, call the transfer function
setIsApprovalTx(false);
let depositMethod;
if (selectedAsset.protocol === 'CCTP') {
// because of how React works we need to use the writeContract wagmi/core action
// here (the hook still thinks the approval has not been set)
depositMethod = async () => await writeContract(depositCCTPAssetConfig);
} else {
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
}
const depositResult = await depositMethod?.();
if (depositResult?.hash) {
const depositTxHash = depositResult.hash;
setL1DepositTxHash(depositTxHash);
setDepositAmount('');
}
}
} catch (error) {
onCloseDepositModal();
@@ -154,11 +188,14 @@ export function DepositContainer() {
})();
}, [
approveWrite,
depositCCTPAssetConfig,
depositERC20ToWrite,
depositERC20Write,
isSmartContractWallet,
onCloseDepositModal,
onOpenDepositModal,
publicClient,
selectedAsset.protocol,
]);
const initiateDeposit = useCallback(() => {
@@ -169,7 +206,11 @@ export function DepositContainer() {
if (isPermittedToBridge) {
let depositMethod;
if (selectedAsset.L1contract) {
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
if (selectedAsset.protocol === 'CCTP') {
depositMethod = depositCCTPAssetWrite;
} else {
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
}
} else {
depositMethod = depositETHWrite;
}
@@ -177,7 +218,7 @@ export function DepositContainer() {
if (depositResult?.hash) {
const depositTxHash = depositResult.hash;
setL1DepositTxHash(depositTxHash);
setDepositAmount('0');
setDepositAmount('');
}
} else {
onCloseDepositModal();
@@ -190,6 +231,8 @@ export function DepositContainer() {
onOpenDepositModal,
isPermittedToBridge,
selectedAsset.L1contract,
selectedAsset.protocol,
depositCCTPAssetWrite,
isSmartContractWallet,
depositERC20ToWrite,
depositERC20Write,
@@ -245,6 +288,7 @@ export function DepositContainer() {
L1ApproveTxHash={L1ApproveTxHash}
L1DepositTxHash={L1DepositTxHash}
isApprovalTx={isApprovalTx}
protocol={selectedAsset.protocol}
/>
<BridgeInput
inputNetwork={getL1NetworkForChainEnv()}

View File

@@ -1,6 +1,9 @@
import { CCTPBridgeProgressBar } from 'apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar';
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
import getConfig from 'next/config';
import Link from 'next/link';
import { ReactNode } from 'react';
import { useWaitForTransaction } from 'wagmi';
const { publicRuntimeConfig } = getConfig();
@@ -11,6 +14,7 @@ type DepositModalProps = {
L1ApproveTxHash: `0x${string}` | undefined;
L1DepositTxHash: `0x${string}` | undefined;
isApprovalTx: boolean;
protocol: BridgeProtocol;
};
type STATE =
@@ -18,7 +22,8 @@ type STATE =
| 'APPROVAL_CONFIRMED'
| 'APPROVAL_NOT_STARTED'
| 'DEPOSIT_LOADING'
| 'DEPOSIT_CONFIRMED'
| 'OP_DEPOSIT_STARTED'
| 'CCTP_DEPOSIT_STARTED'
| 'DEPOSIT_NOT_STARTED';
function getState(
@@ -27,6 +32,7 @@ function getState(
isApproveSuccess: boolean,
isDepositLoading: boolean,
isDepositSuccess: boolean,
protocol: BridgeProtocol,
): STATE {
if (isApprovalTx && isApproveLoading) {
return 'APPROVAL_LOADING';
@@ -41,19 +47,19 @@ function getState(
return 'DEPOSIT_LOADING';
}
if (!isApprovalTx && isDepositSuccess) {
return 'DEPOSIT_CONFIRMED';
return protocol === 'CCTP' ? 'CCTP_DEPOSIT_STARTED' : 'OP_DEPOSIT_STARTED';
}
return 'DEPOSIT_NOT_STARTED';
}
const ModalContents = {
const ModalContents: Record<STATE, ReactNode> = {
APPROVAL_NOT_STARTED: 'Approval will initiate after confirmation.',
APPROVAL_LOADING: 'Waiting for confirmations...',
APPROVAL_CONFIRMED: 'Transaction confirmed.',
DEPOSIT_NOT_STARTED: 'Deposit will initiate after confirmation.',
DEPOSIT_LOADING: 'Waiting for confirmations...',
DEPOSIT_CONFIRMED: (
OP_DEPOSIT_STARTED: (
<>
Deposits typically take a few minutes to reach the Base network. When this is complete, you
can view this transaction at{' '}
@@ -63,24 +69,27 @@ const ModalContents = {
.
</>
),
CCTP_DEPOSIT_STARTED: <CCTPBridgeProgressBar status="REQUEST_SENT" />,
};
const Titles = {
const Titles: Record<STATE, string> = {
APPROVAL_NOT_STARTED: 'APPROVE IN YOUR WALLET',
APPROVAL_LOADING: 'APPROVING',
APPROVAL_CONFIRMED: 'APPROVED',
DEPOSIT_NOT_STARTED: 'CONFIRM DEPOSIT IN WALLET',
DEPOSIT_LOADING: 'CONFIRMING',
DEPOSIT_CONFIRMED: 'DEPOSIT IN TRANSIT TO BASE',
OP_DEPOSIT_STARTED: 'DEPOSIT IN TRANSIT TO BASE',
CCTP_DEPOSIT_STARTED: 'DEPOSIT IN PROGRESS',
};
const Icons = {
const Icons: Record<STATE, string> = {
APPROVAL_NOT_STARTED: 'wallet',
APPROVAL_LOADING: 'wallet',
APPROVAL_CONFIRMED: 'confirm',
DEPOSIT_NOT_STARTED: 'wallet',
DEPOSIT_LOADING: 'wallet',
DEPOSIT_CONFIRMED: 'deposit',
OP_DEPOSIT_STARTED: 'deposit',
CCTP_DEPOSIT_STARTED: '',
};
export function DepositModal({
@@ -89,6 +98,7 @@ export function DepositModal({
L1ApproveTxHash,
L1DepositTxHash,
isApprovalTx,
protocol,
}: DepositModalProps) {
const { isLoading: isApproveLoading, isSuccess: isApproveSuccess } = useWaitForTransaction({
hash: L1ApproveTxHash,
@@ -104,6 +114,7 @@ export function DepositModal({
isApproveSuccess,
isDepositLoading,
isDepositSuccess,
protocol,
);
const L1TxHash = isApprovalTx ? L1ApproveTxHash : L1DepositTxHash;

View File

@@ -1,4 +1,9 @@
import type { DepositPhase, WithdrawalPhase } from 'apps/bridge/src/utils/transactions/phase';
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
import type {
CCTPBridgePhase,
DepositPhase,
WithdrawalPhase,
} from 'apps/bridge/src/utils/transactions/phase';
import Image from 'next/image';
const networkSvgPaths: Record<'deposit' | 'withdraw', string> = {
@@ -21,22 +26,53 @@ const phaseSvgPaths: Record<WithdrawalPhase | DepositPhase, string> = {
FUNDS_DEPOSITED: '/icons/phases/send.svg',
};
const ccptPhasePaths: Record<CCTPBridgePhase, Record<'deposit' | 'withdraw', string>> = {
INITIATE_CCTP_BRIDGE_PENDING: {
deposit: '/icons/phases/send.svg',
withdraw: '/icons/phases/receive.svg',
},
INITIATE_CCTP_BRIDGE_FAILED: {
deposit: '/icons/phases/user_action.svg',
withdraw: '/icons/phases/user_action.svg',
},
FINALIZE_CCTP_BRIDGE: {
deposit: '/icons/phases/user_action.svg',
withdraw: '/icons/phases/user_action.svg',
},
FINALIZE_CCTP_BRIDGE_PENDING: {
deposit: '/icons/phases/wait.svg',
withdraw: '/icons/phases/wait.svg',
},
FINALIZE_CCTP_BRIDGE_FAILED: {
deposit: '/icons/phases/user_action.svg',
withdraw: '/icons/phases/user_action.svg',
},
CCTP_BRIDGE_COMPLETE: {
deposit: '/icons/phases/send.svg',
withdraw: '/icons/phases/receive.svg',
},
};
type Props = {
phase: DepositPhase | WithdrawalPhase;
phase: DepositPhase | WithdrawalPhase | CCTPBridgePhase;
protocol?: BridgeProtocol;
bridgeDirection: 'deposit' | 'withdraw';
size?: number;
};
export function TransactionIcon({ phase, bridgeDirection, size = 32 }: Props) {
const phaseIcon = (
<Image src={phaseSvgPaths[phase]} width={size} height={size} alt={phase.toLowerCase()} />
);
export function TransactionIcon({ phase, protocol, bridgeDirection, size = 32 }: Props) {
const icon =
protocol === 'CCTP'
? ccptPhasePaths[phase as CCTPBridgePhase][bridgeDirection]
: phaseSvgPaths[phase as DepositPhase | WithdrawalPhase];
const phaseIcon = <Image src={icon} width={size} height={size} alt={phase.toLowerCase()} />;
return (
<div className="relative justify-center pt-1 pr-2">
<div className="relative justify-center pr-2 pt-1">
{phaseIcon}
<Image
className="absolute -right-[2px] -bottom-[4px] rounded-[14px] border-2 border-black"
className="absolute -bottom-[4px] -right-[2px] rounded-[14px] border-2 border-black"
src={networkSvgPaths[bridgeDirection]}
width={20}
height={20}

View File

@@ -0,0 +1,62 @@
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
import { CCTPBridgePhase, WithdrawalPhase } from 'apps/bridge/src/utils/transactions/phase';
import { memo } from 'react';
const PHASE_MAP: Record<WithdrawalPhase | CCTPBridgePhase, number> = {
PROPOSING_ON_CHAIN: 1,
PROVE: 2,
PROVE_TX_PENDING: 2,
PROVE_TX_FAILURE: 2,
CHALLENGE_WINDOW: 2,
FINALIZE: 3,
FINALIZE_TX_PENDING: 3,
FINALIZE_TX_FAILURE: 3,
FUNDS_WITHDRAWN: 4,
INITIATE_CCTP_BRIDGE_PENDING: 1,
INITIATE_CCTP_BRIDGE_FAILED: 1,
FINALIZE_CCTP_BRIDGE: 2,
FINALIZE_CCTP_BRIDGE_PENDING: 2,
FINALIZE_CCTP_BRIDGE_FAILED: 2,
CCTP_BRIDGE_COMPLETE: 3,
};
function generatePhaseIndicator(
phase: WithdrawalPhase | CCTPBridgePhase,
protocol?: BridgeProtocol,
): JSX.Element[] {
const rv: JSX.Element[] = [];
const totalPhases = protocol === 'CCTP' ? 3 : 4;
const indicatorWidth = protocol === 'CCTP' ? `w-[calc(8rem/3)]` : `w-[calc(8rem/4)]`;
for (let i = 0; i < PHASE_MAP[phase]; i += 1) {
rv.push(<div className={`border-gray-400 border-t-4 ${indicatorWidth}`} key={`fill-${i}`} />);
}
for (let i = 0; i < totalPhases - PHASE_MAP[phase]; i += 1) {
rv.push(
<div
className={`border-gray-400 border-t-4 text-cds-background-gray-60 ${indicatorWidth}`}
key={`back-${i}`}
/>,
);
}
return rv;
}
type BridgePhaseIndicatorProps = {
phase: WithdrawalPhase | CCTPBridgePhase;
protocol?: BridgeProtocol;
};
// Component to generate the phase indicator for any multi-step bridge transaction.
// This currently includes OP bridge withdrawals and CCTP bridge transactions in either direction.
export const BridgePhaseIndicator = memo(function BridgePhaseIndicator({
phase,
protocol,
}: BridgePhaseIndicatorProps) {
return (
<div className="flex h-6 grow flex-row items-center gap-1">
{generatePhaseIndicator(phase, protocol)}
</div>
);
});

View File

@@ -0,0 +1,124 @@
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
import { BridgePhaseIndicator } from 'apps/bridge/src/components/Transactions/BridgePhaseIndicator';
import { CCTPBridgeStatus } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeStatus';
import { cctpBridgePhaseText } from 'apps/bridge/src/constants/phaseText';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
import { useCCTPBridgeStatus } from 'apps/bridge/src/utils/hooks/useCCTPBridgeStatus';
import { useGetUSDAmount } from 'apps/bridge/src/utils/hooks/useGetUSDAmount';
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
import { formatTimestamp } from 'apps/bridge/src/utils/transactions/formatBlockTimestamp';
import {
blockExplorerUrlForL1Transaction,
blockExplorerUrlForL2Transaction,
} from 'apps/bridge/src/utils/url/blockExplorer';
import { formatUnits } from 'viem';
import { Dispatch, SetStateAction, memo } from 'react';
type CCTPBridgeRowProps = {
transaction: BridgeTransaction;
bridgeDirection: 'deposit' | 'withdraw';
onOpenFinalizeCCTPBridgeModal: () => void;
onCloseFinalizeCCTPBridgeModal: () => void;
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
};
// Transactions table row component for Bridges made using Circle's CCTP.
export const CCTPBridgeRow = memo(function CCTPBridgeRow({
transaction,
bridgeDirection,
onOpenFinalizeCCTPBridgeModal,
onCloseFinalizeCCTPBridgeModal,
setModalFinalizeCCTPTxHash,
}: CCTPBridgeRowProps) {
const { date, shortDate, time } = formatTimestamp(transaction.blockTimestamp);
const { status, setStatus, message, attestation } = useCCTPBridgeStatus({
initiateTxHash: transaction.hash,
bridgeDirection,
});
const explorerURL =
bridgeDirection === 'deposit'
? blockExplorerUrlForL1Transaction(transaction.hash)
: blockExplorerUrlForL2Transaction(transaction.hash);
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
const bridgeAmount = formatUnits(BigInt(transaction.amount), transaction.assetDecimals ?? 18);
const amountFiat = useGetUSDAmount(transaction.priceApiId, bridgeAmount);
return (
<tr className="mb-4 grid grid-cols-2 grid-rows-2 md:table-row">
<td className="hidden md:table-cell">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection={bridgeDirection} phase={status} protocol="CCTP" />
<div className="flex flex-col">
<p>{date ?? ''}</p>
<p>{time ?? ''}</p>
</div>
</div>
</td>
{/* mobile design - left column */}
<td className="md:hidden">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection={bridgeDirection} phase={status} protocol="CCTP" />
<div className="flex flex-col">
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="whitespace-nowrap font-sans text-sm"
>
{abridgedHash}
</a>
<div className="flex flex-row text-cds-background-gray-60">
{transaction.type}
<h3>&#8226;</h3>
<p>{shortDate ?? ''}</p>
</div>
</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
{transaction.type}
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="text-cds-background-gray-60 underline"
>
{abridgedHash}
</a>
</div>
</td>
{/* mobile design - right column */}
<td className="md:hidden">
<div className="flex flex-col items-end">
<div>{`${bridgeAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div>{`${bridgeAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<BridgePhaseIndicator phase={status} protocol="CCTP" />
<div className="text-cds-background-gray-60">{cctpBridgePhaseText[status]}</div>
</div>
</td>
<CCTPBridgeStatus
phase={status}
message={message}
attestation={attestation}
bridgeDirection={bridgeDirection}
setStatus={setStatus}
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
/>
</tr>
);
});

View File

@@ -0,0 +1,56 @@
import { Dispatch, SetStateAction, memo } from 'react';
import { PendingButton } from 'apps/bridge/src/components/Transactions/PendingButton';
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
import { FinalizeCCTPBridgeButton } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeButton';
type CCTPBridgeStatusProps = {
phase: CCTPBridgePhase;
message?: `0x${string}`;
attestation?: `0x${string}`;
bridgeDirection: 'deposit' | 'withdraw';
setStatus: (status: CCTPBridgePhase) => void;
onOpenFinalizeCCTPBridgeModal: () => void;
onCloseFinalizeCCTPBridgeModal: () => void;
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
};
export const CCTPBridgeStatus = memo(function CCTPBridgeStatus({
phase,
message,
attestation,
bridgeDirection,
setStatus,
onOpenFinalizeCCTPBridgeModal,
onCloseFinalizeCCTPBridgeModal,
setModalFinalizeCCTPTxHash,
}: CCTPBridgeStatusProps) {
const isPending =
phase === 'INITIATE_CCTP_BRIDGE_PENDING' || phase === 'FINALIZE_CCTP_BRIDGE_PENDING';
// If the finalize tx failed we can just try again
const isReadyToFinazlie =
phase === 'FINALIZE_CCTP_BRIDGE' || phase === 'FINALIZE_CCTP_BRIDGE_FAILED';
const isFailed = phase === 'INITIATE_CCTP_BRIDGE_FAILED';
// Return pending button if initiate or finalize tx is pending
if (isPending) return <PendingButton />;
// Return 'Ready to finalize' if finalize tx is ready to be sent
if (isReadyToFinazlie)
return (
<FinalizeCCTPBridgeButton
message={message}
attestation={attestation}
bridgeDirection={bridgeDirection}
setStatus={setStatus}
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
/>
);
// Return 'Failed' if initiate tx failed
if (isFailed) return 'Failed';
// Otherwise the bridge is complete
return <span className="text-cds-background-gray-60">Complete</span>;
});

View File

@@ -0,0 +1,108 @@
import { Dispatch, SetStateAction, memo, useCallback } from 'react';
import { usePrepareFinalizeCCTPBridge } from 'apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge';
import { useIsPermittedToBridge } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridge';
import { useContractWrite, useNetwork, usePublicClient, useSwitchNetwork } from 'wagmi';
import getConfig from 'next/config';
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
const { publicRuntimeConfig } = getConfig();
const l1ChainID = parseInt(publicRuntimeConfig.l1ChainID);
const l2ChainID = parseInt(publicRuntimeConfig.l2ChainID);
type FinalizeCCTPBridgeButtonProps = {
message?: `0x${string}`;
attestation?: `0x${string}`;
bridgeDirection: 'deposit' | 'withdraw';
setStatus: (status: CCTPBridgePhase) => void;
onOpenFinalizeCCTPBridgeModal: () => void;
onCloseFinalizeCCTPBridgeModal: () => void;
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
};
export const FinalizeCCTPBridgeButton = memo(function FinalizeCCTPBridgeButton({
message,
attestation,
bridgeDirection,
setStatus,
onOpenFinalizeCCTPBridgeModal,
onCloseFinalizeCCTPBridgeModal,
setModalFinalizeCCTPTxHash,
}: FinalizeCCTPBridgeButtonProps) {
const { chain } = useNetwork();
const { switchNetwork } = useSwitchNetwork();
const isPermittedToBridge = useIsPermittedToBridge();
const publicClient = usePublicClient({
chainId: bridgeDirection === 'deposit' ? l2ChainID : l1ChainID,
});
const chainEnv = useChainEnv();
const isMainnet = chainEnv === 'mainnet';
const includeTosVersionByte = isMainnet;
const finalizeCCTPBridgeConfig = usePrepareFinalizeCCTPBridge({
isPermittedToBridge,
message,
attestation,
bridgeDirection,
includeTosVersionByte,
});
const { writeAsync: finalizeCCTPBridge } = useContractWrite(finalizeCCTPBridgeConfig);
const handleSwitchToCorrectNetwork = useCallback(() => {
switchNetwork?.(bridgeDirection === 'deposit' ? l2ChainID : l1ChainID);
}, [bridgeDirection, switchNetwork]);
const handleFinalizeCCTPBridge = useCallback(() => {
setModalFinalizeCCTPTxHash(undefined);
onOpenFinalizeCCTPBridgeModal();
void (async () => {
try {
if (isPermittedToBridge) {
const finalizeResult = await finalizeCCTPBridge?.();
setStatus('FINALIZE_CCTP_BRIDGE_PENDING');
if (finalizeResult?.hash) {
const finalizeTxHash = finalizeResult.hash;
setModalFinalizeCCTPTxHash(finalizeTxHash);
const finalizeTxReceipt = await publicClient.waitForTransactionReceipt({
hash: finalizeResult.hash,
});
setStatus(
finalizeTxReceipt?.status === 'success'
? 'CCTP_BRIDGE_COMPLETE'
: 'FINALIZE_CCTP_BRIDGE_FAILED',
);
}
} else {
onCloseFinalizeCCTPBridgeModal();
}
} catch {
setStatus('FINALIZE_CCTP_BRIDGE_FAILED');
onCloseFinalizeCCTPBridgeModal();
}
})();
}, [
finalizeCCTPBridge,
isPermittedToBridge,
onCloseFinalizeCCTPBridgeModal,
onOpenFinalizeCCTPBridgeModal,
publicClient,
setModalFinalizeCCTPTxHash,
setStatus,
]);
const isOnCorrectNetwork =
(bridgeDirection === 'deposit' && chain?.id === l2ChainID) ||
(bridgeDirection === 'withdraw' && chain?.id === l1ChainID);
return (
<button
type="button"
onClick={isOnCorrectNetwork ? handleFinalizeCCTPBridge : handleSwitchToCorrectNetwork}
className="w-32 bg-white py-2 font-sans text-sm text-black"
>
{isOnCorrectNetwork
? 'Ready to complete'
: `Switch to ${bridgeDirection === 'deposit' ? 'L2' : 'L1'}`}
</button>
);
});

View File

@@ -0,0 +1,108 @@
import { memo } from 'react';
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
import { useWaitForTransaction } from 'wagmi';
import {
blockExplorerUrlForL1Transaction,
blockExplorerUrlForL2Transaction,
} from 'apps/bridge/src/utils/url/blockExplorer';
import { CCTPBridgeProgressBar } from 'apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar';
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const l2ChainID = parseInt(publicRuntimeConfig.l2ChainID);
type FinalizeCCTPBridgeModalProps = {
isOpen: boolean;
onClose: () => void;
finalizeTxHash?: `0x${string}`;
};
type State = 'FINALIZE_NOT_SUBMITTED' | 'FINALIZE_SUBMITTED' | 'FINALIZE_RECEIVED';
const Titles = {
FINALIZE_NOT_SUBMITTED: 'CONFIRM COMPLETION IN WALLET',
FINALIZE_SUBMITTED: 'COMPLETING BRIDGE',
FINALIZE_RECEIVED: 'COMPLETING BRIDGE',
};
const Icons = {
FINALIZE_NOT_SUBMITTED: 'confirm_deposit',
FINALIZE_SUBMITTED: 'confirm_deposit',
FINALIZE_RECEIVED: '',
};
const ModalContents = {
FINALIZE_NOT_SUBMITTED: 'Completion will begin after confirmation',
FINALIZE_SUBMITTED: 'Waiting for confirmations...',
FINALIZE_RECEIVED: <CCTPBridgeProgressBar status="VERIFIED" />,
};
function getState(
finalizeTxHash?: string,
isFinalizationLoading?: boolean,
isFinalizationSuccess?: boolean,
): State {
if (!finalizeTxHash) {
return 'FINALIZE_NOT_SUBMITTED';
}
if (isFinalizationSuccess) {
return 'FINALIZE_RECEIVED';
}
if (isFinalizationLoading) {
return 'FINALIZE_SUBMITTED';
}
return 'FINALIZE_NOT_SUBMITTED';
}
export const FinalizeCCTPBridgeModal = memo(function FinalizeCCTPBridgeModal({
isOpen,
onClose,
finalizeTxHash,
}: FinalizeCCTPBridgeModalProps) {
const { isLoading: isFinalizationLoading, isSuccess: isFinalizationSuccess } =
useWaitForTransaction({
hash: finalizeTxHash,
});
// We don't have access to wheter this is a deposit or withdrawal here, so just
// see if we can get a receipt for the transaction on L2. If we can, it's a deposit,
// because deposits are finalized with an L2 tx.
const { isSuccess: isDeposit, isLoading } = useWaitForTransaction({
hash: finalizeTxHash,
chainId: l2ChainID,
});
const bridgeDirection = isDeposit ? 'deposit' : 'withdraw';
const state = getState(finalizeTxHash, isFinalizationLoading, isFinalizationSuccess);
const explorerURL =
bridgeDirection === 'withdraw'
? blockExplorerUrlForL1Transaction(finalizeTxHash ?? '')
: blockExplorerUrlForL2Transaction(finalizeTxHash ?? '');
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={Titles[state]}
content={ModalContents[state]}
icon={Icons[state]}
footer={
finalizeTxHash &&
!isLoading && (
<div className="text-center">
<a
className="text-md font-sans text-cds-primary"
href={explorerURL}
target="_blank"
rel="noreferrer noopener"
>
{`View on ${bridgeDirection === 'withdraw' ? 'Etherscan' : 'Basescan'}`}
</a>
</div>
)
}
/>
);
});

View File

@@ -1,176 +1,32 @@
import { memo } from 'react';
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
import { depositPhaseStatusText, depositPhaseText } from 'apps/bridge/src/constants/phaseText';
import { Dispatch, SetStateAction, memo } from 'react';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
import type { DepositPhase } from 'apps/bridge/src/utils/transactions/phase';
import {
blockExplorerUrlForL1Transaction,
blockExplorerUrlForL2Transaction,
} from 'apps/bridge/src/utils/url/blockExplorer';
import { formatUnits } from 'viem';
import getConfig from 'next/config';
import { useWaitForTransaction } from 'wagmi';
const { publicRuntimeConfig } = getConfig();
import { OPDepositRow } from 'apps/bridge/src/components/Transactions/DepositRow/OPDepositRow';
import { CCTPBridgeRow } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow';
type DepositRowProps = {
transaction: BridgeTransaction;
onOpenFinalizeCCTPBridgeModal: () => void;
onCloseFinalizeCCTPBridgeModal: () => void;
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
};
export const DepositRow = memo(function WithdrawalRow({ transaction }: DepositRowProps) {
const date = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
: undefined;
const dateMonthDayOnly = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
: undefined;
const time = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleTimeString('en-US', {
timeStyle: 'short',
})
: undefined;
const depositAmount = formatUnits(
BigInt(transaction.amount),
// TODO: get decimals from asset list
transaction.assetSymbol === 'USDC' ? 6 : 18,
);
const conversionRateData = useConversionRate({
asset: transaction.priceApiId,
});
const amountFiat =
conversionRateData && depositAmount
? usdFormatter(conversionRateData * +depositAmount)
: '$0.00';
const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } = useWaitForTransaction({
chainId: parseInt(publicRuntimeConfig.l1ChainID),
hash: transaction.hash,
});
const explorerURL =
transaction.type === 'Deposit'
? blockExplorerUrlForL1Transaction(transaction.hash)
: blockExplorerUrlForL2Transaction(transaction.hash);
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
const pendingButton = (
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
<svg
aria-hidden="true"
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="grey"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="black"
/>
</svg>
Pending
</button>
);
const PHASE_TO_STATUS = {
DEPOSIT_TX_PENDING: pendingButton,
DEPOSIT_TX_FAILURE: depositPhaseStatusText.DEPOSIT_TX_FAILURE,
FUNDS_DEPOSITED: depositPhaseStatusText.FUNDS_DEPOSITED,
};
let depositStatus: DepositPhase;
if (isDepositLoading) {
depositStatus = 'DEPOSIT_TX_PENDING';
} else if (isDepositSuccess) {
depositStatus = 'FUNDS_DEPOSITED';
} else {
depositStatus = 'DEPOSIT_TX_FAILURE';
export const DepositRow = memo(function DepositRow({
transaction,
onOpenFinalizeCCTPBridgeModal,
onCloseFinalizeCCTPBridgeModal,
setModalFinalizeCCTPTxHash,
}: DepositRowProps) {
if (transaction.protocol === 'CCTP') {
return (
<CCTPBridgeRow
transaction={transaction}
bridgeDirection="deposit"
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
/>
);
}
return (
<tr className="mb-4 grid grid-cols-2 grid-rows-2 md:table-row">
<td className="hidden md:table-cell">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
<div className="flex flex-col">
<p>{date ?? ''}</p>
<p>{time ?? ''}</p>
</div>
</div>
</td>
{/* mobile design - left column */}
<td className="md:hidden">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
<div className="flex flex-col">
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="whitespace-nowrap font-sans text-sm"
>
{abridgedHash}
</a>
<div className="flex flex-row text-cds-background-gray-60">
{transaction.type}
<h3>&#8226;</h3>
<p>{dateMonthDayOnly ?? ''}</p>
</div>
</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
{transaction.type}
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="text-cds-background-gray-60 underline"
>
{abridgedHash}
</a>
</div>
</td>
{/* mobile design - right column */}
<td className="md:table-cell md:hidden">
<div className="flex flex-col items-end">
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div className="flex h-6 grow flex-row items-center gap-2">
<div className="border-gray-400 w-36 border-t-4" />
</div>
<div className="text-cds-background-gray-60">{depositPhaseText[depositStatus]}</div>
</div>
</td>
<td className="hidden text-end text-cds-background-gray-60 md:table-cell md:text-start">
{PHASE_TO_STATUS[depositStatus]}
</td>
</tr>
);
return <OPDepositRow transaction={transaction} />;
});

View File

@@ -0,0 +1,130 @@
import { memo } from 'react';
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
import { depositPhaseStatusText, depositPhaseText } from 'apps/bridge/src/constants/phaseText';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
import type { DepositPhase } from 'apps/bridge/src/utils/transactions/phase';
import {
blockExplorerUrlForL1Transaction,
blockExplorerUrlForL2Transaction,
} from 'apps/bridge/src/utils/url/blockExplorer';
import getConfig from 'next/config';
import { formatUnits } from 'viem';
import { useWaitForTransaction } from 'wagmi';
import { formatTimestamp } from 'apps/bridge/src/utils/transactions/formatBlockTimestamp';
import { useGetUSDAmount } from 'apps/bridge/src/utils/hooks/useGetUSDAmount';
import { PendingButton } from 'apps/bridge/src/components/Transactions/PendingButton';
const { publicRuntimeConfig } = getConfig();
const PHASE_TO_STATUS: Record<Exclude<DepositPhase, 'DEPOSIT_TX_PENDING'>, string> = {
DEPOSIT_TX_FAILURE: depositPhaseStatusText.DEPOSIT_TX_FAILURE,
FUNDS_DEPOSITED: depositPhaseStatusText.FUNDS_DEPOSITED,
};
type OPDepositRowProps = {
transaction: BridgeTransaction;
};
// Transactions table row component for deposits made using the OP bridge.
export const OPDepositRow = memo(function OPDepositRow({ transaction }: OPDepositRowProps) {
const { date, shortDate: dateMonthDayOnly, time } = formatTimestamp(transaction.blockTimestamp);
const depositAmount = formatUnits(BigInt(transaction.amount), transaction.assetDecimals ?? 18);
const amountFiat = useGetUSDAmount(transaction.priceApiId, depositAmount);
const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } = useWaitForTransaction({
chainId: parseInt(publicRuntimeConfig.l1ChainID),
hash: transaction.hash,
});
const explorerURL =
transaction.type === 'Deposit'
? blockExplorerUrlForL1Transaction(transaction.hash)
: blockExplorerUrlForL2Transaction(transaction.hash);
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
let depositStatus: DepositPhase;
if (isDepositLoading) {
depositStatus = 'DEPOSIT_TX_PENDING';
} else if (isDepositSuccess) {
depositStatus = 'FUNDS_DEPOSITED';
} else {
depositStatus = 'DEPOSIT_TX_FAILURE';
}
return (
<tr className="mb-4 grid grid-cols-2 grid-rows-2 md:table-row">
<td className="hidden md:table-cell">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
<div className="flex flex-col">
<p>{date ?? ''}</p>
<p>{time ?? ''}</p>
</div>
</div>
</td>
{/* mobile design - left column */}
<td className="md:hidden">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
<div className="flex flex-col">
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="whitespace-nowrap font-sans text-sm"
>
{abridgedHash}
</a>
<div className="flex flex-row text-cds-background-gray-60">
{transaction.type}
<h3>&#8226;</h3>
<p>{dateMonthDayOnly ?? ''}</p>
</div>
</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
{transaction.type}
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="text-cds-background-gray-60 underline"
>
{abridgedHash}
</a>
</div>
</td>
{/* mobile design - right column */}
<td className="md:table-cell md:hidden">
<div className="flex flex-col items-end">
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div className="flex h-6 grow flex-row items-center gap-2">
<div className="border-gray-400 w-36 border-t-4" />
</div>
<div className="text-cds-background-gray-60">{depositPhaseText[depositStatus]}</div>
</div>
</td>
<td className="hidden text-end text-cds-background-gray-60 md:table-cell md:text-start">
{depositStatus === 'DEPOSIT_TX_PENDING' ? (
<PendingButton />
) : (
PHASE_TO_STATUS[depositStatus]
)}
</td>
</tr>
);
});

View File

@@ -0,0 +1,25 @@
import { memo } from 'react';
export const PendingButton = memo(function PendingButton() {
return (
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
<svg
aria-hidden="true"
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="grey"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="black"
/>
</svg>
Pending
</button>
);
});

View File

@@ -0,0 +1,213 @@
import { Dispatch, memo, SetStateAction, useState } from 'react';
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
import {
withdrawalPhaseStatusText,
withdrawalPhaseText,
} from 'apps/bridge/src/constants/phaseText';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
import { useWithdrawalStatus } from 'apps/bridge/src/utils/hooks/useWithdrawalStatus';
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
import {
blockExplorerUrlForL1Transaction,
blockExplorerUrlForL2Transaction,
} from 'apps/bridge/src/utils/url/blockExplorer';
import { formatUnits } from 'viem';
import { FinalizeWithdrawalButton } from './FinalizeWithdrawalButton';
import { ProveWithdrawalButton } from './ProveWithdrawalButton';
import { BridgePhaseIndicator } from 'apps/bridge/src/components/Transactions/BridgePhaseIndicator';
type OPWithdrawalRowProps = {
transaction: BridgeTransaction;
blockNumberOfLatestL2OutputProposal?: bigint;
onOpenProveWithdrawalModal: () => void;
onCloseProveWithdrawalModal: () => void;
onOpenFinalizeWithdrawalModal: () => void;
onCloseFinalizeWithdrawalModal: () => void;
setModalProveTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
setModalFinalizeTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
};
export const OPWithdrawalRow = memo(function OPWithdrawalRow({
transaction,
blockNumberOfLatestL2OutputProposal,
onOpenProveWithdrawalModal,
onCloseProveWithdrawalModal,
onOpenFinalizeWithdrawalModal,
onCloseFinalizeWithdrawalModal,
setModalProveTxHash,
setModalFinalizeTxHash,
}: OPWithdrawalRowProps) {
const isERC20Withdrawal = transaction.assetSymbol !== 'ETH';
const [proveTxHash, setProveTxHash] = useState<`0x${string}` | undefined>(undefined);
const [finalizeTxHash, setFinalizeTxHash] = useState<`0x${string}` | undefined>(undefined);
const { status: withdrawalStatus, challengeWindowEndTime } = useWithdrawalStatus({
initializeTxHash: transaction.hash,
blockNumberOfLatestL2OutputProposal,
isERC20Withdrawal,
proveTxHash,
finalizeTxHash,
});
const date = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
: undefined;
const dateMonthDayOnly = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
: undefined;
const time = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleTimeString('en-US', {
timeStyle: 'short',
})
: undefined;
const withdrawalAmount = formatUnits(
BigInt(transaction.amount),
// TODO: get decimals from asset list
transaction.assetSymbol === 'USDbC' ? 6 : 18,
);
const conversionRateData = useConversionRate({
asset: transaction.priceApiId,
});
const amountFiat =
conversionRateData && withdrawalAmount
? usdFormatter(conversionRateData * +withdrawalAmount)
: '$0.00';
const explorerURL =
transaction.type === 'Deposit'
? blockExplorerUrlForL1Transaction(transaction.hash)
: blockExplorerUrlForL2Transaction(transaction.hash);
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
const pendingButton = (
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
<svg
aria-hidden="true"
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="grey"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="black"
/>
</svg>
Pending
</button>
);
const PHASE_TO_STATUS = {
PROPOSING_ON_CHAIN: withdrawalPhaseStatusText.PROPOSING_ON_CHAIN,
PROVE: (
<ProveWithdrawalButton
txHash={transaction.hash}
isERC20Withdrawal={isERC20Withdrawal}
onOpenProveWithdrawalModal={onOpenProveWithdrawalModal}
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
setProveTxHash={setProveTxHash}
setModalProveTxHash={setModalProveTxHash}
blockNumberOfLatestL2OutputProposal={blockNumberOfLatestL2OutputProposal}
/>
),
PROVE_TX_PENDING: pendingButton,
PROVE_TX_FAILURE: withdrawalPhaseStatusText.PROVE_TX_FAILURE,
CHALLENGE_WINDOW: withdrawalPhaseStatusText.CHALLENGE_WINDOW(Number(challengeWindowEndTime)),
FINALIZE: (
<FinalizeWithdrawalButton
txHash={transaction.hash}
isERC20Withdrawal={isERC20Withdrawal}
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
setFinalizeTxHash={setFinalizeTxHash}
setModalFinalizeTxHash={setModalFinalizeTxHash}
/>
),
FINALIZE_TX_PENDING: pendingButton,
FINALIZE_TX_FAILURE: withdrawalPhaseStatusText.FINALIZE_TX_FAILURE,
FUNDS_WITHDRAWN: withdrawalPhaseStatusText.FUNDS_WITHDRAWN,
};
return (
<tr className="mb-4 grid grid-cols-3 grid-rows-3 md:table-row">
<td className="hidden md:table-cell">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
<div className="flex flex-col">
<p>{date ?? ''}</p>
<p>{time ?? ''}</p>
</div>
</div>
</td>
{/* mobile design - left column */}
<td className="md:hidden">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
<div className="flex flex-col">
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="whitespace-nowrap font-sans text-sm"
>
{abridgedHash}
</a>
<div className="md:text-md flex flex-row text-xs text-cds-background-gray-60">
{transaction.type}
<h3>&#8226;</h3>
<p>{dateMonthDayOnly ?? ''}</p>
</div>
</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
{transaction.type}
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="text-cds-background-gray-60 underline"
>
{abridgedHash}
</a>
</div>
</td>
{/* mobile design - right column */}
<td className="table-cell md:hidden">
<div className="flex flex-col items-end">
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<BridgePhaseIndicator phase={withdrawalStatus} />
<div className="text-cds-background-gray-60">{withdrawalPhaseText[withdrawalStatus]}</div>
</div>
</td>
<td className="table-cell text-end text-cds-background-gray-60 md:text-start">
{PHASE_TO_STATUS[withdrawalStatus]}
</td>
</tr>
);
});

View File

@@ -1,23 +1,7 @@
import { Dispatch, memo, SetStateAction, useState } from 'react';
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
import {
withdrawalPhaseStatusText,
withdrawalPhaseText,
} from 'apps/bridge/src/constants/phaseText';
import { Dispatch, SetStateAction, memo } from 'react';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
import { useWithdrawalStatus } from 'apps/bridge/src/utils/hooks/useWithdrawalStatus';
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
import type { WithdrawalPhase } from 'apps/bridge/src/utils/transactions/phase';
import {
blockExplorerUrlForL1Transaction,
blockExplorerUrlForL2Transaction,
} from 'apps/bridge/src/utils/url/blockExplorer';
import { formatUnits } from 'viem';
import { FinalizeWithdrawalButton } from './FinalizeWithdrawalButton';
import { ProveWithdrawalButton } from './ProveWithdrawalButton';
import { CCTPBridgeRow } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow';
import { OPWithdrawalRow } from 'apps/bridge/src/components/Transactions/WithdrawalRow/OPWithdrawalRow';
type WithdrawalRowProps = {
transaction: BridgeTransaction;
@@ -26,8 +10,11 @@ type WithdrawalRowProps = {
onCloseProveWithdrawalModal: () => void;
onOpenFinalizeWithdrawalModal: () => void;
onCloseFinalizeWithdrawalModal: () => void;
onOpenFinalizeCCTPBridgeModal: () => void;
onCloseFinalizeCCTPBridgeModal: () => void;
setModalProveTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
setModalFinalizeTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
setModalFinalizeOPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
};
export const WithdrawalRow = memo(function WithdrawalRow({
@@ -37,206 +24,34 @@ export const WithdrawalRow = memo(function WithdrawalRow({
onCloseProveWithdrawalModal,
onOpenFinalizeWithdrawalModal,
onCloseFinalizeWithdrawalModal,
onOpenFinalizeCCTPBridgeModal,
onCloseFinalizeCCTPBridgeModal,
setModalProveTxHash,
setModalFinalizeTxHash,
setModalFinalizeOPTxHash,
setModalFinalizeCCTPTxHash,
}: WithdrawalRowProps) {
const isERC20Withdrawal = transaction.assetSymbol !== 'ETH';
const [proveTxHash, setProveTxHash] = useState<`0x${string}` | undefined>(undefined);
const [finalizeTxHash, setFinalizeTxHash] = useState<`0x${string}` | undefined>(undefined);
const { status: withdrawalStatus, challengeWindowEndTime } = useWithdrawalStatus({
initializeTxHash: transaction.hash,
blockNumberOfLatestL2OutputProposal,
isERC20Withdrawal,
proveTxHash,
finalizeTxHash,
});
const date = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
: undefined;
const dateMonthDayOnly = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
: undefined;
const time = transaction.blockTimestamp
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleTimeString('en-US', {
timeStyle: 'short',
})
: undefined;
const withdrawalAmount = formatUnits(
BigInt(transaction.amount),
// TODO: get decimals from asset list
transaction.assetSymbol === 'USDbC' ? 6 : 18,
);
const conversionRateData = useConversionRate({
asset: transaction.priceApiId,
});
const amountFiat =
conversionRateData && withdrawalAmount
? usdFormatter(conversionRateData * +withdrawalAmount)
: '$0.00';
const explorerURL =
transaction.type === 'Deposit'
? blockExplorerUrlForL1Transaction(transaction.hash)
: blockExplorerUrlForL2Transaction(transaction.hash);
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
const generatePhaseIndicator = (phase: WithdrawalPhase): JSX.Element[] => {
const PHASE_MAP = {
PROPOSING_ON_CHAIN: 1,
PROVE: 2,
PROVE_TX_PENDING: 2,
PROVE_TX_FAILURE: 2,
CHALLENGE_WINDOW: 2,
FINALIZE: 3,
FINALIZE_TX_PENDING: 3,
FINALIZE_TX_FAILURE: 3,
FUNDS_WITHDRAWN: 4,
};
const rv: JSX.Element[] = [];
for (let i = 0; i < PHASE_MAP[phase]; i += 1) {
rv.push(<div className="border-gray-400 w-8 border-t-4" key={`fill-${i}`} />);
}
for (let i = 0; i < 4 - PHASE_MAP[phase]; i += 1) {
rv.push(
<div
className="border-gray-400 w-8 border-t-4 text-cds-background-gray-60"
key={`back-${i}`}
/>,
);
}
return rv;
};
const pendingButton = (
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
<svg
aria-hidden="true"
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="grey"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="black"
/>
</svg>
Pending
</button>
);
const PHASE_TO_STATUS = {
PROPOSING_ON_CHAIN: withdrawalPhaseStatusText.PROPOSING_ON_CHAIN,
PROVE: (
<ProveWithdrawalButton
txHash={transaction.hash}
isERC20Withdrawal={isERC20Withdrawal}
onOpenProveWithdrawalModal={onOpenProveWithdrawalModal}
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
setProveTxHash={setProveTxHash}
setModalProveTxHash={setModalProveTxHash}
blockNumberOfLatestL2OutputProposal={blockNumberOfLatestL2OutputProposal}
if (transaction.protocol === 'CCTP') {
return (
<CCTPBridgeRow
transaction={transaction}
bridgeDirection="withdraw"
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
/>
),
PROVE_TX_PENDING: pendingButton,
PROVE_TX_FAILURE: withdrawalPhaseStatusText.PROVE_TX_FAILURE,
CHALLENGE_WINDOW: withdrawalPhaseStatusText.CHALLENGE_WINDOW(Number(challengeWindowEndTime)),
FINALIZE: (
<FinalizeWithdrawalButton
txHash={transaction.hash}
isERC20Withdrawal={isERC20Withdrawal}
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
setFinalizeTxHash={setFinalizeTxHash}
setModalFinalizeTxHash={setModalFinalizeTxHash}
/>
),
FINALIZE_TX_PENDING: pendingButton,
FINALIZE_TX_FAILURE: withdrawalPhaseStatusText.FINALIZE_TX_FAILURE,
FUNDS_WITHDRAWN: withdrawalPhaseStatusText.FUNDS_WITHDRAWN,
};
);
}
return (
<tr className="mb-4 grid grid-cols-3 grid-rows-3 md:table-row">
<td className="hidden md:table-cell">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
<div className="flex flex-col">
<p>{date ?? ''}</p>
<p>{time ?? ''}</p>
</div>
</div>
</td>
{/* mobile design - left column */}
<td className="md:hidden">
<div className="flex flex-row items-start gap-2">
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
<div className="flex flex-col">
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="whitespace-nowrap font-sans text-sm"
>
{abridgedHash}
</a>
<div className="md:text-md flex flex-row text-xs text-cds-background-gray-60">
{transaction.type}
<h3>&#8226;</h3>
<p>{dateMonthDayOnly ?? ''}</p>
</div>
</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
{transaction.type}
<a
target="_blank"
href={explorerURL}
rel="noreferrer noopener"
className="text-cds-background-gray-60 underline"
>
{abridgedHash}
</a>
</div>
</td>
{/* mobile design - right column */}
<td className="table-cell md:hidden">
<div className="flex flex-col items-end">
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
</div>
</td>
<td className="hidden md:table-cell">
<div className="flex flex-col">
<div className="flex h-6 grow flex-row items-center gap-1">
{generatePhaseIndicator(withdrawalStatus)}
</div>
<div className="text-cds-background-gray-60">{withdrawalPhaseText[withdrawalStatus]}</div>
</div>
</td>
<td className="table-cell text-end text-cds-background-gray-60 md:text-start">
{PHASE_TO_STATUS[withdrawalStatus]}
</td>
</tr>
<OPWithdrawalRow
transaction={transaction}
blockNumberOfLatestL2OutputProposal={blockNumberOfLatestL2OutputProposal}
onOpenProveWithdrawalModal={onOpenProveWithdrawalModal}
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
setModalProveTxHash={setModalProveTxHash}
setModalFinalizeTxHash={setModalFinalizeOPTxHash}
/>
);
});

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BridgeInput } from 'apps/bridge/src/components/BridgeInput/BridgeInput';
import { BridgeToInput } from 'apps/bridge/src/components/BridgeToInput/BridgeToInput';
import { ConnectWalletButton } from 'apps/bridge/src/components/ConnectWalletButton/ConnectWalletButton';
@@ -7,7 +7,6 @@ import { BaseButton } from 'apps/bridge/src/components/SwitchNetworkButton/Switc
import { TransactionSummary } from 'apps/bridge/src/components/TransactionSummary/TransactionSummary';
import { WithdrawModal } from 'apps/bridge/src/components/WithdrawModal/WithdrawModal';
import { Asset } from 'apps/bridge/src/types/Asset';
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
import { useGetCode } from 'apps/bridge/src/utils/hooks/useGetCode';
@@ -18,30 +17,69 @@ import { usePrepareERC20WithdrawalTo } from 'apps/bridge/src/utils/hooks/usePrep
import { usePrepareETHWithdrawal } from 'apps/bridge/src/utils/hooks/usePrepareETHWithdrawal';
import { isAddress } from 'viem';
import getConfig from 'next/config';
import { useAccount, useBalance, useContractWrite } from 'wagmi';
import { useAccount, useBalance, useContractWrite, usePublicClient, useSwitchNetwork } from 'wagmi';
import { writeContract } from 'wagmi/actions';
import { useIsPermittedToBridgeTo } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridgeTo';
import { getL2NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL2NetworkForChainEnv';
import { getL1NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL1NetworkForChainEnv';
import { getWithdrawalAssetsForChainEnv } from 'apps/bridge/src/utils/assets/getWithdrawalAssetsForChainEnv';
import { usePrepareInitiateCCTPBridge } from 'apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge';
import { useIsContractApproved } from 'apps/bridge/src/utils/hooks/useIsContractApproved';
import { useApproveContract } from 'apps/bridge/src/utils/hooks/useApproveContract';
import { BridgeButton } from 'apps/bridge/src/components/BridgeButton/BridgeButton';
import { parseUnits } from 'viem';
const assetList = getAssetListForChainEnv();
const activeAssets = getWithdrawalAssetsForChainEnv();
const { publicRuntimeConfig } = getConfig();
const chainId = parseInt(publicRuntimeConfig.l2ChainID);
export function WithdrawContainer() {
const [withdrawAmount, setWithdrawAmount] = useState('');
const [L2TxHash, setL2TxHash] = useState('');
const [L2ApproveTxHash, setL2ApproveTxHash] = useState<`0x${string}` | undefined>(undefined);
const [L2WithdrawTxHash, setL2WithdrawTxHash] = useState<`0x${string}` | undefined>(undefined);
const [withdrawTo, setWithdrawTo] = useState('');
const [isApprovalTx, setIsApprovalTx] = useState(false);
const isWalletConnected = useIsWalletConnected();
const activeAssets = assetList.filter((asset) =>
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
);
const [selectedAsset, setSelectedAsset] = useState<Asset>(assetList[0]);
const [selectedAsset, setSelectedAsset] = useState<Asset>(activeAssets[0]);
const publicClient = usePublicClient({ chainId });
const { switchNetwork } = useSwitchNetwork();
useEffect(() => {
switchNetwork?.(chainId);
}, [switchNetwork]);
const { address } = useAccount();
const codeAtAddress = useGetCode(chainId, address);
const isSmartContractWallet = !!codeAtAddress && codeAtAddress !== '0x';
const erc20Spender = publicRuntimeConfig.l2CCTPTokenMessengerAddress;
const { data: readERC20Approval, error: readERC20ApprovalError } = useIsContractApproved({
contactAddress: selectedAsset.L2contract,
address,
spender: erc20Spender,
bridgeDirection: 'withdraw',
});
const readApprovalResult = useMemo(() => {
const withdrawAmountBN =
withdrawAmount === '' || Number.isNaN(Number(withdrawAmount))
? parseUnits('0', selectedAsset.decimals)
: parseUnits(withdrawAmount, selectedAsset.decimals);
return !readERC20ApprovalError && readERC20Approval && readERC20Approval >= withdrawAmountBN;
}, [withdrawAmount, selectedAsset.decimals, readERC20ApprovalError, readERC20Approval]);
// approve erc20
const approveConfig = useApproveContract({
contractAddress: selectedAsset.L2contract,
spender: erc20Spender,
approveAmount: withdrawAmount,
decimals: selectedAsset.decimals,
bridgeDirection: 'withdraw',
});
const { writeAsync: approveWrite } = useContractWrite(approveConfig);
const { data: L2Balance } = useBalance({
address,
token: selectedAsset.L2contract,
@@ -50,7 +88,7 @@ export function WithdrawContainer() {
const chainEnv = useChainEnv();
const isMainnet = chainEnv === 'mainnet';
const includeTosVersionByte = isMainnet && withdrawTo === '';
const includeTosVersionByte = isMainnet;
const isUserPermittedToBridge = useIsPermittedToBridge();
const isPermittedToBridgeTo = useIsPermittedToBridgeTo(withdrawTo as `0x${string}`);
const isPermittedToBridge = isSmartContractWallet
@@ -82,6 +120,18 @@ export function WithdrawContainer() {
});
const { writeAsync: withdraw } = useContractWrite(withdrawConfig);
// withdraw using CCTP (eg USDC)
const withdrawCCTPAssetConfig = usePrepareInitiateCCTPBridge({
mintRecipient: isSmartContractWallet ? (withdrawTo as `0x${string}`) : address,
asset: selectedAsset,
amount: withdrawAmount,
destinationDomain: parseInt(publicRuntimeConfig.l1CCTPDomain),
isPermittedToBridge,
includeTosVersionByte,
bridgeDirection: 'withdraw',
});
const { writeAsync: withdrawCCTPAssetWrite } = useContractWrite(withdrawCCTPAssetConfig);
const {
isOpen: isWithdrawModalOpen,
onOpen: onOpenWithdrawModal,
@@ -90,9 +140,58 @@ export function WithdrawContainer() {
const handleCloseWithdrawModal = useCallback(() => {
onCloseWithdrawModal();
setL2TxHash('');
setL2WithdrawTxHash(undefined);
setL2ApproveTxHash(undefined);
setIsApprovalTx(false);
}, [onCloseWithdrawModal]);
const initiateApproval = useCallback(() => {
void (async () => {
setIsApprovalTx(true);
onOpenWithdrawModal();
try {
const approveResult = await approveWrite?.();
if (approveResult?.hash) {
const approveTxHash: `0x${string}` = approveResult.hash;
setL2ApproveTxHash(approveTxHash);
// wait for confirmations
await publicClient.waitForTransactionReceipt({ hash: approveResult.hash });
// next, call the transfer function
setIsApprovalTx(false);
let withdrawMethod;
if (selectedAsset.protocol === 'CCTP') {
// because of how React works we need to use the writeContract wagmi/core action
// here (the hook still thinks the approval has not been set)
withdrawMethod = async () => await writeContract(withdrawCCTPAssetConfig);
} else {
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
}
const withdrawResult = await withdrawMethod?.();
if (withdrawResult?.hash) {
const withdrawTxHash = withdrawResult.hash;
setL2WithdrawTxHash(withdrawTxHash);
setWithdrawAmount('');
}
}
} catch (error) {
onCloseWithdrawModal();
}
})();
}, [
approveWrite,
isSmartContractWallet,
onCloseWithdrawModal,
onOpenWithdrawModal,
publicClient,
selectedAsset.protocol,
withdrawCCTPAssetConfig,
withdrawERC20,
withdrawERC20To,
]);
const initiateWithdrawal = useCallback(() => {
void (async () => {
onOpenWithdrawModal();
@@ -101,15 +200,19 @@ export function WithdrawContainer() {
if (isPermittedToBridge) {
let withdrawMethod;
if (selectedAsset.L1contract) {
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
if (selectedAsset.protocol === 'CCTP') {
withdrawMethod = withdrawCCTPAssetWrite;
} else {
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
}
} else {
withdrawMethod = withdraw;
}
const withdrawalResult = await withdrawMethod?.();
if (withdrawalResult?.hash) {
const withdrawalTxHsh = withdrawalResult.hash;
setL2TxHash(withdrawalTxHsh);
setWithdrawAmount('0');
setL2WithdrawTxHash(withdrawalTxHsh);
setWithdrawAmount('');
}
} else {
onCloseWithdrawModal();
@@ -122,6 +225,8 @@ export function WithdrawContainer() {
onOpenWithdrawModal,
isPermittedToBridge,
selectedAsset.L1contract,
selectedAsset.protocol,
withdrawCCTPAssetWrite,
isSmartContractWallet,
withdrawERC20To,
withdrawERC20,
@@ -136,6 +241,19 @@ export function WithdrawContainer() {
button = (
<ConnectWalletButton className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto" />
);
} else if (selectedAsset.protocol === 'CCTP' && !readApprovalResult) {
withdrawDisabled =
(isSmartContractWallet && !isAddress(withdrawTo ?? '')) || !isPermittedToBridge;
button = (
<BridgeButton
onClick={initiateApproval}
disabled={withdrawDisabled}
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
>
Approval
</BridgeButton>
);
} else {
withdrawDisabled =
parseFloat(withdrawAmount) <= 0 ||
@@ -162,7 +280,10 @@ export function WithdrawContainer() {
<WithdrawModal
isOpen={isWithdrawModalOpen}
onClose={handleCloseWithdrawModal}
L2TxHash={L2TxHash}
L2ApproveTxHash={L2ApproveTxHash}
L2WithdrawTxHash={L2WithdrawTxHash}
isApprovalTx={isApprovalTx}
protocol={selectedAsset.protocol}
/>
<BridgeInput
inputNetwork={getL2NetworkForChainEnv()}

View File

@@ -1,32 +1,112 @@
import { CCTPBridgeProgressBar } from 'apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar';
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
import { WithdrawProgressBar } from 'apps/bridge/src/components/WithdrawProgressBar/WithdrawProgressBar';
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
import getConfig from 'next/config';
import { ReactNode } from 'react';
import { useWaitForTransaction } from 'wagmi';
const { publicRuntimeConfig } = getConfig();
type WithdrawModalProps = {
isOpen: boolean;
onClose: () => void;
L2TxHash: string;
L2ApproveTxHash: `0x${string}` | undefined;
L2WithdrawTxHash: `0x${string}` | undefined;
isApprovalTx: boolean;
protocol: BridgeProtocol;
};
const Titles = {
WITHDRAW_NOT_STARTED: 'CONFIRM WITHDRAWAL IN WALLET',
WITHDRAW_STARTED: 'WITHDRAWAL IN PROGRESS',
type STATE =
| 'APPROVAL_LOADING'
| 'APPROVAL_CONFIRMED'
| 'APPROVAL_NOT_STARTED'
| 'WITHDRAWAL_LOADING'
| 'OP_WITHDRAWAL_STARTED'
| 'CCTP_WITHDRAWAL_STARTED'
| 'WITHDRAWAL_NOT_STARTED';
function getState(
isApprovalTx: boolean,
isApproveLoading: boolean,
isApproveSuccess: boolean,
isWithdrawalLoading: boolean,
isWithdrawalSuccess: boolean,
protocol: BridgeProtocol,
): STATE {
if (isApprovalTx && isApproveLoading) {
return 'APPROVAL_LOADING';
}
if (isApprovalTx && isApproveSuccess) {
return 'APPROVAL_CONFIRMED';
}
if (isApprovalTx) {
return 'APPROVAL_NOT_STARTED';
}
if (!isApprovalTx && isWithdrawalLoading) {
return 'WITHDRAWAL_LOADING';
}
if (!isApprovalTx && isWithdrawalSuccess) {
return protocol === 'CCTP' ? 'CCTP_WITHDRAWAL_STARTED' : 'OP_WITHDRAWAL_STARTED';
}
return 'WITHDRAWAL_NOT_STARTED';
}
const Titles: Record<STATE, string> = {
APPROVAL_NOT_STARTED: 'APPROVE IN YOUR WALLET',
APPROVAL_LOADING: 'APPROVING',
APPROVAL_CONFIRMED: 'APPROVED',
WITHDRAWAL_NOT_STARTED: 'CONFIRM WITHDRAWAL IN WALLET',
WITHDRAWAL_LOADING: 'CONFIRMING',
OP_WITHDRAWAL_STARTED: 'WITHDRAWAL IN PROGRESS',
CCTP_WITHDRAWAL_STARTED: 'WITHDRAWAL IN PROGRESS',
};
const Icons = {
WITHDRAW_NOT_STARTED: 'wallet',
WITHDRAW_STARTED: '',
const Icons: Record<STATE, string> = {
APPROVAL_NOT_STARTED: 'wallet',
APPROVAL_LOADING: 'wallet',
APPROVAL_CONFIRMED: 'confirm',
WITHDRAWAL_NOT_STARTED: 'wallet',
WITHDRAWAL_LOADING: 'wallet',
OP_WITHDRAWAL_STARTED: '',
CCTP_WITHDRAWAL_STARTED: '',
};
const ModalContents = {
WITHDRAW_NOT_STARTED: 'Withdrawal will begin after confirmation.',
WITHDRAW_STARTED: <WithdrawProgressBar status="REQUEST_SENT" />,
const ModalContents: Record<STATE, ReactNode> = {
APPROVAL_NOT_STARTED: 'Approval will initiate after confirmation.',
APPROVAL_LOADING: 'Waiting for confirmations...',
APPROVAL_CONFIRMED: 'Transaction confirmed.',
WITHDRAWAL_NOT_STARTED: 'Withdrawal will initiate after confirmation.',
WITHDRAWAL_LOADING: 'Waiting for confirmations...',
OP_WITHDRAWAL_STARTED: <WithdrawProgressBar status="REQUEST_SENT" />,
CCTP_WITHDRAWAL_STARTED: <CCTPBridgeProgressBar status="REQUEST_SENT" />,
};
export function WithdrawModal({ isOpen, onClose, L2TxHash }: WithdrawModalProps) {
const state = L2TxHash === '' ? 'WITHDRAW_NOT_STARTED' : 'WITHDRAW_STARTED';
export function WithdrawModal({
isOpen,
onClose,
L2ApproveTxHash,
L2WithdrawTxHash,
isApprovalTx,
protocol,
}: WithdrawModalProps) {
const { isLoading: isApproveLoading, isSuccess: isApproveSuccess } = useWaitForTransaction({
hash: L2ApproveTxHash,
});
const { isLoading: isWithdrawalLoading, isSuccess: isWithdrawalSuccess } = useWaitForTransaction({
hash: L2WithdrawTxHash,
});
const state = getState(
isApprovalTx,
isApproveLoading,
isApproveSuccess,
isWithdrawalLoading,
isWithdrawalSuccess,
protocol,
);
return (
<Modal
@@ -36,11 +116,11 @@ export function WithdrawModal({ isOpen, onClose, L2TxHash }: WithdrawModalProps)
content={ModalContents[state]}
icon={Icons[state]}
footer={
L2TxHash !== '' && (
L2WithdrawTxHash && (
<div className="text-center">
<a
className="text-md font-sans text-cds-primary"
href={`${publicRuntimeConfig.l2ExplorerURL}/tx/${L2TxHash}`}
href={`${publicRuntimeConfig.l2ExplorerURL}/tx/${L2WithdrawTxHash}`}
target="_blank"
rel="noreferrer noopener"
>

View File

@@ -54,9 +54,9 @@ const DisclaimerContent: Record<BarStatus, ReactNode> = {
<>
In order to minimize security risk, withdrawals using the official Base Bridge take up to{' '}
{challengeWindow}. After your withdrawal request is proposed onchain (within an hour) you must
verify and complete the transaction in order to access your funds, at{' '}
verify and complete the transaction in order to access your funds, on{' '}
<Link href="/transactions" className="underline">
bridge.base.org/transactions
the transactions page
</Link>
.
</>
@@ -64,9 +64,9 @@ const DisclaimerContent: Record<BarStatus, ReactNode> = {
VERIFYING: (
<>
In order to keep attackers from withdrawing your funds, there is a {challengeWindow} waiting
period until you can complete your withdrawal. Check back at{' '}
period until you can complete your withdrawal. Check back on{' '}
<Link href="/transactions" className="underline">
bridge.base.org/transactions
the transactions page
</Link>{' '}
to complete your withdrawal.
</>

View File

@@ -1,3 +1,4 @@
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
import moment from 'moment';
const withdrawalPhaseText = {
@@ -18,6 +19,15 @@ const depositPhaseText = {
FUNDS_DEPOSITED: 'Funds deposited',
};
const cctpBridgePhaseText: Record<CCTPBridgePhase, string> = {
INITIATE_CCTP_BRIDGE_PENDING: 'Processing',
INITIATE_CCTP_BRIDGE_FAILED: 'Failed',
FINALIZE_CCTP_BRIDGE: 'Ready to complete',
FINALIZE_CCTP_BRIDGE_PENDING: 'Processing',
FINALIZE_CCTP_BRIDGE_FAILED: 'Failed',
CCTP_BRIDGE_COMPLETE: 'Funds moved',
};
const withdrawalPhaseStatusText = {
PROPOSING_ON_CHAIN: 'Wait up to 1 hr',
PROVE: '',
@@ -38,4 +48,10 @@ const depositPhaseStatusText = {
FUNDS_DEPOSITED: 'Complete',
};
export { depositPhaseStatusText, depositPhaseText, withdrawalPhaseStatusText, withdrawalPhaseText };
export {
depositPhaseStatusText,
depositPhaseText,
withdrawalPhaseStatusText,
withdrawalPhaseText,
cctpBridgePhaseText,
};

View File

@@ -0,0 +1,326 @@
export default [
{
inputs: [
{ internalType: 'uint32', name: '_localDomain', type: 'uint32' },
{ internalType: 'address', name: '_attester', type: 'address' },
{ internalType: 'uint32', name: '_maxMessageBodySize', type: 'uint32' },
{ internalType: 'uint32', name: '_version', type: 'uint32' },
],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
anonymous: false,
inputs: [{ indexed: true, internalType: 'address', name: 'attester', type: 'address' }],
name: 'AttesterDisabled',
type: 'event',
},
{
anonymous: false,
inputs: [{ indexed: true, internalType: 'address', name: 'attester', type: 'address' }],
name: 'AttesterEnabled',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'previousAttesterManager', type: 'address' },
{ indexed: true, internalType: 'address', name: 'newAttesterManager', type: 'address' },
],
name: 'AttesterManagerUpdated',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: false, internalType: 'uint256', name: 'newMaxMessageBodySize', type: 'uint256' },
],
name: 'MaxMessageBodySizeUpdated',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'caller', type: 'address' },
{ indexed: false, internalType: 'uint32', name: 'sourceDomain', type: 'uint32' },
{ indexed: true, internalType: 'uint64', name: 'nonce', type: 'uint64' },
{ indexed: false, internalType: 'bytes32', name: 'sender', type: 'bytes32' },
{ indexed: false, internalType: 'bytes', name: 'messageBody', type: 'bytes' },
],
name: 'MessageReceived',
type: 'event',
},
{
anonymous: false,
inputs: [{ indexed: false, internalType: 'bytes', name: 'message', type: 'bytes' }],
name: 'MessageSent',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
],
name: 'OwnershipTransferStarted',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
],
name: 'OwnershipTransferred',
type: 'event',
},
{ anonymous: false, inputs: [], name: 'Pause', type: 'event' },
{
anonymous: false,
inputs: [{ indexed: true, internalType: 'address', name: 'newAddress', type: 'address' }],
name: 'PauserChanged',
type: 'event',
},
{
anonymous: false,
inputs: [{ indexed: true, internalType: 'address', name: 'newRescuer', type: 'address' }],
name: 'RescuerChanged',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: false, internalType: 'uint256', name: 'oldSignatureThreshold', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'newSignatureThreshold', type: 'uint256' },
],
name: 'SignatureThresholdUpdated',
type: 'event',
},
{ anonymous: false, inputs: [], name: 'Unpause', type: 'event' },
{
inputs: [],
name: 'acceptOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'attesterManager',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'attester', type: 'address' }],
name: 'disableAttester',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newAttester', type: 'address' }],
name: 'enableAttester',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'uint256', name: 'index', type: 'uint256' }],
name: 'getEnabledAttester',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'getNumEnabledAttesters',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'attester', type: 'address' }],
name: 'isEnabledAttester',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'localDomain',
outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'maxMessageBodySize',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'nextAvailableNonce',
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'owner',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{ inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{
inputs: [],
name: 'paused',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'pauser',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'pendingOwner',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'bytes', name: 'message', type: 'bytes' },
{ internalType: 'bytes', name: 'attestation', type: 'bytes' },
],
name: 'receiveMessage',
outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'bytes', name: 'originalMessage', type: 'bytes' },
{ internalType: 'bytes', name: 'originalAttestation', type: 'bytes' },
{ internalType: 'bytes', name: 'newMessageBody', type: 'bytes' },
{ internalType: 'bytes32', name: 'newDestinationCaller', type: 'bytes32' },
],
name: 'replaceMessage',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'contract IERC20', name: 'tokenContract', type: 'address' },
{ internalType: 'address', name: 'to', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'rescueERC20',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'rescuer',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
{ internalType: 'bytes32', name: 'recipient', type: 'bytes32' },
{ internalType: 'bytes', name: 'messageBody', type: 'bytes' },
],
name: 'sendMessage',
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
{ internalType: 'bytes32', name: 'recipient', type: 'bytes32' },
{ internalType: 'bytes32', name: 'destinationCaller', type: 'bytes32' },
{ internalType: 'bytes', name: 'messageBody', type: 'bytes' },
],
name: 'sendMessageWithCaller',
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'uint256', name: 'newMaxMessageBodySize', type: 'uint256' }],
name: 'setMaxMessageBodySize',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'uint256', name: 'newSignatureThreshold', type: 'uint256' }],
name: 'setSignatureThreshold',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'signatureThreshold',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{ inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{
inputs: [{ internalType: 'address', name: 'newAttesterManager', type: 'address' }],
name: 'updateAttesterManager',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: '_newPauser', type: 'address' }],
name: 'updatePauser',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newRescuer', type: 'address' }],
name: 'updateRescuer',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
name: 'usedNonces',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'version',
outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
stateMutability: 'view',
type: 'function',
},
] as const;

View File

@@ -0,0 +1,254 @@
export default [
{
inputs: [
{ internalType: 'address', name: '_messageTransmitter', type: 'address' },
{ internalType: 'uint32', name: '_messageBodyVersion', type: 'uint32' },
],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'uint64', name: 'nonce', type: 'uint64' },
{ indexed: true, internalType: 'address', name: 'burnToken', type: 'address' },
{ indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
{ indexed: true, internalType: 'address', name: 'depositor', type: 'address' },
{ indexed: false, internalType: 'bytes32', name: 'mintRecipient', type: 'bytes32' },
{ indexed: false, internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
{
indexed: false,
internalType: 'bytes32',
name: 'destinationTokenMessenger',
type: 'bytes32',
},
{ indexed: false, internalType: 'bytes32', name: 'destinationCaller', type: 'bytes32' },
],
name: 'DepositForBurn',
type: 'event',
},
{
anonymous: false,
inputs: [{ indexed: false, internalType: 'address', name: 'localMinter', type: 'address' }],
name: 'LocalMinterAdded',
type: 'event',
},
{
anonymous: false,
inputs: [{ indexed: false, internalType: 'address', name: 'localMinter', type: 'address' }],
name: 'LocalMinterRemoved',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'mintRecipient', type: 'address' },
{ indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
{ indexed: true, internalType: 'address', name: 'mintToken', type: 'address' },
],
name: 'MintAndWithdraw',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
],
name: 'OwnershipTransferStarted',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
],
name: 'OwnershipTransferred',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: false, internalType: 'uint32', name: 'domain', type: 'uint32' },
{ indexed: false, internalType: 'bytes32', name: 'tokenMessenger', type: 'bytes32' },
],
name: 'RemoteTokenMessengerAdded',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: false, internalType: 'uint32', name: 'domain', type: 'uint32' },
{ indexed: false, internalType: 'bytes32', name: 'tokenMessenger', type: 'bytes32' },
],
name: 'RemoteTokenMessengerRemoved',
type: 'event',
},
{
anonymous: false,
inputs: [{ indexed: true, internalType: 'address', name: 'newRescuer', type: 'address' }],
name: 'RescuerChanged',
type: 'event',
},
{
inputs: [],
name: 'acceptOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newLocalMinter', type: 'address' }],
name: 'addLocalMinter',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint32', name: 'domain', type: 'uint32' },
{ internalType: 'bytes32', name: 'tokenMessenger', type: 'bytes32' },
],
name: 'addRemoteTokenMessenger',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
{ internalType: 'bytes32', name: 'mintRecipient', type: 'bytes32' },
{ internalType: 'address', name: 'burnToken', type: 'address' },
],
name: 'depositForBurn',
outputs: [{ internalType: 'uint64', name: '_nonce', type: 'uint64' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
{ internalType: 'bytes32', name: 'mintRecipient', type: 'bytes32' },
{ internalType: 'address', name: 'burnToken', type: 'address' },
{ internalType: 'bytes32', name: 'destinationCaller', type: 'bytes32' },
],
name: 'depositForBurnWithCaller',
outputs: [{ internalType: 'uint64', name: 'nonce', type: 'uint64' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint32', name: 'remoteDomain', type: 'uint32' },
{ internalType: 'bytes32', name: 'sender', type: 'bytes32' },
{ internalType: 'bytes', name: 'messageBody', type: 'bytes' },
],
name: 'handleReceiveMessage',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'localMessageTransmitter',
outputs: [{ internalType: 'contract IMessageTransmitter', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'localMinter',
outputs: [{ internalType: 'contract ITokenMinter', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'messageBodyVersion',
outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'owner',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'pendingOwner',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
name: 'remoteTokenMessengers',
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'removeLocalMinter',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'uint32', name: 'domain', type: 'uint32' }],
name: 'removeRemoteTokenMessenger',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'bytes', name: 'originalMessage', type: 'bytes' },
{ internalType: 'bytes', name: 'originalAttestation', type: 'bytes' },
{ internalType: 'bytes32', name: 'newDestinationCaller', type: 'bytes32' },
{ internalType: 'bytes32', name: 'newMintRecipient', type: 'bytes32' },
],
name: 'replaceDepositForBurn',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'contract IERC20', name: 'tokenContract', type: 'address' },
{ internalType: 'address', name: 'to', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'rescueERC20',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'rescuer',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newRescuer', type: 'address' }],
name: 'updateRescuer',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;

View File

@@ -1,6 +1,10 @@
import { Address } from 'wagmi';
import { Chain } from 'wagmi/chains';
// OP --> Optimism bridge
// CCTP --> Circle Cross-Chain Transfer Protocol (eg USDC)
export type BridgeProtocol = 'OP' | 'CCTP';
export type Asset = {
L1symbol: string;
L2symbol: string;
@@ -12,6 +16,7 @@ export type Asset = {
L1contract?: Address;
L2contract?: Address;
decimals: number;
protocol: BridgeProtocol;
};
export type CustomChain = Chain & {

View File

@@ -1,4 +1,5 @@
import { TransactionStatus } from 'apps/bridge/src/types/API';
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
type TransactionType = 'Deposit' | 'Withdrawal';
@@ -12,4 +13,6 @@ export type BridgeTransaction = {
hash: `0x${string}`;
status?: TransactionStatus;
priceApiId: string;
assetDecimals?: number;
protocol: BridgeProtocol;
};

View File

@@ -3,10 +3,12 @@ import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
// Get all assets for the current chain environment.
export function getAssetListForChainEnv() {
return assetList.filter(
(asset) =>
asset.L1chainId === parseInt(publicRuntimeConfig.l1ChainID) &&
asset.L2chainId === parseInt(publicRuntimeConfig.l2ChainID),
asset.L2chainId === parseInt(publicRuntimeConfig.l2ChainID) &&
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
);
}

View File

@@ -0,0 +1,28 @@
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
// Get depositable assets on current chain. Note that we do not want to include
// bridged USDC (USDbC on L2) because it is preferable to bridge native USDC using
// CCTP. Returned list is sorted as [ETH, USDC, ...rest].
export function getDepositAssetsForChainEnv() {
const assetList = getAssetListForChainEnv();
const nonUSDCAssets = assetList.filter((asset) => asset.L1symbol !== 'USDC');
const bridgedUSDC = assetList.filter(
(asset) => asset.L1symbol === 'USDC' && asset.protocol === 'OP',
);
const nativeUSDC = assetList.filter(
(asset) => asset.L1symbol === 'USDC' && asset.protocol === 'CCTP',
);
const usdcForChainEnv = publicRuntimeConfig.cctpEnabled === 'true' ? nativeUSDC : bridgedUSDC;
return [...nonUSDCAssets, ...usdcForChainEnv].sort((a, b) => {
if (a.L1symbol === 'ETH') return -1;
if (b.L1symbol === 'ETH') return 1;
return a.L1symbol === 'USDC' ? -1 : 0;
});
}

View File

@@ -0,0 +1,30 @@
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
// Get withdrawable assets on current chain. Note that we want to include
// both bridge and native USDC so users who have previously minted USDbC
// on L2 have a way of bridging it back to USDC on L1.
// Returned list is sorted as [ETH, USDC, ...rest].
export function getWithdrawalAssetsForChainEnv() {
const assetList = getAssetListForChainEnv();
const nonUSDCAssets = assetList.filter((asset) => asset.L1symbol !== 'USDC');
const bridgedUSDC = assetList.filter(
(asset) => asset.L1symbol === 'USDC' && asset.protocol === 'OP',
);
const nativeUSDC = assetList.filter(
(asset) => asset.L1symbol === 'USDC' && asset.protocol === 'CCTP',
);
const usdcForChainEnv =
publicRuntimeConfig.cctpEnabled === 'true' ? [...bridgedUSDC, ...nativeUSDC] : bridgedUSDC;
return [...nonUSDCAssets, ...usdcForChainEnv].sort((a, b) => {
if (a.L1symbol === 'ETH') return -1;
if (b.L1symbol === 'ETH') return 1;
return a.L1symbol === 'USDC' ? -1 : 0;
});
}

View File

@@ -6,14 +6,18 @@ const { publicRuntimeConfig } = getConfig();
type UseApproveContractProps = {
contractAddress?: Address;
spender: Address;
approveAmount: string;
decimals: number;
bridgeDirection: 'deposit' | 'withdraw';
};
export function useApproveContract({
contractAddress,
spender,
approveAmount,
decimals,
bridgeDirection,
}: UseApproveContractProps) {
const approveAmountBN =
approveAmount === '' || Number.isNaN(Number(approveAmount))
@@ -21,12 +25,13 @@ export function useApproveContract({
: parseUnits(approveAmount, decimals);
const { config: depositConfig } = usePrepareContractWrite({
address: contractAddress,
// TODO: Replace with dynamic abi importer
abi: erc20ABI,
functionName: 'approve',
chainId: parseInt(publicRuntimeConfig.l1ChainID),
// TODO: Add Allowance selection components
args: [publicRuntimeConfig.l1BridgeProxyAddress, approveAmountBN],
chainId:
bridgeDirection === 'deposit'
? parseInt(publicRuntimeConfig.l1ChainID)
: parseInt(publicRuntimeConfig.l2ChainID),
args: [spender, approveAmountBN],
cacheTime: 0,
});
return depositConfig;

View File

@@ -0,0 +1,131 @@
import { useEffect, useState } from 'react';
import { usePublicClient, useWaitForTransaction } from 'wagmi';
import getConfig from 'next/config';
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
import { useQuery } from 'react-query';
import { keccak256, getEventSelector, decodeAbiParameters, parseAbiParameter } from 'viem';
import MessageTransmitter from 'apps/bridge/src/contract-abis/MessageTransmitter';
const { publicRuntimeConfig } = getConfig();
const l1ChainID = parseInt(publicRuntimeConfig.l1ChainID);
const l2ChainID = parseInt(publicRuntimeConfig.l2ChainID);
type BridgeAttestation = {
attestation: `0x${string}`;
status: string;
};
async function fetchBridgeAttestation(messageHash: string): Promise<BridgeAttestation | undefined> {
const response = await fetch(
`${publicRuntimeConfig.cctpAttestationsAPIURL}/attestations/${messageHash}`,
);
return (await response.json()) as BridgeAttestation;
}
type UseCCTPBridgeStatusProps = {
initiateTxHash: `0x${string}`;
bridgeDirection: 'deposit' | 'withdraw';
};
type CCTPBridgeStatus = {
status: CCTPBridgePhase;
message?: `0x${string}`;
attestation?: `0x${string}`;
setStatus: (newStatus: CCTPBridgePhase) => void;
};
export function useCCTPBridgeStatus({
initiateTxHash,
bridgeDirection,
}: UseCCTPBridgeStatusProps): CCTPBridgeStatus {
const [status, setStatus] = useState<CCTPBridgePhase>('INITIATE_CCTP_BRIDGE_PENDING');
const [message, setMessage] = useState<`0x${string}` | undefined>(undefined);
const [messageHash, setMessageHash] = useState<`0x${string}` | undefined>(undefined);
const isDeposit = bridgeDirection === 'deposit';
// The public client we will use to simulate finalizing the bridge
// on the other chain. Ie if we are depositing this will be an L2 client,
// and if we are withdrawing this will be an L1 client.
const publicClient = usePublicClient({
chainId: isDeposit ? l2ChainID : l1ChainID,
});
// Waiting for the bridge initiation on the chain that the user is bridging from.
// Ie if we are depositing this will be an L1 tx,
// and if we are withdrawing this will be an L2 tx.
const { data: initiateTxReceipt } = useWaitForTransaction({
hash: initiateTxHash,
chainId: isDeposit ? l1ChainID : l2ChainID,
});
const { data: bridgeAttestation } = useQuery(
['bridgeAttestation', { initiateTxHash, messageHash }],
async () => fetchBridgeAttestation(messageHash as string),
{
enabled: !!messageHash,
refetchOnMount: false,
refetchOnWindowFocus: false,
placeholderData: undefined,
},
);
useEffect(() => {
if (initiateTxReceipt) {
if (initiateTxReceipt.status === 'reverted') {
setStatus('INITIATE_CCTP_BRIDGE_FAILED');
return;
}
const eventTopic = getEventSelector('MessageSent(bytes)');
const log = initiateTxReceipt.logs.find((l) => l.topics[0] === eventTopic);
const messageBytes = decodeAbiParameters(
[parseAbiParameter('bytes message')],
log?.data as `0x${string}`,
)[0];
const hash = keccak256(messageBytes);
setMessage(messageBytes);
setMessageHash(hash);
}
}, [initiateTxReceipt]);
useEffect(() => {
if (bridgeAttestation?.attestation && message) {
void (async () => {
if (bridgeAttestation.status === 'pending_confirmations') {
setStatus('INITIATE_CCTP_BRIDGE_PENDING');
return;
}
// We still need to check whether or not the message was received on the other chain.
// If it hasn't been received yet, the user needs to call receiveMessage on the MessageTransmitter.
// It it has been received, the bridge is complete.
// We can check if the message has been received by simulating a call to receiveMessage.
// If it fails, assume it's because the message has already been received.
try {
await publicClient.simulateContract({
address: isDeposit
? publicRuntimeConfig.l2CCTPMessageTransmitterAddress
: publicRuntimeConfig.l1CCTPMessageTransmitterAddress,
abi: MessageTransmitter,
functionName: 'receiveMessage',
args: [message, bridgeAttestation.attestation],
});
// Success, ie the message still needs to be received.
setStatus('FINALIZE_CCTP_BRIDGE');
} catch (e) {
// Failed, ie the message has already been received.
setStatus('CCTP_BRIDGE_COMPLETE');
}
})();
}
}, [bridgeAttestation, bridgeDirection, initiateTxReceipt, isDeposit, message, publicClient]);
// We need to be able to set the status from the FinzliaeCCTPBridgeButton, so we expose a setter.
return {
status,
message,
attestation: bridgeAttestation?.attestation,
setStatus: setStatus,
};
}

View File

@@ -0,0 +1,14 @@
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
export function useGetUSDAmount(priceAPIID: string, formattedAmount: string) {
const conversionRateData = useConversionRate({
asset: priceAPIID,
});
const amountFiat = conversionRateData
? usdFormatter(conversionRateData * +formattedAmount)
: '$0.00';
return amountFiat;
}

View File

@@ -6,15 +6,25 @@ const { publicRuntimeConfig } = getConfig();
type UseIsContractApprovedProps = {
contactAddress?: `0x${string}`;
address?: `0x${string}`;
spender: `0x${string}`;
bridgeDirection: 'deposit' | 'withdraw';
};
export function useIsContractApproved({ contactAddress, address }: UseIsContractApprovedProps) {
export function useIsContractApproved({
contactAddress,
address,
spender,
bridgeDirection,
}: UseIsContractApprovedProps) {
return useContractRead({
address: contactAddress,
abi: erc20ABI,
functionName: 'allowance',
watch: true,
chainId: parseInt(publicRuntimeConfig.l1ChainID),
args: [address as Address, publicRuntimeConfig.l1BridgeProxyAddress],
chainId:
bridgeDirection === 'deposit'
? parseInt(publicRuntimeConfig.l1ChainID)
: parseInt(publicRuntimeConfig.l2ChainID),
args: [address as Address, spender],
});
}

View File

@@ -0,0 +1,44 @@
import { usePrepareContractWrite } from 'wagmi';
import getConfig from 'next/config';
import MessageTransmitter from 'apps/bridge/src/contract-abis/MessageTransmitter';
const { publicRuntimeConfig } = getConfig();
const BRIDGE_DIRECTION_TO_MESSAGE_TRANSMITTER: Record<'deposit' | 'withdraw', `0x${string}`> = {
deposit: publicRuntimeConfig.l2CCTPMessageTransmitterAddress,
withdraw: publicRuntimeConfig.l1CCTPMessageTransmitterAddress,
};
const BRIDGE_DIRECTION_TO_CHAIN_ID: Record<'deposit' | 'withdraw', number> = {
deposit: parseInt(publicRuntimeConfig.l2ChainID),
withdraw: parseInt(publicRuntimeConfig.l1ChainID),
};
type UsePrepareFinalizeCCTPBridgeProps = {
isPermittedToBridge: boolean;
message?: `0x${string}`;
attestation?: `0x${string}`;
bridgeDirection: 'deposit' | 'withdraw';
includeTosVersionByte: boolean;
};
export function usePrepareFinalizeCCTPBridge({
isPermittedToBridge,
message,
attestation,
bridgeDirection,
includeTosVersionByte,
}: UsePrepareFinalizeCCTPBridgeProps) {
const shouldPrepare = isPermittedToBridge && message && attestation;
const { config } = usePrepareContractWrite({
address: shouldPrepare ? BRIDGE_DIRECTION_TO_MESSAGE_TRANSMITTER[bridgeDirection] : undefined,
abi: MessageTransmitter,
functionName: 'receiveMessage',
chainId: BRIDGE_DIRECTION_TO_CHAIN_ID[bridgeDirection],
args: shouldPrepare ? [message, attestation] : undefined,
dataSuffix: includeTosVersionByte ? publicRuntimeConfig.tosVersion : undefined,
});
return config;
}

View File

@@ -0,0 +1,56 @@
import TokenMessenger from 'apps/bridge/src/contract-abis/TokenMessenger';
import { Asset } from 'apps/bridge/src/types/Asset';
import { parseUnits, pad } from 'viem';
import getConfig from 'next/config';
import { Address, usePrepareContractWrite } from 'wagmi';
const { publicRuntimeConfig } = getConfig();
const BRIDGE_DIRECTION_TO_TOKEN_MESSENGER: Record<'deposit' | 'withdraw', `0x${string}`> = {
deposit: publicRuntimeConfig.l1CCTPTokenMessengerAddress,
withdraw: publicRuntimeConfig.l2CCTPTokenMessengerAddress,
};
const BRIDGE_DIRECTION_TO_CHAIN_ID: Record<'deposit' | 'withdraw', number> = {
deposit: parseInt(publicRuntimeConfig.l1ChainID),
withdraw: parseInt(publicRuntimeConfig.l2ChainID),
};
type UsePrepareInitiateCCTPBridgeProps = {
mintRecipient?: Address;
asset: Asset;
amount: string;
destinationDomain: number;
isPermittedToBridge: boolean;
includeTosVersionByte: boolean;
bridgeDirection: 'deposit' | 'withdraw';
};
export function usePrepareInitiateCCTPBridge({
mintRecipient,
asset,
amount,
destinationDomain,
isPermittedToBridge,
bridgeDirection,
includeTosVersionByte,
}: UsePrepareInitiateCCTPBridgeProps) {
const shouldPrepare = isPermittedToBridge && amount !== '' && mintRecipient;
const { config } = usePrepareContractWrite({
address: shouldPrepare ? BRIDGE_DIRECTION_TO_TOKEN_MESSENGER[bridgeDirection] : undefined,
abi: TokenMessenger,
functionName: 'depositForBurn',
chainId: BRIDGE_DIRECTION_TO_CHAIN_ID[bridgeDirection],
args: shouldPrepare
? [
amount !== '' ? parseUnits(amount, asset.decimals) : parseUnits('0', asset.decimals),
destinationDomain,
pad(mintRecipient),
(bridgeDirection === 'deposit' ? asset.L1contract : asset.L2contract) as Address,
]
: undefined,
dataSuffix: includeTosVersionByte ? publicRuntimeConfig.tosVersion : undefined,
});
return config;
}

View File

@@ -3,8 +3,9 @@ import { Asset } from 'apps/bridge/src/types/Asset';
import { decodeFunctionData } from 'viem';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
import getConfig from 'next/config';
import { l1StandardBridgeABI } from '@eth-optimism/contracts-ts';
import getConfig from 'next/config';
import TokenMessenger from 'apps/bridge/src/contract-abis/TokenMessenger';
const assetList = getAssetListForChainEnv();
@@ -14,8 +15,13 @@ const ETH_DEPOSIT_ADDRESS = (
publicRuntimeConfig?.l1OptimismPortalProxyAddress ?? '0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA'
).toLowerCase();
const CCTP_DEPOSIT_ADDRESS = (
publicRuntimeConfig?.l1CCTPTokenMessengerAddress ?? '0xd0c3da58f55358142b8d3e06c1c30c5c6114efe8'
).toLowerCase();
export function explorerTxToBridgeDeposit(tx: BlockExplorerTransaction): BridgeTransaction {
if (tx.to === ETH_DEPOSIT_ADDRESS) {
// ETH deposit (OP)
return {
type: 'Deposit',
from: tx.from,
@@ -26,6 +32,32 @@ export function explorerTxToBridgeDeposit(tx: BlockExplorerTransaction): BridgeT
hash: tx.hash as `0x${string}`,
status: 'Complete',
priceApiId: 'ethereum',
assetDecimals: 18,
protocol: 'OP',
};
} else if (tx.to === CCTP_DEPOSIT_ADDRESS) {
// CCTP deposit (CCTP)
const { args } = decodeFunctionData({
abi: TokenMessenger,
data: tx.input,
});
const token = assetList.find(
(asset) =>
asset.L1chainId === parseInt(publicRuntimeConfig.l1ChainID) &&
asset.L1contract?.toLowerCase() === (args?.[3] as string).toLowerCase() &&
asset.protocol === 'CCTP',
) as Asset;
return {
type: 'Deposit',
from: tx.from,
to: tx.to,
assetSymbol: token.L1symbol ?? '',
amount: (args?.[0] as bigint).toString(),
blockTimestamp: tx.timeStamp,
hash: tx.hash as `0x${string}`,
priceApiId: token.apiId,
assetDecimals: token.decimals,
protocol: 'CCTP',
};
}
@@ -48,5 +80,7 @@ export function explorerTxToBridgeDeposit(tx: BlockExplorerTransaction): BridgeT
hash: tx.hash as `0x${string}`,
status: 'Complete',
priceApiId: token.apiId,
assetDecimals: token.decimals,
protocol: 'OP',
};
}

View File

@@ -1,4 +1,5 @@
import { l2StandardBridgeABI } from '@eth-optimism/contracts-ts';
import TokenMessenger from 'apps/bridge/src/contract-abis/TokenMessenger';
import { BlockExplorerTransaction } from 'apps/bridge/src/types/API';
import { Asset } from 'apps/bridge/src/types/Asset';
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
@@ -14,8 +15,13 @@ const ETH_WITHDRAWAL_ADDRESS = (
publicRuntimeConfig?.l2L1MessagePasserAddress ?? '0x4200000000000000000000000000000000000016'
).toLowerCase();
const CCTP_WITHDRAWAL_ADDRESS = (
publicRuntimeConfig?.l2CCTPTokenMessengerAddress ?? '0x877b8e8c9e2383077809787ED6F279ce01CB4cc8'
).toLowerCase();
export function explorerTxToBridgeWithdrawal(tx: BlockExplorerTransaction): BridgeTransaction {
if (tx.to === ETH_WITHDRAWAL_ADDRESS) {
// ETH withdrawal (OP)
return {
type: 'Withdrawal',
from: tx.from,
@@ -25,6 +31,28 @@ export function explorerTxToBridgeWithdrawal(tx: BlockExplorerTransaction): Brid
blockTimestamp: tx.timeStamp,
hash: tx.hash as `0x${string}`,
priceApiId: 'ethereum',
protocol: 'OP',
};
} else if (tx.to === CCTP_WITHDRAWAL_ADDRESS) {
// CCTP withdrawal (CCTP)
const { args } = decodeFunctionData({ abi: TokenMessenger, data: tx.input });
const token = assetList.find(
(asset) =>
asset.L1chainId === parseInt(publicRuntimeConfig.l1ChainID) &&
asset.L2contract?.toLowerCase() === (args?.[3] as string).toLowerCase() &&
asset.protocol === 'CCTP',
) as Asset;
return {
type: 'Withdrawal',
from: tx.from,
to: tx.to,
assetSymbol: token.L2symbol ?? '',
amount: (args?.[0] as bigint).toString(),
blockTimestamp: tx.timeStamp,
hash: tx.hash as `0x${string}`,
priceApiId: token.apiId,
assetDecimals: token.decimals,
protocol: 'CCTP',
};
}
@@ -42,5 +70,6 @@ export function explorerTxToBridgeWithdrawal(tx: BlockExplorerTransaction): Brid
blockTimestamp: tx.timeStamp,
hash: tx.hash as `0x${string}`,
priceApiId: token?.apiId,
protocol: 'OP',
};
}

View File

@@ -0,0 +1,24 @@
type FormattedTimestamp = {
date: string;
shortDate: string;
time: string;
};
export function formatTimestamp(timestamp: string): FormattedTimestamp {
const date = new Date(Number(timestamp) * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
const shortDate = new Date(Number(timestamp) * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
const time = new Date(Number(timestamp) * 1000).toLocaleTimeString('en-US', {
timeStyle: 'short',
});
return { date, shortDate, time };
}

View File

@@ -2,6 +2,7 @@ import { l1StandardBridgeABI } from '@eth-optimism/contracts-ts';
import { decodeFunctionData } from 'viem';
import { BlockExplorerTransaction } from 'apps/bridge/src/types/API';
import getConfig from 'next/config';
import TokenMessenger from 'apps/bridge/src/contract-abis/TokenMessenger';
const { publicRuntimeConfig } = getConfig();
@@ -13,9 +14,17 @@ const ERC20_DEPOSIT_ADDRESS = (
publicRuntimeConfig?.l1BridgeProxyAddress ?? '0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a'
).toLowerCase();
const CCTP_DEPOSIT_ADDRESS = (
publicRuntimeConfig?.l1CCTPTokenMessengerAddress ?? '0xd0c3da58f55358142b8d3e06c1c30c5c6114efe8'
).toLowerCase();
export function isETHOrERC20Deposit(tx: BlockExplorerTransaction) {
// Immediately filter out if tx is not to an address we don't care about
if (tx.to !== ETH_DEPOSIT_ADDRESS && tx.to !== ERC20_DEPOSIT_ADDRESS) {
if (
tx.to !== ETH_DEPOSIT_ADDRESS &&
tx.to !== ERC20_DEPOSIT_ADDRESS &&
tx.to !== CCTP_DEPOSIT_ADDRESS
) {
return false;
}
@@ -35,5 +44,16 @@ export function isETHOrERC20Deposit(tx: BlockExplorerTransaction) {
}
}
// CCTP deposit
if (tx.to === CCTP_DEPOSIT_ADDRESS) {
const { functionName } = decodeFunctionData({
abi: TokenMessenger,
data: tx.input,
});
if (functionName === 'depositForBurn') {
return true;
}
}
return false;
}

View File

@@ -1,4 +1,5 @@
import { l2StandardBridgeABI } from '@eth-optimism/contracts-ts';
import TokenMessenger from 'apps/bridge/src/contract-abis/TokenMessenger';
import { BlockExplorerTransaction } from 'apps/bridge/src/types/API';
import getConfig from 'next/config';
import { decodeFunctionData } from 'viem';
@@ -13,9 +14,17 @@ const ERC20_WITHDRAWAL_ADDRESS = (
publicRuntimeConfig?.L2StandardBridge ?? '0x4200000000000000000000000000000000000010'
).toLowerCase();
const CCTP_WITHDRAWAL_ADDRESS = (
publicRuntimeConfig?.l2CCTPTokenMessengerAddress ?? '0x877b8e8c9e2383077809787ED6F279ce01CB4cc8'
).toLowerCase();
export function isETHOrERC20Withdrawal(tx: BlockExplorerTransaction) {
// Immediately filter out if tx is not to an address we don't care about
if (tx.to !== ETH_WITHDRAWAL_ADDRESS && tx.to !== ERC20_WITHDRAWAL_ADDRESS) {
if (
tx.to !== ETH_WITHDRAWAL_ADDRESS &&
tx.to !== ERC20_WITHDRAWAL_ADDRESS &&
tx.to !== CCTP_WITHDRAWAL_ADDRESS
) {
return false;
}
@@ -32,5 +41,13 @@ export function isETHOrERC20Withdrawal(tx: BlockExplorerTransaction) {
}
}
// CCTP deposit
if (tx.to === CCTP_WITHDRAWAL_ADDRESS) {
const { functionName } = decodeFunctionData({ abi: TokenMessenger, data: tx.input });
if (functionName === 'depositForBurn') {
return true;
}
}
return false;
}

View File

@@ -10,4 +10,13 @@ type WithdrawalPhase =
| 'FUNDS_WITHDRAWN';
type DepositPhase = 'DEPOSIT_TX_PENDING' | 'FUNDS_DEPOSITED' | 'DEPOSIT_TX_FAILURE';
export type { DepositPhase, WithdrawalPhase };
type CCTPBridgePhase =
| 'INITIATE_CCTP_BRIDGE_PENDING'
| 'INITIATE_CCTP_BRIDGE_FAILED'
| 'FINALIZE_CCTP_BRIDGE'
| 'FINALIZE_CCTP_BRIDGE_PENDING'
| 'FINALIZE_CCTP_BRIDGE_FAILED'
| 'CCTP_BRIDGE_COMPLETE';
export type { DepositPhase, WithdrawalPhase, CCTPBridgePhase };

View File

@@ -273,7 +273,7 @@ __metadata:
tailwindcss: ^3.2.4
typescript: next
viem: latest
wagmi: ^1.4.3
wagmi: ^1.4.4
webpack-bugsnag-plugins: ^1.8.0
languageName: unknown
linkType: soft
@@ -6656,9 +6656,9 @@ __metadata:
languageName: node
linkType: hard
"@wagmi/core@npm:1.4.3":
version: 1.4.3
resolution: "@wagmi/core@npm:1.4.3"
"@wagmi/core@npm:1.4.4":
version: 1.4.4
resolution: "@wagmi/core@npm:1.4.4"
dependencies:
"@wagmi/connectors": 3.1.2
abitype: 0.8.7
@@ -6670,7 +6670,7 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: a8a0622324076bc3087eb05eb1d74652511433bc68e1972e24f699018140508e56473760c5de0a56c92f621aed52ef7e683f3e0aca34aec5428464d28559c27f
checksum: ee4946a6ebdc9526024898e10d472b64eba673f7d9075f56aa564541a4a3c394c5e285a64f297f1a315471a8b7b7649e688ddd2cd82c9b38c77d036d2278b86f
languageName: node
linkType: hard
@@ -21207,14 +21207,14 @@ typescript@next:
languageName: node
linkType: hard
"wagmi@npm:^1.4.3":
version: 1.4.3
resolution: "wagmi@npm:1.4.3"
"wagmi@npm:^1.4.4":
version: 1.4.4
resolution: "wagmi@npm:1.4.4"
dependencies:
"@tanstack/query-sync-storage-persister": ^4.27.1
"@tanstack/react-query": ^4.28.0
"@tanstack/react-query-persist-client": ^4.28.0
"@wagmi/core": 1.4.3
"@wagmi/core": 1.4.4
abitype: 0.8.7
use-sync-external-store: ^1.2.0
peerDependencies:
@@ -21224,7 +21224,7 @@ typescript@next:
peerDependenciesMeta:
typescript:
optional: true
checksum: df0dfddb898a59246c14e7053caeeea313801340d589e5545bad631ec47fc66e2cc1dafaaa7880c9947931efad442f834ed3d3c6b2d0952b586f6da575c3293f
checksum: 4cf7ce978400d21e27d25871a4c8bc6b05fabd61d6d3d6e705c120a1c1dd4ba260d6ddfb2bb28b5e18591ba825c89839d0f3c874c55dfa255a9cde96d0785202
languageName: node
linkType: hard