diff --git a/apps/bridge/.env.goerli.example b/apps/bridge/.env.goerli.example
index c1b5f8b..075e4be 100644
--- a/apps/bridge/.env.goerli.example
+++ b/apps/bridge/.env.goerli.example
@@ -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
\ No newline at end of file
diff --git a/apps/bridge/.eslintrc.js b/apps/bridge/.eslintrc.js
index ad8092e..f4555b9 100644
--- a/apps/bridge/.eslintrc.js
+++ b/apps/bridge/.eslintrc.js
@@ -13,5 +13,6 @@ module.exports = {
rules: {
// Does not work with `:` aliases
'import/extensions': 'off',
+ 'react/prop-types': 'off',
},
};
diff --git a/apps/bridge/assets.ts b/apps/bridge/assets.ts
index 8c0cb46..f72714d 100644
--- a/apps/bridge/assets.ts
+++ b/apps/bridge/assets.ts
@@ -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',
},
];
diff --git a/apps/bridge/global.d.ts b/apps/bridge/global.d.ts
index 76d2a6c..c32ab58 100644
--- a/apps/bridge/global.d.ts
+++ b/apps/bridge/global.d.ts
@@ -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;
};
};
diff --git a/apps/bridge/next.config.js b/apps/bridge/next.config.js
index 58eafb6..305ae86 100644
--- a/apps/bridge/next.config.js
+++ b/apps/bridge/next.config.js
@@ -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() {
diff --git a/apps/bridge/package.json b/apps/bridge/package.json
index 6b0cce1..4b69530 100644
--- a/apps/bridge/package.json
+++ b/apps/bridge/package.json
@@ -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": {
diff --git a/apps/bridge/pages/transactions.tsx b/apps/bridge/pages/transactions.tsx
index fba9dff..fc1e620 100644
--- a/apps/bridge/pages/transactions.tsx
+++ b/apps/bridge/pages/transactions.tsx
@@ -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 (
@@ -52,13 +61,26 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
+
{
if (transaction.type === 'Deposit') {
- return ;
+ return (
+
+ );
}
return (
);
})}
diff --git a/apps/bridge/src/components/BridgeInput/BridgeInput.tsx b/apps/bridge/src/components/BridgeInput/BridgeInput.tsx
index d16e23f..d4e4af7 100644
--- a/apps/bridge/src/components/BridgeInput/BridgeInput.tsx
+++ b/apps/bridge/src/components/BridgeInput/BridgeInput.tsx
@@ -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,
}
: {
diff --git a/apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar.tsx b/apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar.tsx
new file mode 100644
index 0000000..8fc349c
--- /dev/null
+++ b/apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar.tsx
@@ -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 (
+
+
+ {status === 'DONE' ? '✓' : num}
+
+
{label}
+
+ );
+}
+
+type BarStatus = 'REQUEST_SENT' | 'VERIFIED';
+type CCTPBridgeProgressBarProps = {
+ status: BarStatus;
+};
+
+const BarStatusToBadgeStatuses: Record = {
+ REQUEST_SENT: ['STARTED', 'NOT_STARTED'],
+ VERIFIED: ['DONE', 'STARTED'],
+};
+
+const DisclaimerContent: Record = {
+ REQUEST_SENT: (
+ <>
+ USDC deposits and withdrawals use Circle's CCTP. After you initiate a deposit or
+ withdrawal, you must complete the bridge in order to access your funds, on{' '}
+
+ the transactions page
+
+ .
+ >
+ ),
+ 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 (
+
+
+
+
+ Takes a few minutes
+
+
+
+ Takes a few minutes
+
+
+
{DisclaimerContent[status]}
+
+
+ Learn more
+
+
+
+ );
+}
diff --git a/apps/bridge/src/components/DepositContainer/DepositContainer.tsx b/apps/bridge/src/components/DepositContainer/DepositContainer.tsx
index 6a8a46a..990d814 100644
--- a/apps/bridge/src/components/DepositContainer/DepositContainer.tsx
+++ b/apps/bridge/src/components/DepositContainer/DepositContainer.tsx
@@ -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(assetList[0]);
- const activeAssets = assetList.filter((asset) =>
- publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
- );
+ const [selectedAsset, setSelectedAsset] = useState(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}
/>
= {
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: ,
};
-const Titles = {
+const Titles: Record = {
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 = {
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;
diff --git a/apps/bridge/src/components/TransactionIcon/TransactionIcon.tsx b/apps/bridge/src/components/TransactionIcon/TransactionIcon.tsx
index 8d81333..f01ee76 100644
--- a/apps/bridge/src/components/TransactionIcon/TransactionIcon.tsx
+++ b/apps/bridge/src/components/TransactionIcon/TransactionIcon.tsx
@@ -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 = {
FUNDS_DEPOSITED: '/icons/phases/send.svg',
};
+const ccptPhasePaths: Record> = {
+ 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 = (
-
- );
+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 = ;
return (
-
+
{phaseIcon}
= {
+ 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(
);
+ }
+
+ for (let i = 0; i < totalPhases - PHASE_MAP[phase]; i += 1) {
+ rv.push(
+
,
+ );
+ }
+ 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 (
+
+ {generatePhaseIndicator(phase, protocol)}
+
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow.tsx b/apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow.tsx
new file mode 100644
index 0000000..384f08e
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow.tsx
@@ -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>;
+};
+
+// 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 (
+
+
+
+
+
+
{date ?? ''}
+
{time ?? ''}
+
+
+
+ {/* mobile design - left column */}
+
+
+
+
+
+
+ {/* mobile design - right column */}
+
+
+
{`${bridgeAmount} ${transaction.assetSymbol}`}
+
{`${amountFiat} USD`}
+
+
+
+
+
{`${bridgeAmount} ${transaction.assetSymbol}`}
+
{`${amountFiat} USD`}
+
+
+
+
+
+
{cctpBridgePhaseText[status]}
+
+
+
+
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeStatus.tsx b/apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeStatus.tsx
new file mode 100644
index 0000000..2cee549
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeStatus.tsx
@@ -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>;
+};
+
+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 ;
+
+ // Return 'Ready to finalize' if finalize tx is ready to be sent
+ if (isReadyToFinazlie)
+ return (
+
+ );
+
+ // Return 'Failed' if initiate tx failed
+ if (isFailed) return 'Failed';
+
+ // Otherwise the bridge is complete
+ return Complete ;
+});
diff --git a/apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeButton.tsx b/apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeButton.tsx
new file mode 100644
index 0000000..705a731
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeButton.tsx
@@ -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>;
+};
+
+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 (
+
+ {isOnCorrectNetwork
+ ? 'Ready to complete'
+ : `Switch to ${bridgeDirection === 'deposit' ? 'L2' : 'L1'}`}
+
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeModal.tsx b/apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeModal.tsx
new file mode 100644
index 0000000..c2c31eb
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeModal.tsx
@@ -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: ,
+};
+
+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 (
+
+
+ {`View on ${bridgeDirection === 'withdraw' ? 'Etherscan' : 'Basescan'}`}
+
+
+ )
+ }
+ />
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/DepositRow/DepositRow.tsx b/apps/bridge/src/components/Transactions/DepositRow/DepositRow.tsx
index 7a2ffba..01fbb58 100644
--- a/apps/bridge/src/components/Transactions/DepositRow/DepositRow.tsx
+++ b/apps/bridge/src/components/Transactions/DepositRow/DepositRow.tsx
@@ -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
>;
};
-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 = (
-
-
-
-
-
- Pending
-
- );
-
- 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 (
+
+ );
}
- return (
-
-
-
-
-
-
{date ?? ''}
-
{time ?? ''}
-
-
-
- {/* mobile design - left column */}
-
-
-
-
-
-
- {/* mobile design - right column */}
-
-
-
{`${depositAmount} ${transaction.assetSymbol}`}
-
{`${amountFiat} USD`}
-
-
-
-
-
{`${depositAmount} ${transaction.assetSymbol}`}
-
{`${amountFiat} USD`}
-
-
-
-
-
-
{depositPhaseText[depositStatus]}
-
-
-
- {PHASE_TO_STATUS[depositStatus]}
-
-
- );
+ return ;
});
diff --git a/apps/bridge/src/components/Transactions/DepositRow/OPDepositRow.tsx b/apps/bridge/src/components/Transactions/DepositRow/OPDepositRow.tsx
new file mode 100644
index 0000000..088cedb
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/DepositRow/OPDepositRow.tsx
@@ -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, 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 (
+
+
+
+
+
+
{date ?? ''}
+
{time ?? ''}
+
+
+
+ {/* mobile design - left column */}
+
+
+
+
+
+
+ {/* mobile design - right column */}
+
+
+
{`${depositAmount} ${transaction.assetSymbol}`}
+
{`${amountFiat} USD`}
+
+
+
+
+
{`${depositAmount} ${transaction.assetSymbol}`}
+
{`${amountFiat} USD`}
+
+
+
+
+
+
{depositPhaseText[depositStatus]}
+
+
+
+ {depositStatus === 'DEPOSIT_TX_PENDING' ? (
+
+ ) : (
+ PHASE_TO_STATUS[depositStatus]
+ )}
+
+
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/PendingButton.tsx b/apps/bridge/src/components/Transactions/PendingButton.tsx
new file mode 100644
index 0000000..4c86a2b
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/PendingButton.tsx
@@ -0,0 +1,25 @@
+import { memo } from 'react';
+
+export const PendingButton = memo(function PendingButton() {
+ return (
+
+
+
+
+
+ Pending
+
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/WithdrawalRow/OPWithdrawalRow.tsx b/apps/bridge/src/components/Transactions/WithdrawalRow/OPWithdrawalRow.tsx
new file mode 100644
index 0000000..95b7bc2
--- /dev/null
+++ b/apps/bridge/src/components/Transactions/WithdrawalRow/OPWithdrawalRow.tsx
@@ -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>;
+ setModalFinalizeTxHash: Dispatch>;
+};
+
+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 = (
+
+
+
+
+
+ Pending
+
+ );
+ const PHASE_TO_STATUS = {
+ PROPOSING_ON_CHAIN: withdrawalPhaseStatusText.PROPOSING_ON_CHAIN,
+ PROVE: (
+
+ ),
+ PROVE_TX_PENDING: pendingButton,
+ PROVE_TX_FAILURE: withdrawalPhaseStatusText.PROVE_TX_FAILURE,
+ CHALLENGE_WINDOW: withdrawalPhaseStatusText.CHALLENGE_WINDOW(Number(challengeWindowEndTime)),
+ FINALIZE: (
+
+ ),
+ FINALIZE_TX_PENDING: pendingButton,
+ FINALIZE_TX_FAILURE: withdrawalPhaseStatusText.FINALIZE_TX_FAILURE,
+ FUNDS_WITHDRAWN: withdrawalPhaseStatusText.FUNDS_WITHDRAWN,
+ };
+
+ return (
+
+
+
+
+
+
{date ?? ''}
+
{time ?? ''}
+
+
+
+ {/* mobile design - left column */}
+
+
+
+
+
+
+ {/* mobile design - right column */}
+
+
+
{`${withdrawalAmount} ${transaction.assetSymbol}`}
+
{`${amountFiat} USD`}
+
+
+
+
+
{`${withdrawalAmount} ${transaction.assetSymbol}`}
+
{`${amountFiat} USD`}
+
+
+
+
+
+
{withdrawalPhaseText[withdrawalStatus]}
+
+
+
+ {PHASE_TO_STATUS[withdrawalStatus]}
+
+
+ );
+});
diff --git a/apps/bridge/src/components/Transactions/WithdrawalRow/WithdrawalRow.tsx b/apps/bridge/src/components/Transactions/WithdrawalRow/WithdrawalRow.tsx
index 841d903..d3a8475 100644
--- a/apps/bridge/src/components/Transactions/WithdrawalRow/WithdrawalRow.tsx
+++ b/apps/bridge/src/components/Transactions/WithdrawalRow/WithdrawalRow.tsx
@@ -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>;
- setModalFinalizeTxHash: Dispatch>;
+ setModalFinalizeOPTxHash: Dispatch>;
+ setModalFinalizeCCTPTxHash: Dispatch>;
};
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(
);
- }
- for (let i = 0; i < 4 - PHASE_MAP[phase]; i += 1) {
- rv.push(
-
,
- );
- }
- return rv;
- };
-
- const pendingButton = (
-
-
-
-
-
- Pending
-
- );
- const PHASE_TO_STATUS = {
- PROPOSING_ON_CHAIN: withdrawalPhaseStatusText.PROPOSING_ON_CHAIN,
- PROVE: (
-
- ),
- PROVE_TX_PENDING: pendingButton,
- PROVE_TX_FAILURE: withdrawalPhaseStatusText.PROVE_TX_FAILURE,
- CHALLENGE_WINDOW: withdrawalPhaseStatusText.CHALLENGE_WINDOW(Number(challengeWindowEndTime)),
- FINALIZE: (
-
- ),
- FINALIZE_TX_PENDING: pendingButton,
- FINALIZE_TX_FAILURE: withdrawalPhaseStatusText.FINALIZE_TX_FAILURE,
- FUNDS_WITHDRAWN: withdrawalPhaseStatusText.FUNDS_WITHDRAWN,
- };
+ );
+ }
return (
-
-
-
-
-
-
{date ?? ''}
-
{time ?? ''}
-
-
-
- {/* mobile design - left column */}
-
-
-
-
-
-
- {/* mobile design - right column */}
-
-
-
{`${withdrawalAmount} ${transaction.assetSymbol}`}
-
{`${amountFiat} USD`}
-
-
-
-
-
{`${withdrawalAmount} ${transaction.assetSymbol}`}
-
{`${amountFiat} USD`}
-
-
-
-
-
- {generatePhaseIndicator(withdrawalStatus)}
-
-
{withdrawalPhaseText[withdrawalStatus]}
-
-
-
- {PHASE_TO_STATUS[withdrawalStatus]}
-
-
+
);
});
diff --git a/apps/bridge/src/components/WithdrawContainer/WithdrawContainer.tsx b/apps/bridge/src/components/WithdrawContainer/WithdrawContainer.tsx
index 84b644c..bc260fc 100644
--- a/apps/bridge/src/components/WithdrawContainer/WithdrawContainer.tsx
+++ b/apps/bridge/src/components/WithdrawContainer/WithdrawContainer.tsx
@@ -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(assetList[0]);
+ const [selectedAsset, setSelectedAsset] = useState(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 = (
);
+ } else if (selectedAsset.protocol === 'CCTP' && !readApprovalResult) {
+ withdrawDisabled =
+ (isSmartContractWallet && !isAddress(withdrawTo ?? '')) || !isPermittedToBridge;
+
+ button = (
+
+ Approval
+
+ );
} else {
withdrawDisabled =
parseFloat(withdrawAmount) <= 0 ||
@@ -162,7 +280,10 @@ export function WithdrawContainer() {
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 = {
+ 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 = {
+ 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: ,
+const ModalContents: Record = {
+ 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: ,
+ CCTP_WITHDRAWAL_STARTED: ,
};
-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 (
diff --git a/apps/bridge/src/components/WithdrawProgressBar/WithdrawProgressBar.tsx b/apps/bridge/src/components/WithdrawProgressBar/WithdrawProgressBar.tsx
index 120761a..04c0ec3 100644
--- a/apps/bridge/src/components/WithdrawProgressBar/WithdrawProgressBar.tsx
+++ b/apps/bridge/src/components/WithdrawProgressBar/WithdrawProgressBar.tsx
@@ -54,9 +54,9 @@ const DisclaimerContent: Record = {
<>
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{' '}
- bridge.base.org/transactions
+ the transactions page
.
>
@@ -64,9 +64,9 @@ const DisclaimerContent: Record = {
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{' '}
- bridge.base.org/transactions
+ the transactions page
{' '}
to complete your withdrawal.
>
diff --git a/apps/bridge/src/constants/phaseText.ts b/apps/bridge/src/constants/phaseText.ts
index 41a83b5..f91b020 100644
--- a/apps/bridge/src/constants/phaseText.ts
+++ b/apps/bridge/src/constants/phaseText.ts
@@ -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 = {
+ 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,
+};
diff --git a/apps/bridge/src/contract-abis/MessageTransmitter.ts b/apps/bridge/src/contract-abis/MessageTransmitter.ts
new file mode 100644
index 0000000..10f4864
--- /dev/null
+++ b/apps/bridge/src/contract-abis/MessageTransmitter.ts
@@ -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;
diff --git a/apps/bridge/src/contract-abis/TokenMessenger.ts b/apps/bridge/src/contract-abis/TokenMessenger.ts
new file mode 100644
index 0000000..14c67c5
--- /dev/null
+++ b/apps/bridge/src/contract-abis/TokenMessenger.ts
@@ -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;
diff --git a/apps/bridge/src/types/Asset.ts b/apps/bridge/src/types/Asset.ts
index fae98b2..c5ff7e7 100644
--- a/apps/bridge/src/types/Asset.ts
+++ b/apps/bridge/src/types/Asset.ts
@@ -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 & {
diff --git a/apps/bridge/src/types/BridgeTransaction.ts b/apps/bridge/src/types/BridgeTransaction.ts
index 4b72f58..4e08289 100644
--- a/apps/bridge/src/types/BridgeTransaction.ts
+++ b/apps/bridge/src/types/BridgeTransaction.ts
@@ -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;
};
diff --git a/apps/bridge/src/utils/assets/getAssetListForChainEnv.ts b/apps/bridge/src/utils/assets/getAssetListForChainEnv.ts
index 3317025..611625a 100644
--- a/apps/bridge/src/utils/assets/getAssetListForChainEnv.ts
+++ b/apps/bridge/src/utils/assets/getAssetListForChainEnv.ts
@@ -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()),
);
}
diff --git a/apps/bridge/src/utils/assets/getDepositAssetsForChainEnv.ts b/apps/bridge/src/utils/assets/getDepositAssetsForChainEnv.ts
new file mode 100644
index 0000000..77f4dc4
--- /dev/null
+++ b/apps/bridge/src/utils/assets/getDepositAssetsForChainEnv.ts
@@ -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;
+ });
+}
diff --git a/apps/bridge/src/utils/assets/getWithdrawalAssetsForChainEnv.ts b/apps/bridge/src/utils/assets/getWithdrawalAssetsForChainEnv.ts
new file mode 100644
index 0000000..2471a21
--- /dev/null
+++ b/apps/bridge/src/utils/assets/getWithdrawalAssetsForChainEnv.ts
@@ -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;
+ });
+}
diff --git a/apps/bridge/src/utils/hooks/useApproveContract.ts b/apps/bridge/src/utils/hooks/useApproveContract.ts
index 59c02dc..f651590 100644
--- a/apps/bridge/src/utils/hooks/useApproveContract.ts
+++ b/apps/bridge/src/utils/hooks/useApproveContract.ts
@@ -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;
diff --git a/apps/bridge/src/utils/hooks/useCCTPBridgeStatus.ts b/apps/bridge/src/utils/hooks/useCCTPBridgeStatus.ts
new file mode 100644
index 0000000..f9b105f
--- /dev/null
+++ b/apps/bridge/src/utils/hooks/useCCTPBridgeStatus.ts
@@ -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 {
+ 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('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,
+ };
+}
diff --git a/apps/bridge/src/utils/hooks/useGetUSDAmount.ts b/apps/bridge/src/utils/hooks/useGetUSDAmount.ts
new file mode 100644
index 0000000..8ca711a
--- /dev/null
+++ b/apps/bridge/src/utils/hooks/useGetUSDAmount.ts
@@ -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;
+}
diff --git a/apps/bridge/src/utils/hooks/useIsContractApproved.ts b/apps/bridge/src/utils/hooks/useIsContractApproved.ts
index 262bc82..cd13705 100644
--- a/apps/bridge/src/utils/hooks/useIsContractApproved.ts
+++ b/apps/bridge/src/utils/hooks/useIsContractApproved.ts
@@ -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],
});
}
diff --git a/apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge.ts b/apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge.ts
new file mode 100644
index 0000000..6e3c00b
--- /dev/null
+++ b/apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge.ts
@@ -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;
+}
diff --git a/apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge.ts b/apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge.ts
new file mode 100644
index 0000000..0c2b6e6
--- /dev/null
+++ b/apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge.ts
@@ -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;
+}
diff --git a/apps/bridge/src/utils/transactions/explorerTxToBridgeDeposit.ts b/apps/bridge/src/utils/transactions/explorerTxToBridgeDeposit.ts
index 51503f9..efb1603 100644
--- a/apps/bridge/src/utils/transactions/explorerTxToBridgeDeposit.ts
+++ b/apps/bridge/src/utils/transactions/explorerTxToBridgeDeposit.ts
@@ -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',
};
}
diff --git a/apps/bridge/src/utils/transactions/explorerTxToBridgeWithdrawal.ts b/apps/bridge/src/utils/transactions/explorerTxToBridgeWithdrawal.ts
index 153ad29..bf28100 100644
--- a/apps/bridge/src/utils/transactions/explorerTxToBridgeWithdrawal.ts
+++ b/apps/bridge/src/utils/transactions/explorerTxToBridgeWithdrawal.ts
@@ -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',
};
}
diff --git a/apps/bridge/src/utils/transactions/formatBlockTimestamp.ts b/apps/bridge/src/utils/transactions/formatBlockTimestamp.ts
new file mode 100644
index 0000000..132cbb4
--- /dev/null
+++ b/apps/bridge/src/utils/transactions/formatBlockTimestamp.ts
@@ -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 };
+}
diff --git a/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts b/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts
index 2338d39..85e331e 100644
--- a/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts
+++ b/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts
@@ -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;
}
diff --git a/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts b/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts
index af678dc..7ce72a6 100644
--- a/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts
+++ b/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts
@@ -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;
}
diff --git a/apps/bridge/src/utils/transactions/phase.ts b/apps/bridge/src/utils/transactions/phase.ts
index 708e978..2d275c5 100644
--- a/apps/bridge/src/utils/transactions/phase.ts
+++ b/apps/bridge/src/utils/transactions/phase.ts
@@ -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 };
diff --git a/yarn.lock b/yarn.lock
index 2a55a50..cbe24b0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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