mirror of
https://github.com/placeholder-soft/web.git
synced 2026-01-12 22:45:00 +08:00
[feat] native USDC bridging (#38)
* cctp bridging part 1: deposits * small optimization * part 2: finalize deposits * add erc20spender * fix build * show correct statuses for deposits * withdrawal functionality * change copy * rebase * update copy, add modals, complete ux * dry
This commit is contained in:
@@ -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
|
||||
@@ -13,5 +13,6 @@ module.exports = {
|
||||
rules: {
|
||||
// Does not work with `:` aliases
|
||||
'import/extensions': 'off',
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
8
apps/bridge/global.d.ts
vendored
8
apps/bridge/global.d.ts
vendored
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
|
||||
import { useBlockNumberOfLatestL2OutputProposal } from 'apps/bridge/src/utils/hooks/useBlockNumberOfLatestL2OutputProposal';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { FinalizeCCTPBridgeModal } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeModal';
|
||||
|
||||
const COLUMNS = ['Time', 'Type', 'Amount', 'Phase', 'Status'];
|
||||
|
||||
@@ -27,7 +28,10 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
|
||||
const blockNumberOfLatestL2OutputProposal = useBlockNumberOfLatestL2OutputProposal();
|
||||
|
||||
const [modalProveTxHash, setModalProveTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [modalFinalizeTxHash, setModalFinalizeTxHash] = useState<`0x${string}` | undefined>(
|
||||
const [modalFinalizeOPTxHash, setModalFinalizeOPTxHash] = useState<`0x${string}` | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [modalFinalizeCCTPTxHash, setModalFinalizeCCTPTxHash] = useState<`0x${string}` | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
@@ -41,6 +45,11 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
|
||||
onOpen: onOpenFinalizeWithdrawalModal,
|
||||
onClose: onCloseFinalizeWithdrawalModal,
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isFinalizeCCTPBridgeModalOpen,
|
||||
onOpen: onOpenFinalizeCCTPBridgeModal,
|
||||
onClose: onCloseFinalizeCCTPBridgeModal,
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full overflow-auto">
|
||||
@@ -52,13 +61,26 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
|
||||
<FinalizeWithdrawalModal
|
||||
isOpen={isFinalizeWithdrawalModalOpen}
|
||||
onClose={onCloseFinalizeWithdrawalModal}
|
||||
finalizeTxHash={modalFinalizeTxHash}
|
||||
finalizeTxHash={modalFinalizeOPTxHash}
|
||||
/>
|
||||
<FinalizeCCTPBridgeModal
|
||||
isOpen={isFinalizeCCTPBridgeModalOpen}
|
||||
onClose={onCloseFinalizeCCTPBridgeModal}
|
||||
finalizeTxHash={modalFinalizeCCTPTxHash}
|
||||
/>
|
||||
<Table
|
||||
head={COLUMNS}
|
||||
rows={transactions.map((transaction) => {
|
||||
if (transaction.type === 'Deposit') {
|
||||
return <DepositRow key={transaction.hash} transaction={transaction} />;
|
||||
return (
|
||||
<DepositRow
|
||||
key={transaction.hash}
|
||||
transaction={transaction}
|
||||
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
|
||||
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
|
||||
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WithdrawalRow
|
||||
@@ -69,8 +91,11 @@ const TransactionsTable = memo(function TransactionsTable({ transactions }: Tran
|
||||
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
|
||||
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
|
||||
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
|
||||
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
|
||||
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
|
||||
setModalProveTxHash={setModalProveTxHash}
|
||||
setModalFinalizeTxHash={setModalFinalizeTxHash}
|
||||
setModalFinalizeOPTxHash={setModalFinalizeOPTxHash}
|
||||
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
type BadgeStatus = 'NOT_STARTED' | 'STARTED' | 'DONE';
|
||||
|
||||
type StepBadgeProps = {
|
||||
num: string;
|
||||
label: string;
|
||||
status: BadgeStatus;
|
||||
};
|
||||
|
||||
const BadgeClassNames = {
|
||||
NOT_STARTED: '',
|
||||
STARTED: 'border-white text-white',
|
||||
DONE: 'border-white bg-white text-black font-bold',
|
||||
};
|
||||
const LabelClassNames = {
|
||||
NOT_STARTED: '',
|
||||
STARTED: 'text-white',
|
||||
DONE: 'text-white',
|
||||
};
|
||||
|
||||
function StepBadge({ num, label, status }: StepBadgeProps) {
|
||||
const badgeClassNames = BadgeClassNames[status];
|
||||
const labelClassNames = LabelClassNames[status];
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className={`h-6 w-6 rounded-full border-2 leading-6 ${badgeClassNames}`}>
|
||||
<span className="align-super text-sm">{status === 'DONE' ? '✓' : num}</span>
|
||||
</div>
|
||||
<span className={`font-sans text-base font-medium ${labelClassNames}`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type BarStatus = 'REQUEST_SENT' | 'VERIFIED';
|
||||
type CCTPBridgeProgressBarProps = {
|
||||
status: BarStatus;
|
||||
};
|
||||
|
||||
const BarStatusToBadgeStatuses: Record<BarStatus, BadgeStatus[]> = {
|
||||
REQUEST_SENT: ['STARTED', 'NOT_STARTED'],
|
||||
VERIFIED: ['DONE', 'STARTED'],
|
||||
};
|
||||
|
||||
const DisclaimerContent: Record<BarStatus, ReactNode> = {
|
||||
REQUEST_SENT: (
|
||||
<>
|
||||
USDC deposits and withdrawals use Circle's CCTP. After you initiate a deposit or
|
||||
withdrawal, you must complete the bridge in order to access your funds, on{' '}
|
||||
<Link href="/transactions" className="underline">
|
||||
the transactions page
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
VERIFIED:
|
||||
'It takes a few minutes for the transaction to complete onchain. After this period, you can access your funds.',
|
||||
};
|
||||
|
||||
export function CCTPBridgeProgressBar({ status }: CCTPBridgeProgressBarProps) {
|
||||
const badgeStatuses = BarStatusToBadgeStatuses[status];
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="mt-8 flex flex-row justify-around gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<StepBadge num="1" status={badgeStatuses[0]} label="Send request" />
|
||||
<span>Takes a few minutes</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<StepBadge num="2" status={badgeStatuses[1]} label="Complete" />
|
||||
<span>Takes a few minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-base">{DisclaimerContent[status]}</span>
|
||||
<span className="text-white underline">
|
||||
<Link href="https://developers.circle.com/stablecoin/docs/cctp-getting-started">
|
||||
Learn more
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BridgeButton } from 'apps/bridge/src/components/BridgeButton/BridgeButton';
|
||||
import { BridgeInput } from 'apps/bridge/src/components/BridgeInput/BridgeInput';
|
||||
import { BridgeToInput } from 'apps/bridge/src/components/BridgeToInput/BridgeToInput';
|
||||
@@ -8,7 +8,6 @@ import { FaqSidebar } from 'apps/bridge/src/components/Faq/FaqSidebar';
|
||||
import { BaseButton } from 'apps/bridge/src/components/SwitchNetworkButton/SwitchNetworkButton';
|
||||
import { TransactionSummary } from 'apps/bridge/src/components/TransactionSummary/TransactionSummary';
|
||||
import { Asset } from 'apps/bridge/src/types/Asset';
|
||||
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
|
||||
import { useApproveContract } from 'apps/bridge/src/utils/hooks/useApproveContract';
|
||||
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
|
||||
import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
|
||||
@@ -20,29 +19,36 @@ import { usePrepareERC20Deposit } from 'apps/bridge/src/utils/hooks/usePrepareER
|
||||
import { usePrepareERC20DepositTo } from 'apps/bridge/src/utils/hooks/usePrepareERC20DepositTo';
|
||||
import { usePrepareETHDeposit } from 'apps/bridge/src/utils/hooks/usePrepareETHDeposit';
|
||||
import { isAddress, parseUnits } from 'viem';
|
||||
import { waitForTransaction } from 'wagmi/actions';
|
||||
import getConfig from 'next/config';
|
||||
import { useAccount, useBalance, useContractWrite } from 'wagmi';
|
||||
import { useAccount, useBalance, useContractWrite, usePublicClient, useSwitchNetwork } from 'wagmi';
|
||||
import { writeContract } from 'wagmi/actions';
|
||||
import { useIsPermittedToBridgeTo } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridgeTo';
|
||||
import { getL1NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL1NetworkForChainEnv';
|
||||
import { getL2NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL2NetworkForChainEnv';
|
||||
|
||||
const assetList = getAssetListForChainEnv();
|
||||
import { getDepositAssetsForChainEnv } from 'apps/bridge/src/utils/assets/getDepositAssetsForChainEnv';
|
||||
import { usePrepareInitiateCCTPBridge } from 'apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const activeAssets = getDepositAssetsForChainEnv();
|
||||
|
||||
const chainId = parseInt(publicRuntimeConfig.l1ChainID);
|
||||
|
||||
export function DepositContainer() {
|
||||
const [depositAmount, setDepositAmount] = useState('0');
|
||||
const [depositAmount, setDepositAmount] = useState('');
|
||||
const [L1ApproveTxHash, setL1ApproveTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [L1DepositTxHash, setL1DepositTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [depositTo, setDepositTo] = useState('');
|
||||
const [isApprovalTx, setIsApprovalTx] = useState(false);
|
||||
const isWalletConnected = useIsWalletConnected();
|
||||
const [selectedAsset, setSelectedAsset] = useState<Asset>(assetList[0]);
|
||||
const activeAssets = assetList.filter((asset) =>
|
||||
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
|
||||
);
|
||||
const [selectedAsset, setSelectedAsset] = useState<Asset>(activeAssets[0]);
|
||||
const publicClient = usePublicClient({ chainId });
|
||||
const { switchNetwork } = useSwitchNetwork();
|
||||
|
||||
useEffect(() => {
|
||||
switchNetwork?.(chainId);
|
||||
}, [switchNetwork]);
|
||||
|
||||
const { address } = useAccount();
|
||||
const codeAtAddress = useGetCode(chainId, address);
|
||||
const isSmartContractWallet = !!codeAtAddress && codeAtAddress !== '0x';
|
||||
@@ -53,9 +59,16 @@ export function DepositContainer() {
|
||||
chainId: parseInt(publicRuntimeConfig.l1ChainID),
|
||||
});
|
||||
|
||||
const erc20Spender =
|
||||
selectedAsset.protocol === 'CCTP'
|
||||
? publicRuntimeConfig.l1CCTPTokenMessengerAddress
|
||||
: publicRuntimeConfig.l1BridgeProxyAddress;
|
||||
|
||||
const { data: readERC20Approval, error: readERC20ApprovalError } = useIsContractApproved({
|
||||
contactAddress: selectedAsset.L1contract,
|
||||
address,
|
||||
spender: erc20Spender,
|
||||
bridgeDirection: 'deposit',
|
||||
});
|
||||
|
||||
const readApprovalResult = useMemo(() => {
|
||||
@@ -75,15 +88,18 @@ export function DepositContainer() {
|
||||
const handleCloseDepositModal = useCallback(() => {
|
||||
onCloseDepositModal();
|
||||
setL1DepositTxHash(undefined);
|
||||
setL1ApproveTxHash(undefined);
|
||||
setIsApprovalTx(false);
|
||||
}, [onCloseDepositModal]);
|
||||
|
||||
// approve erc20
|
||||
const approveConfig = useApproveContract({
|
||||
contractAddress: selectedAsset.L1contract,
|
||||
spender: erc20Spender,
|
||||
approveAmount: depositAmount,
|
||||
decimals: selectedAsset.decimals,
|
||||
bridgeDirection: 'deposit',
|
||||
});
|
||||
|
||||
const { writeAsync: approveWrite } = useContractWrite(approveConfig);
|
||||
|
||||
const chainEnv = useChainEnv();
|
||||
@@ -102,7 +118,6 @@ export function DepositContainer() {
|
||||
isPermittedToBridge,
|
||||
includeTosVersionByte,
|
||||
});
|
||||
|
||||
const { writeAsync: depositETHWrite } = useContractWrite(depositETHConfig);
|
||||
|
||||
// deposit erc20
|
||||
@@ -121,10 +136,21 @@ export function DepositContainer() {
|
||||
isPermittedToBridge,
|
||||
includeTosVersionByte,
|
||||
});
|
||||
|
||||
const { writeAsync: depositERC20Write } = useContractWrite(depositERC20Config);
|
||||
const { writeAsync: depositERC20ToWrite } = useContractWrite(depositERC20ToConfig);
|
||||
|
||||
// deposit using CCTP (eg USDC)
|
||||
const depositCCTPAssetConfig = usePrepareInitiateCCTPBridge({
|
||||
mintRecipient: isSmartContractWallet ? (depositTo as `0x${string}`) : address,
|
||||
asset: selectedAsset,
|
||||
amount: depositAmount,
|
||||
destinationDomain: parseInt(publicRuntimeConfig.l2CCTPDomain),
|
||||
isPermittedToBridge,
|
||||
includeTosVersionByte,
|
||||
bridgeDirection: 'deposit',
|
||||
});
|
||||
const { writeAsync: depositCCTPAssetWrite } = useContractWrite(depositCCTPAssetConfig);
|
||||
|
||||
const initiateApproval = useCallback(() => {
|
||||
void (async () => {
|
||||
setIsApprovalTx(true);
|
||||
@@ -134,19 +160,27 @@ export function DepositContainer() {
|
||||
if (approveResult?.hash) {
|
||||
const approveTxHash: `0x${string}` = approveResult.hash;
|
||||
setL1ApproveTxHash(approveTxHash);
|
||||
// wait for confirmations
|
||||
await waitForTransaction({ hash: approveResult?.hash });
|
||||
}
|
||||
|
||||
// next, call the transfer function
|
||||
setIsApprovalTx(false);
|
||||
const depositResult = await (isSmartContractWallet
|
||||
? depositERC20ToWrite?.()
|
||||
: depositERC20Write?.());
|
||||
if (depositResult?.hash) {
|
||||
const depositTxHash = depositResult.hash;
|
||||
setL1DepositTxHash(depositTxHash);
|
||||
setDepositAmount('0');
|
||||
// wait for confirmations
|
||||
await publicClient.waitForTransactionReceipt({ hash: approveResult.hash });
|
||||
|
||||
// next, call the transfer function
|
||||
setIsApprovalTx(false);
|
||||
|
||||
let depositMethod;
|
||||
if (selectedAsset.protocol === 'CCTP') {
|
||||
// because of how React works we need to use the writeContract wagmi/core action
|
||||
// here (the hook still thinks the approval has not been set)
|
||||
depositMethod = async () => await writeContract(depositCCTPAssetConfig);
|
||||
} else {
|
||||
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
|
||||
}
|
||||
const depositResult = await depositMethod?.();
|
||||
if (depositResult?.hash) {
|
||||
const depositTxHash = depositResult.hash;
|
||||
setL1DepositTxHash(depositTxHash);
|
||||
setDepositAmount('');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
onCloseDepositModal();
|
||||
@@ -154,11 +188,14 @@ export function DepositContainer() {
|
||||
})();
|
||||
}, [
|
||||
approveWrite,
|
||||
depositCCTPAssetConfig,
|
||||
depositERC20ToWrite,
|
||||
depositERC20Write,
|
||||
isSmartContractWallet,
|
||||
onCloseDepositModal,
|
||||
onOpenDepositModal,
|
||||
publicClient,
|
||||
selectedAsset.protocol,
|
||||
]);
|
||||
|
||||
const initiateDeposit = useCallback(() => {
|
||||
@@ -169,7 +206,11 @@ export function DepositContainer() {
|
||||
if (isPermittedToBridge) {
|
||||
let depositMethod;
|
||||
if (selectedAsset.L1contract) {
|
||||
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
|
||||
if (selectedAsset.protocol === 'CCTP') {
|
||||
depositMethod = depositCCTPAssetWrite;
|
||||
} else {
|
||||
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
|
||||
}
|
||||
} else {
|
||||
depositMethod = depositETHWrite;
|
||||
}
|
||||
@@ -177,7 +218,7 @@ export function DepositContainer() {
|
||||
if (depositResult?.hash) {
|
||||
const depositTxHash = depositResult.hash;
|
||||
setL1DepositTxHash(depositTxHash);
|
||||
setDepositAmount('0');
|
||||
setDepositAmount('');
|
||||
}
|
||||
} else {
|
||||
onCloseDepositModal();
|
||||
@@ -190,6 +231,8 @@ export function DepositContainer() {
|
||||
onOpenDepositModal,
|
||||
isPermittedToBridge,
|
||||
selectedAsset.L1contract,
|
||||
selectedAsset.protocol,
|
||||
depositCCTPAssetWrite,
|
||||
isSmartContractWallet,
|
||||
depositERC20ToWrite,
|
||||
depositERC20Write,
|
||||
@@ -245,6 +288,7 @@ export function DepositContainer() {
|
||||
L1ApproveTxHash={L1ApproveTxHash}
|
||||
L1DepositTxHash={L1DepositTxHash}
|
||||
isApprovalTx={isApprovalTx}
|
||||
protocol={selectedAsset.protocol}
|
||||
/>
|
||||
<BridgeInput
|
||||
inputNetwork={getL1NetworkForChainEnv()}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { CCTPBridgeProgressBar } from 'apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar';
|
||||
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
|
||||
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
|
||||
import getConfig from 'next/config';
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
import { useWaitForTransaction } from 'wagmi';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
@@ -11,6 +14,7 @@ type DepositModalProps = {
|
||||
L1ApproveTxHash: `0x${string}` | undefined;
|
||||
L1DepositTxHash: `0x${string}` | undefined;
|
||||
isApprovalTx: boolean;
|
||||
protocol: BridgeProtocol;
|
||||
};
|
||||
|
||||
type STATE =
|
||||
@@ -18,7 +22,8 @@ type STATE =
|
||||
| 'APPROVAL_CONFIRMED'
|
||||
| 'APPROVAL_NOT_STARTED'
|
||||
| 'DEPOSIT_LOADING'
|
||||
| 'DEPOSIT_CONFIRMED'
|
||||
| 'OP_DEPOSIT_STARTED'
|
||||
| 'CCTP_DEPOSIT_STARTED'
|
||||
| 'DEPOSIT_NOT_STARTED';
|
||||
|
||||
function getState(
|
||||
@@ -27,6 +32,7 @@ function getState(
|
||||
isApproveSuccess: boolean,
|
||||
isDepositLoading: boolean,
|
||||
isDepositSuccess: boolean,
|
||||
protocol: BridgeProtocol,
|
||||
): STATE {
|
||||
if (isApprovalTx && isApproveLoading) {
|
||||
return 'APPROVAL_LOADING';
|
||||
@@ -41,19 +47,19 @@ function getState(
|
||||
return 'DEPOSIT_LOADING';
|
||||
}
|
||||
if (!isApprovalTx && isDepositSuccess) {
|
||||
return 'DEPOSIT_CONFIRMED';
|
||||
return protocol === 'CCTP' ? 'CCTP_DEPOSIT_STARTED' : 'OP_DEPOSIT_STARTED';
|
||||
}
|
||||
|
||||
return 'DEPOSIT_NOT_STARTED';
|
||||
}
|
||||
|
||||
const ModalContents = {
|
||||
const ModalContents: Record<STATE, ReactNode> = {
|
||||
APPROVAL_NOT_STARTED: 'Approval will initiate after confirmation.',
|
||||
APPROVAL_LOADING: 'Waiting for confirmations...',
|
||||
APPROVAL_CONFIRMED: 'Transaction confirmed.',
|
||||
DEPOSIT_NOT_STARTED: 'Deposit will initiate after confirmation.',
|
||||
DEPOSIT_LOADING: 'Waiting for confirmations...',
|
||||
DEPOSIT_CONFIRMED: (
|
||||
OP_DEPOSIT_STARTED: (
|
||||
<>
|
||||
Deposits typically take a few minutes to reach the Base network. When this is complete, you
|
||||
can view this transaction at{' '}
|
||||
@@ -63,24 +69,27 @@ const ModalContents = {
|
||||
.
|
||||
</>
|
||||
),
|
||||
CCTP_DEPOSIT_STARTED: <CCTPBridgeProgressBar status="REQUEST_SENT" />,
|
||||
};
|
||||
|
||||
const Titles = {
|
||||
const Titles: Record<STATE, string> = {
|
||||
APPROVAL_NOT_STARTED: 'APPROVE IN YOUR WALLET',
|
||||
APPROVAL_LOADING: 'APPROVING',
|
||||
APPROVAL_CONFIRMED: 'APPROVED',
|
||||
DEPOSIT_NOT_STARTED: 'CONFIRM DEPOSIT IN WALLET',
|
||||
DEPOSIT_LOADING: 'CONFIRMING',
|
||||
DEPOSIT_CONFIRMED: 'DEPOSIT IN TRANSIT TO BASE',
|
||||
OP_DEPOSIT_STARTED: 'DEPOSIT IN TRANSIT TO BASE',
|
||||
CCTP_DEPOSIT_STARTED: 'DEPOSIT IN PROGRESS',
|
||||
};
|
||||
|
||||
const Icons = {
|
||||
const Icons: Record<STATE, string> = {
|
||||
APPROVAL_NOT_STARTED: 'wallet',
|
||||
APPROVAL_LOADING: 'wallet',
|
||||
APPROVAL_CONFIRMED: 'confirm',
|
||||
DEPOSIT_NOT_STARTED: 'wallet',
|
||||
DEPOSIT_LOADING: 'wallet',
|
||||
DEPOSIT_CONFIRMED: 'deposit',
|
||||
OP_DEPOSIT_STARTED: 'deposit',
|
||||
CCTP_DEPOSIT_STARTED: '',
|
||||
};
|
||||
|
||||
export function DepositModal({
|
||||
@@ -89,6 +98,7 @@ export function DepositModal({
|
||||
L1ApproveTxHash,
|
||||
L1DepositTxHash,
|
||||
isApprovalTx,
|
||||
protocol,
|
||||
}: DepositModalProps) {
|
||||
const { isLoading: isApproveLoading, isSuccess: isApproveSuccess } = useWaitForTransaction({
|
||||
hash: L1ApproveTxHash,
|
||||
@@ -104,6 +114,7 @@ export function DepositModal({
|
||||
isApproveSuccess,
|
||||
isDepositLoading,
|
||||
isDepositSuccess,
|
||||
protocol,
|
||||
);
|
||||
|
||||
const L1TxHash = isApprovalTx ? L1ApproveTxHash : L1DepositTxHash;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { DepositPhase, WithdrawalPhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
|
||||
import type {
|
||||
CCTPBridgePhase,
|
||||
DepositPhase,
|
||||
WithdrawalPhase,
|
||||
} from 'apps/bridge/src/utils/transactions/phase';
|
||||
import Image from 'next/image';
|
||||
|
||||
const networkSvgPaths: Record<'deposit' | 'withdraw', string> = {
|
||||
@@ -21,22 +26,53 @@ const phaseSvgPaths: Record<WithdrawalPhase | DepositPhase, string> = {
|
||||
FUNDS_DEPOSITED: '/icons/phases/send.svg',
|
||||
};
|
||||
|
||||
const ccptPhasePaths: Record<CCTPBridgePhase, Record<'deposit' | 'withdraw', string>> = {
|
||||
INITIATE_CCTP_BRIDGE_PENDING: {
|
||||
deposit: '/icons/phases/send.svg',
|
||||
withdraw: '/icons/phases/receive.svg',
|
||||
},
|
||||
INITIATE_CCTP_BRIDGE_FAILED: {
|
||||
deposit: '/icons/phases/user_action.svg',
|
||||
withdraw: '/icons/phases/user_action.svg',
|
||||
},
|
||||
FINALIZE_CCTP_BRIDGE: {
|
||||
deposit: '/icons/phases/user_action.svg',
|
||||
withdraw: '/icons/phases/user_action.svg',
|
||||
},
|
||||
FINALIZE_CCTP_BRIDGE_PENDING: {
|
||||
deposit: '/icons/phases/wait.svg',
|
||||
withdraw: '/icons/phases/wait.svg',
|
||||
},
|
||||
FINALIZE_CCTP_BRIDGE_FAILED: {
|
||||
deposit: '/icons/phases/user_action.svg',
|
||||
withdraw: '/icons/phases/user_action.svg',
|
||||
},
|
||||
CCTP_BRIDGE_COMPLETE: {
|
||||
deposit: '/icons/phases/send.svg',
|
||||
withdraw: '/icons/phases/receive.svg',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
phase: DepositPhase | WithdrawalPhase;
|
||||
phase: DepositPhase | WithdrawalPhase | CCTPBridgePhase;
|
||||
protocol?: BridgeProtocol;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export function TransactionIcon({ phase, bridgeDirection, size = 32 }: Props) {
|
||||
const phaseIcon = (
|
||||
<Image src={phaseSvgPaths[phase]} width={size} height={size} alt={phase.toLowerCase()} />
|
||||
);
|
||||
export function TransactionIcon({ phase, protocol, bridgeDirection, size = 32 }: Props) {
|
||||
const icon =
|
||||
protocol === 'CCTP'
|
||||
? ccptPhasePaths[phase as CCTPBridgePhase][bridgeDirection]
|
||||
: phaseSvgPaths[phase as DepositPhase | WithdrawalPhase];
|
||||
|
||||
const phaseIcon = <Image src={icon} width={size} height={size} alt={phase.toLowerCase()} />;
|
||||
|
||||
return (
|
||||
<div className="relative justify-center pt-1 pr-2">
|
||||
<div className="relative justify-center pr-2 pt-1">
|
||||
{phaseIcon}
|
||||
<Image
|
||||
className="absolute -right-[2px] -bottom-[4px] rounded-[14px] border-2 border-black"
|
||||
className="absolute -bottom-[4px] -right-[2px] rounded-[14px] border-2 border-black"
|
||||
src={networkSvgPaths[bridgeDirection]}
|
||||
width={20}
|
||||
height={20}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
|
||||
import { CCTPBridgePhase, WithdrawalPhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import { memo } from 'react';
|
||||
|
||||
const PHASE_MAP: Record<WithdrawalPhase | CCTPBridgePhase, number> = {
|
||||
PROPOSING_ON_CHAIN: 1,
|
||||
PROVE: 2,
|
||||
PROVE_TX_PENDING: 2,
|
||||
PROVE_TX_FAILURE: 2,
|
||||
CHALLENGE_WINDOW: 2,
|
||||
FINALIZE: 3,
|
||||
FINALIZE_TX_PENDING: 3,
|
||||
FINALIZE_TX_FAILURE: 3,
|
||||
FUNDS_WITHDRAWN: 4,
|
||||
INITIATE_CCTP_BRIDGE_PENDING: 1,
|
||||
INITIATE_CCTP_BRIDGE_FAILED: 1,
|
||||
FINALIZE_CCTP_BRIDGE: 2,
|
||||
FINALIZE_CCTP_BRIDGE_PENDING: 2,
|
||||
FINALIZE_CCTP_BRIDGE_FAILED: 2,
|
||||
CCTP_BRIDGE_COMPLETE: 3,
|
||||
};
|
||||
|
||||
function generatePhaseIndicator(
|
||||
phase: WithdrawalPhase | CCTPBridgePhase,
|
||||
protocol?: BridgeProtocol,
|
||||
): JSX.Element[] {
|
||||
const rv: JSX.Element[] = [];
|
||||
const totalPhases = protocol === 'CCTP' ? 3 : 4;
|
||||
const indicatorWidth = protocol === 'CCTP' ? `w-[calc(8rem/3)]` : `w-[calc(8rem/4)]`;
|
||||
|
||||
for (let i = 0; i < PHASE_MAP[phase]; i += 1) {
|
||||
rv.push(<div className={`border-gray-400 border-t-4 ${indicatorWidth}`} key={`fill-${i}`} />);
|
||||
}
|
||||
|
||||
for (let i = 0; i < totalPhases - PHASE_MAP[phase]; i += 1) {
|
||||
rv.push(
|
||||
<div
|
||||
className={`border-gray-400 border-t-4 text-cds-background-gray-60 ${indicatorWidth}`}
|
||||
key={`back-${i}`}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
type BridgePhaseIndicatorProps = {
|
||||
phase: WithdrawalPhase | CCTPBridgePhase;
|
||||
protocol?: BridgeProtocol;
|
||||
};
|
||||
|
||||
// Component to generate the phase indicator for any multi-step bridge transaction.
|
||||
// This currently includes OP bridge withdrawals and CCTP bridge transactions in either direction.
|
||||
export const BridgePhaseIndicator = memo(function BridgePhaseIndicator({
|
||||
phase,
|
||||
protocol,
|
||||
}: BridgePhaseIndicatorProps) {
|
||||
return (
|
||||
<div className="flex h-6 grow flex-row items-center gap-1">
|
||||
{generatePhaseIndicator(phase, protocol)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
|
||||
import { BridgePhaseIndicator } from 'apps/bridge/src/components/Transactions/BridgePhaseIndicator';
|
||||
import { CCTPBridgeStatus } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeStatus';
|
||||
import { cctpBridgePhaseText } from 'apps/bridge/src/constants/phaseText';
|
||||
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
|
||||
import { useCCTPBridgeStatus } from 'apps/bridge/src/utils/hooks/useCCTPBridgeStatus';
|
||||
import { useGetUSDAmount } from 'apps/bridge/src/utils/hooks/useGetUSDAmount';
|
||||
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
|
||||
import { formatTimestamp } from 'apps/bridge/src/utils/transactions/formatBlockTimestamp';
|
||||
import {
|
||||
blockExplorerUrlForL1Transaction,
|
||||
blockExplorerUrlForL2Transaction,
|
||||
} from 'apps/bridge/src/utils/url/blockExplorer';
|
||||
import { formatUnits } from 'viem';
|
||||
import { Dispatch, SetStateAction, memo } from 'react';
|
||||
|
||||
type CCTPBridgeRowProps = {
|
||||
transaction: BridgeTransaction;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
onOpenFinalizeCCTPBridgeModal: () => void;
|
||||
onCloseFinalizeCCTPBridgeModal: () => void;
|
||||
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
};
|
||||
|
||||
// Transactions table row component for Bridges made using Circle's CCTP.
|
||||
export const CCTPBridgeRow = memo(function CCTPBridgeRow({
|
||||
transaction,
|
||||
bridgeDirection,
|
||||
onOpenFinalizeCCTPBridgeModal,
|
||||
onCloseFinalizeCCTPBridgeModal,
|
||||
setModalFinalizeCCTPTxHash,
|
||||
}: CCTPBridgeRowProps) {
|
||||
const { date, shortDate, time } = formatTimestamp(transaction.blockTimestamp);
|
||||
const { status, setStatus, message, attestation } = useCCTPBridgeStatus({
|
||||
initiateTxHash: transaction.hash,
|
||||
bridgeDirection,
|
||||
});
|
||||
|
||||
const explorerURL =
|
||||
bridgeDirection === 'deposit'
|
||||
? blockExplorerUrlForL1Transaction(transaction.hash)
|
||||
: blockExplorerUrlForL2Transaction(transaction.hash);
|
||||
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
|
||||
|
||||
const bridgeAmount = formatUnits(BigInt(transaction.amount), transaction.assetDecimals ?? 18);
|
||||
const amountFiat = useGetUSDAmount(transaction.priceApiId, bridgeAmount);
|
||||
|
||||
return (
|
||||
<tr className="mb-4 grid grid-cols-2 grid-rows-2 md:table-row">
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection={bridgeDirection} phase={status} protocol="CCTP" />
|
||||
<div className="flex flex-col">
|
||||
<p>{date ?? ''}</p>
|
||||
<p>{time ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - left column */}
|
||||
<td className="md:hidden">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection={bridgeDirection} phase={status} protocol="CCTP" />
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="whitespace-nowrap font-sans text-sm"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
<div className="flex flex-row text-cds-background-gray-60">
|
||||
{transaction.type}
|
||||
<h3>•</h3>
|
||||
<p>{shortDate ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
{transaction.type}
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="text-cds-background-gray-60 underline"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - right column */}
|
||||
<td className="md:hidden">
|
||||
<div className="flex flex-col items-end">
|
||||
<div>{`${bridgeAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div>{`${bridgeAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<BridgePhaseIndicator phase={status} protocol="CCTP" />
|
||||
<div className="text-cds-background-gray-60">{cctpBridgePhaseText[status]}</div>
|
||||
</div>
|
||||
</td>
|
||||
<CCTPBridgeStatus
|
||||
phase={status}
|
||||
message={message}
|
||||
attestation={attestation}
|
||||
bridgeDirection={bridgeDirection}
|
||||
setStatus={setStatus}
|
||||
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
|
||||
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
|
||||
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Dispatch, SetStateAction, memo } from 'react';
|
||||
import { PendingButton } from 'apps/bridge/src/components/Transactions/PendingButton';
|
||||
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import { FinalizeCCTPBridgeButton } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/FinalizeCCTPBridgeButton';
|
||||
|
||||
type CCTPBridgeStatusProps = {
|
||||
phase: CCTPBridgePhase;
|
||||
message?: `0x${string}`;
|
||||
attestation?: `0x${string}`;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
setStatus: (status: CCTPBridgePhase) => void;
|
||||
onOpenFinalizeCCTPBridgeModal: () => void;
|
||||
onCloseFinalizeCCTPBridgeModal: () => void;
|
||||
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
};
|
||||
|
||||
export const CCTPBridgeStatus = memo(function CCTPBridgeStatus({
|
||||
phase,
|
||||
message,
|
||||
attestation,
|
||||
bridgeDirection,
|
||||
setStatus,
|
||||
onOpenFinalizeCCTPBridgeModal,
|
||||
onCloseFinalizeCCTPBridgeModal,
|
||||
setModalFinalizeCCTPTxHash,
|
||||
}: CCTPBridgeStatusProps) {
|
||||
const isPending =
|
||||
phase === 'INITIATE_CCTP_BRIDGE_PENDING' || phase === 'FINALIZE_CCTP_BRIDGE_PENDING';
|
||||
// If the finalize tx failed we can just try again
|
||||
const isReadyToFinazlie =
|
||||
phase === 'FINALIZE_CCTP_BRIDGE' || phase === 'FINALIZE_CCTP_BRIDGE_FAILED';
|
||||
const isFailed = phase === 'INITIATE_CCTP_BRIDGE_FAILED';
|
||||
|
||||
// Return pending button if initiate or finalize tx is pending
|
||||
if (isPending) return <PendingButton />;
|
||||
|
||||
// Return 'Ready to finalize' if finalize tx is ready to be sent
|
||||
if (isReadyToFinazlie)
|
||||
return (
|
||||
<FinalizeCCTPBridgeButton
|
||||
message={message}
|
||||
attestation={attestation}
|
||||
bridgeDirection={bridgeDirection}
|
||||
setStatus={setStatus}
|
||||
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
|
||||
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
|
||||
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
|
||||
/>
|
||||
);
|
||||
|
||||
// Return 'Failed' if initiate tx failed
|
||||
if (isFailed) return 'Failed';
|
||||
|
||||
// Otherwise the bridge is complete
|
||||
return <span className="text-cds-background-gray-60">Complete</span>;
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Dispatch, SetStateAction, memo, useCallback } from 'react';
|
||||
import { usePrepareFinalizeCCTPBridge } from 'apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge';
|
||||
import { useIsPermittedToBridge } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridge';
|
||||
import { useContractWrite, useNetwork, usePublicClient, useSwitchNetwork } from 'wagmi';
|
||||
import getConfig from 'next/config';
|
||||
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const l1ChainID = parseInt(publicRuntimeConfig.l1ChainID);
|
||||
const l2ChainID = parseInt(publicRuntimeConfig.l2ChainID);
|
||||
|
||||
type FinalizeCCTPBridgeButtonProps = {
|
||||
message?: `0x${string}`;
|
||||
attestation?: `0x${string}`;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
setStatus: (status: CCTPBridgePhase) => void;
|
||||
onOpenFinalizeCCTPBridgeModal: () => void;
|
||||
onCloseFinalizeCCTPBridgeModal: () => void;
|
||||
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
};
|
||||
|
||||
export const FinalizeCCTPBridgeButton = memo(function FinalizeCCTPBridgeButton({
|
||||
message,
|
||||
attestation,
|
||||
bridgeDirection,
|
||||
setStatus,
|
||||
onOpenFinalizeCCTPBridgeModal,
|
||||
onCloseFinalizeCCTPBridgeModal,
|
||||
setModalFinalizeCCTPTxHash,
|
||||
}: FinalizeCCTPBridgeButtonProps) {
|
||||
const { chain } = useNetwork();
|
||||
const { switchNetwork } = useSwitchNetwork();
|
||||
const isPermittedToBridge = useIsPermittedToBridge();
|
||||
const publicClient = usePublicClient({
|
||||
chainId: bridgeDirection === 'deposit' ? l2ChainID : l1ChainID,
|
||||
});
|
||||
const chainEnv = useChainEnv();
|
||||
const isMainnet = chainEnv === 'mainnet';
|
||||
const includeTosVersionByte = isMainnet;
|
||||
|
||||
const finalizeCCTPBridgeConfig = usePrepareFinalizeCCTPBridge({
|
||||
isPermittedToBridge,
|
||||
message,
|
||||
attestation,
|
||||
bridgeDirection,
|
||||
includeTosVersionByte,
|
||||
});
|
||||
const { writeAsync: finalizeCCTPBridge } = useContractWrite(finalizeCCTPBridgeConfig);
|
||||
|
||||
const handleSwitchToCorrectNetwork = useCallback(() => {
|
||||
switchNetwork?.(bridgeDirection === 'deposit' ? l2ChainID : l1ChainID);
|
||||
}, [bridgeDirection, switchNetwork]);
|
||||
|
||||
const handleFinalizeCCTPBridge = useCallback(() => {
|
||||
setModalFinalizeCCTPTxHash(undefined);
|
||||
onOpenFinalizeCCTPBridgeModal();
|
||||
void (async () => {
|
||||
try {
|
||||
if (isPermittedToBridge) {
|
||||
const finalizeResult = await finalizeCCTPBridge?.();
|
||||
setStatus('FINALIZE_CCTP_BRIDGE_PENDING');
|
||||
if (finalizeResult?.hash) {
|
||||
const finalizeTxHash = finalizeResult.hash;
|
||||
setModalFinalizeCCTPTxHash(finalizeTxHash);
|
||||
const finalizeTxReceipt = await publicClient.waitForTransactionReceipt({
|
||||
hash: finalizeResult.hash,
|
||||
});
|
||||
setStatus(
|
||||
finalizeTxReceipt?.status === 'success'
|
||||
? 'CCTP_BRIDGE_COMPLETE'
|
||||
: 'FINALIZE_CCTP_BRIDGE_FAILED',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onCloseFinalizeCCTPBridgeModal();
|
||||
}
|
||||
} catch {
|
||||
setStatus('FINALIZE_CCTP_BRIDGE_FAILED');
|
||||
onCloseFinalizeCCTPBridgeModal();
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
finalizeCCTPBridge,
|
||||
isPermittedToBridge,
|
||||
onCloseFinalizeCCTPBridgeModal,
|
||||
onOpenFinalizeCCTPBridgeModal,
|
||||
publicClient,
|
||||
setModalFinalizeCCTPTxHash,
|
||||
setStatus,
|
||||
]);
|
||||
|
||||
const isOnCorrectNetwork =
|
||||
(bridgeDirection === 'deposit' && chain?.id === l2ChainID) ||
|
||||
(bridgeDirection === 'withdraw' && chain?.id === l1ChainID);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={isOnCorrectNetwork ? handleFinalizeCCTPBridge : handleSwitchToCorrectNetwork}
|
||||
className="w-32 bg-white py-2 font-sans text-sm text-black"
|
||||
>
|
||||
{isOnCorrectNetwork
|
||||
? 'Ready to complete'
|
||||
: `Switch to ${bridgeDirection === 'deposit' ? 'L2' : 'L1'}`}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { memo } from 'react';
|
||||
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
|
||||
import { useWaitForTransaction } from 'wagmi';
|
||||
import {
|
||||
blockExplorerUrlForL1Transaction,
|
||||
blockExplorerUrlForL2Transaction,
|
||||
} from 'apps/bridge/src/utils/url/blockExplorer';
|
||||
import { CCTPBridgeProgressBar } from 'apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar';
|
||||
import getConfig from 'next/config';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const l2ChainID = parseInt(publicRuntimeConfig.l2ChainID);
|
||||
|
||||
type FinalizeCCTPBridgeModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
finalizeTxHash?: `0x${string}`;
|
||||
};
|
||||
|
||||
type State = 'FINALIZE_NOT_SUBMITTED' | 'FINALIZE_SUBMITTED' | 'FINALIZE_RECEIVED';
|
||||
|
||||
const Titles = {
|
||||
FINALIZE_NOT_SUBMITTED: 'CONFIRM COMPLETION IN WALLET',
|
||||
FINALIZE_SUBMITTED: 'COMPLETING BRIDGE',
|
||||
FINALIZE_RECEIVED: 'COMPLETING BRIDGE',
|
||||
};
|
||||
|
||||
const Icons = {
|
||||
FINALIZE_NOT_SUBMITTED: 'confirm_deposit',
|
||||
FINALIZE_SUBMITTED: 'confirm_deposit',
|
||||
FINALIZE_RECEIVED: '',
|
||||
};
|
||||
|
||||
const ModalContents = {
|
||||
FINALIZE_NOT_SUBMITTED: 'Completion will begin after confirmation',
|
||||
FINALIZE_SUBMITTED: 'Waiting for confirmations...',
|
||||
FINALIZE_RECEIVED: <CCTPBridgeProgressBar status="VERIFIED" />,
|
||||
};
|
||||
|
||||
function getState(
|
||||
finalizeTxHash?: string,
|
||||
isFinalizationLoading?: boolean,
|
||||
isFinalizationSuccess?: boolean,
|
||||
): State {
|
||||
if (!finalizeTxHash) {
|
||||
return 'FINALIZE_NOT_SUBMITTED';
|
||||
}
|
||||
if (isFinalizationSuccess) {
|
||||
return 'FINALIZE_RECEIVED';
|
||||
}
|
||||
if (isFinalizationLoading) {
|
||||
return 'FINALIZE_SUBMITTED';
|
||||
}
|
||||
return 'FINALIZE_NOT_SUBMITTED';
|
||||
}
|
||||
|
||||
export const FinalizeCCTPBridgeModal = memo(function FinalizeCCTPBridgeModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
finalizeTxHash,
|
||||
}: FinalizeCCTPBridgeModalProps) {
|
||||
const { isLoading: isFinalizationLoading, isSuccess: isFinalizationSuccess } =
|
||||
useWaitForTransaction({
|
||||
hash: finalizeTxHash,
|
||||
});
|
||||
|
||||
// We don't have access to wheter this is a deposit or withdrawal here, so just
|
||||
// see if we can get a receipt for the transaction on L2. If we can, it's a deposit,
|
||||
// because deposits are finalized with an L2 tx.
|
||||
const { isSuccess: isDeposit, isLoading } = useWaitForTransaction({
|
||||
hash: finalizeTxHash,
|
||||
chainId: l2ChainID,
|
||||
});
|
||||
const bridgeDirection = isDeposit ? 'deposit' : 'withdraw';
|
||||
|
||||
const state = getState(finalizeTxHash, isFinalizationLoading, isFinalizationSuccess);
|
||||
|
||||
const explorerURL =
|
||||
bridgeDirection === 'withdraw'
|
||||
? blockExplorerUrlForL1Transaction(finalizeTxHash ?? '')
|
||||
: blockExplorerUrlForL2Transaction(finalizeTxHash ?? '');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={Titles[state]}
|
||||
content={ModalContents[state]}
|
||||
icon={Icons[state]}
|
||||
footer={
|
||||
finalizeTxHash &&
|
||||
!isLoading && (
|
||||
<div className="text-center">
|
||||
<a
|
||||
className="text-md font-sans text-cds-primary"
|
||||
href={explorerURL}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{`View on ${bridgeDirection === 'withdraw' ? 'Etherscan' : 'Basescan'}`}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1,176 +1,32 @@
|
||||
import { memo } from 'react';
|
||||
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
|
||||
import { depositPhaseStatusText, depositPhaseText } from 'apps/bridge/src/constants/phaseText';
|
||||
import { Dispatch, SetStateAction, memo } from 'react';
|
||||
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
|
||||
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
|
||||
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
|
||||
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
|
||||
import type { DepositPhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import {
|
||||
blockExplorerUrlForL1Transaction,
|
||||
blockExplorerUrlForL2Transaction,
|
||||
} from 'apps/bridge/src/utils/url/blockExplorer';
|
||||
import { formatUnits } from 'viem';
|
||||
import getConfig from 'next/config';
|
||||
import { useWaitForTransaction } from 'wagmi';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
import { OPDepositRow } from 'apps/bridge/src/components/Transactions/DepositRow/OPDepositRow';
|
||||
import { CCTPBridgeRow } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow';
|
||||
|
||||
type DepositRowProps = {
|
||||
transaction: BridgeTransaction;
|
||||
onOpenFinalizeCCTPBridgeModal: () => void;
|
||||
onCloseFinalizeCCTPBridgeModal: () => void;
|
||||
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
};
|
||||
|
||||
export const DepositRow = memo(function WithdrawalRow({ transaction }: DepositRowProps) {
|
||||
const date = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined;
|
||||
const dateMonthDayOnly = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined;
|
||||
const time = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleTimeString('en-US', {
|
||||
timeStyle: 'short',
|
||||
})
|
||||
: undefined;
|
||||
const depositAmount = formatUnits(
|
||||
BigInt(transaction.amount),
|
||||
// TODO: get decimals from asset list
|
||||
transaction.assetSymbol === 'USDC' ? 6 : 18,
|
||||
);
|
||||
|
||||
const conversionRateData = useConversionRate({
|
||||
asset: transaction.priceApiId,
|
||||
});
|
||||
|
||||
const amountFiat =
|
||||
conversionRateData && depositAmount
|
||||
? usdFormatter(conversionRateData * +depositAmount)
|
||||
: '$0.00';
|
||||
|
||||
const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } = useWaitForTransaction({
|
||||
chainId: parseInt(publicRuntimeConfig.l1ChainID),
|
||||
hash: transaction.hash,
|
||||
});
|
||||
|
||||
const explorerURL =
|
||||
transaction.type === 'Deposit'
|
||||
? blockExplorerUrlForL1Transaction(transaction.hash)
|
||||
: blockExplorerUrlForL2Transaction(transaction.hash);
|
||||
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
|
||||
|
||||
const pendingButton = (
|
||||
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="grey"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
Pending
|
||||
</button>
|
||||
);
|
||||
|
||||
const PHASE_TO_STATUS = {
|
||||
DEPOSIT_TX_PENDING: pendingButton,
|
||||
DEPOSIT_TX_FAILURE: depositPhaseStatusText.DEPOSIT_TX_FAILURE,
|
||||
FUNDS_DEPOSITED: depositPhaseStatusText.FUNDS_DEPOSITED,
|
||||
};
|
||||
|
||||
let depositStatus: DepositPhase;
|
||||
if (isDepositLoading) {
|
||||
depositStatus = 'DEPOSIT_TX_PENDING';
|
||||
} else if (isDepositSuccess) {
|
||||
depositStatus = 'FUNDS_DEPOSITED';
|
||||
} else {
|
||||
depositStatus = 'DEPOSIT_TX_FAILURE';
|
||||
export const DepositRow = memo(function DepositRow({
|
||||
transaction,
|
||||
onOpenFinalizeCCTPBridgeModal,
|
||||
onCloseFinalizeCCTPBridgeModal,
|
||||
setModalFinalizeCCTPTxHash,
|
||||
}: DepositRowProps) {
|
||||
if (transaction.protocol === 'CCTP') {
|
||||
return (
|
||||
<CCTPBridgeRow
|
||||
transaction={transaction}
|
||||
bridgeDirection="deposit"
|
||||
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
|
||||
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
|
||||
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="mb-4 grid grid-cols-2 grid-rows-2 md:table-row">
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
|
||||
<div className="flex flex-col">
|
||||
<p>{date ?? ''}</p>
|
||||
<p>{time ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - left column */}
|
||||
<td className="md:hidden">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="whitespace-nowrap font-sans text-sm"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
<div className="flex flex-row text-cds-background-gray-60">
|
||||
{transaction.type}
|
||||
<h3>•</h3>
|
||||
<p>{dateMonthDayOnly ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
{transaction.type}
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="text-cds-background-gray-60 underline"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - right column */}
|
||||
<td className="md:table-cell md:hidden">
|
||||
<div className="flex flex-col items-end">
|
||||
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-6 grow flex-row items-center gap-2">
|
||||
<div className="border-gray-400 w-36 border-t-4" />
|
||||
</div>
|
||||
<div className="text-cds-background-gray-60">{depositPhaseText[depositStatus]}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden text-end text-cds-background-gray-60 md:table-cell md:text-start">
|
||||
{PHASE_TO_STATUS[depositStatus]}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
return <OPDepositRow transaction={transaction} />;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { memo } from 'react';
|
||||
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
|
||||
import { depositPhaseStatusText, depositPhaseText } from 'apps/bridge/src/constants/phaseText';
|
||||
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
|
||||
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
|
||||
import type { DepositPhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import {
|
||||
blockExplorerUrlForL1Transaction,
|
||||
blockExplorerUrlForL2Transaction,
|
||||
} from 'apps/bridge/src/utils/url/blockExplorer';
|
||||
import getConfig from 'next/config';
|
||||
import { formatUnits } from 'viem';
|
||||
import { useWaitForTransaction } from 'wagmi';
|
||||
import { formatTimestamp } from 'apps/bridge/src/utils/transactions/formatBlockTimestamp';
|
||||
import { useGetUSDAmount } from 'apps/bridge/src/utils/hooks/useGetUSDAmount';
|
||||
import { PendingButton } from 'apps/bridge/src/components/Transactions/PendingButton';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const PHASE_TO_STATUS: Record<Exclude<DepositPhase, 'DEPOSIT_TX_PENDING'>, string> = {
|
||||
DEPOSIT_TX_FAILURE: depositPhaseStatusText.DEPOSIT_TX_FAILURE,
|
||||
FUNDS_DEPOSITED: depositPhaseStatusText.FUNDS_DEPOSITED,
|
||||
};
|
||||
|
||||
type OPDepositRowProps = {
|
||||
transaction: BridgeTransaction;
|
||||
};
|
||||
|
||||
// Transactions table row component for deposits made using the OP bridge.
|
||||
export const OPDepositRow = memo(function OPDepositRow({ transaction }: OPDepositRowProps) {
|
||||
const { date, shortDate: dateMonthDayOnly, time } = formatTimestamp(transaction.blockTimestamp);
|
||||
const depositAmount = formatUnits(BigInt(transaction.amount), transaction.assetDecimals ?? 18);
|
||||
const amountFiat = useGetUSDAmount(transaction.priceApiId, depositAmount);
|
||||
|
||||
const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } = useWaitForTransaction({
|
||||
chainId: parseInt(publicRuntimeConfig.l1ChainID),
|
||||
hash: transaction.hash,
|
||||
});
|
||||
|
||||
const explorerURL =
|
||||
transaction.type === 'Deposit'
|
||||
? blockExplorerUrlForL1Transaction(transaction.hash)
|
||||
: blockExplorerUrlForL2Transaction(transaction.hash);
|
||||
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
|
||||
|
||||
let depositStatus: DepositPhase;
|
||||
if (isDepositLoading) {
|
||||
depositStatus = 'DEPOSIT_TX_PENDING';
|
||||
} else if (isDepositSuccess) {
|
||||
depositStatus = 'FUNDS_DEPOSITED';
|
||||
} else {
|
||||
depositStatus = 'DEPOSIT_TX_FAILURE';
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="mb-4 grid grid-cols-2 grid-rows-2 md:table-row">
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
|
||||
<div className="flex flex-col">
|
||||
<p>{date ?? ''}</p>
|
||||
<p>{time ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - left column */}
|
||||
<td className="md:hidden">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="deposit" phase={depositStatus} />
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="whitespace-nowrap font-sans text-sm"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
<div className="flex flex-row text-cds-background-gray-60">
|
||||
{transaction.type}
|
||||
<h3>•</h3>
|
||||
<p>{dateMonthDayOnly ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
{transaction.type}
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="text-cds-background-gray-60 underline"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - right column */}
|
||||
<td className="md:table-cell md:hidden">
|
||||
<div className="flex flex-col items-end">
|
||||
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div>{`${depositAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-6 grow flex-row items-center gap-2">
|
||||
<div className="border-gray-400 w-36 border-t-4" />
|
||||
</div>
|
||||
<div className="text-cds-background-gray-60">{depositPhaseText[depositStatus]}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden text-end text-cds-background-gray-60 md:table-cell md:text-start">
|
||||
{depositStatus === 'DEPOSIT_TX_PENDING' ? (
|
||||
<PendingButton />
|
||||
) : (
|
||||
PHASE_TO_STATUS[depositStatus]
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
25
apps/bridge/src/components/Transactions/PendingButton.tsx
Normal file
25
apps/bridge/src/components/Transactions/PendingButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
export const PendingButton = memo(function PendingButton() {
|
||||
return (
|
||||
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="grey"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
Pending
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Dispatch, memo, SetStateAction, useState } from 'react';
|
||||
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
|
||||
import {
|
||||
withdrawalPhaseStatusText,
|
||||
withdrawalPhaseText,
|
||||
} from 'apps/bridge/src/constants/phaseText';
|
||||
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
|
||||
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
|
||||
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
|
||||
import { useWithdrawalStatus } from 'apps/bridge/src/utils/hooks/useWithdrawalStatus';
|
||||
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
|
||||
import {
|
||||
blockExplorerUrlForL1Transaction,
|
||||
blockExplorerUrlForL2Transaction,
|
||||
} from 'apps/bridge/src/utils/url/blockExplorer';
|
||||
import { formatUnits } from 'viem';
|
||||
|
||||
import { FinalizeWithdrawalButton } from './FinalizeWithdrawalButton';
|
||||
import { ProveWithdrawalButton } from './ProveWithdrawalButton';
|
||||
import { BridgePhaseIndicator } from 'apps/bridge/src/components/Transactions/BridgePhaseIndicator';
|
||||
|
||||
type OPWithdrawalRowProps = {
|
||||
transaction: BridgeTransaction;
|
||||
blockNumberOfLatestL2OutputProposal?: bigint;
|
||||
onOpenProveWithdrawalModal: () => void;
|
||||
onCloseProveWithdrawalModal: () => void;
|
||||
onOpenFinalizeWithdrawalModal: () => void;
|
||||
onCloseFinalizeWithdrawalModal: () => void;
|
||||
setModalProveTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
setModalFinalizeTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
};
|
||||
|
||||
export const OPWithdrawalRow = memo(function OPWithdrawalRow({
|
||||
transaction,
|
||||
blockNumberOfLatestL2OutputProposal,
|
||||
onOpenProveWithdrawalModal,
|
||||
onCloseProveWithdrawalModal,
|
||||
onOpenFinalizeWithdrawalModal,
|
||||
onCloseFinalizeWithdrawalModal,
|
||||
setModalProveTxHash,
|
||||
setModalFinalizeTxHash,
|
||||
}: OPWithdrawalRowProps) {
|
||||
const isERC20Withdrawal = transaction.assetSymbol !== 'ETH';
|
||||
const [proveTxHash, setProveTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [finalizeTxHash, setFinalizeTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const { status: withdrawalStatus, challengeWindowEndTime } = useWithdrawalStatus({
|
||||
initializeTxHash: transaction.hash,
|
||||
blockNumberOfLatestL2OutputProposal,
|
||||
isERC20Withdrawal,
|
||||
proveTxHash,
|
||||
finalizeTxHash,
|
||||
});
|
||||
|
||||
const date = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined;
|
||||
const dateMonthDayOnly = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined;
|
||||
const time = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleTimeString('en-US', {
|
||||
timeStyle: 'short',
|
||||
})
|
||||
: undefined;
|
||||
const withdrawalAmount = formatUnits(
|
||||
BigInt(transaction.amount),
|
||||
// TODO: get decimals from asset list
|
||||
transaction.assetSymbol === 'USDbC' ? 6 : 18,
|
||||
);
|
||||
|
||||
const conversionRateData = useConversionRate({
|
||||
asset: transaction.priceApiId,
|
||||
});
|
||||
const amountFiat =
|
||||
conversionRateData && withdrawalAmount
|
||||
? usdFormatter(conversionRateData * +withdrawalAmount)
|
||||
: '$0.00';
|
||||
|
||||
const explorerURL =
|
||||
transaction.type === 'Deposit'
|
||||
? blockExplorerUrlForL1Transaction(transaction.hash)
|
||||
: blockExplorerUrlForL2Transaction(transaction.hash);
|
||||
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
|
||||
|
||||
const pendingButton = (
|
||||
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="grey"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
Pending
|
||||
</button>
|
||||
);
|
||||
const PHASE_TO_STATUS = {
|
||||
PROPOSING_ON_CHAIN: withdrawalPhaseStatusText.PROPOSING_ON_CHAIN,
|
||||
PROVE: (
|
||||
<ProveWithdrawalButton
|
||||
txHash={transaction.hash}
|
||||
isERC20Withdrawal={isERC20Withdrawal}
|
||||
onOpenProveWithdrawalModal={onOpenProveWithdrawalModal}
|
||||
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
|
||||
setProveTxHash={setProveTxHash}
|
||||
setModalProveTxHash={setModalProveTxHash}
|
||||
blockNumberOfLatestL2OutputProposal={blockNumberOfLatestL2OutputProposal}
|
||||
/>
|
||||
),
|
||||
PROVE_TX_PENDING: pendingButton,
|
||||
PROVE_TX_FAILURE: withdrawalPhaseStatusText.PROVE_TX_FAILURE,
|
||||
CHALLENGE_WINDOW: withdrawalPhaseStatusText.CHALLENGE_WINDOW(Number(challengeWindowEndTime)),
|
||||
FINALIZE: (
|
||||
<FinalizeWithdrawalButton
|
||||
txHash={transaction.hash}
|
||||
isERC20Withdrawal={isERC20Withdrawal}
|
||||
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
|
||||
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
|
||||
setFinalizeTxHash={setFinalizeTxHash}
|
||||
setModalFinalizeTxHash={setModalFinalizeTxHash}
|
||||
/>
|
||||
),
|
||||
FINALIZE_TX_PENDING: pendingButton,
|
||||
FINALIZE_TX_FAILURE: withdrawalPhaseStatusText.FINALIZE_TX_FAILURE,
|
||||
FUNDS_WITHDRAWN: withdrawalPhaseStatusText.FUNDS_WITHDRAWN,
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className="mb-4 grid grid-cols-3 grid-rows-3 md:table-row">
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
|
||||
<div className="flex flex-col">
|
||||
<p>{date ?? ''}</p>
|
||||
<p>{time ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - left column */}
|
||||
<td className="md:hidden">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="whitespace-nowrap font-sans text-sm"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
<div className="md:text-md flex flex-row text-xs text-cds-background-gray-60">
|
||||
{transaction.type}
|
||||
<h3>•</h3>
|
||||
<p>{dateMonthDayOnly ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
{transaction.type}
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="text-cds-background-gray-60 underline"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - right column */}
|
||||
<td className="table-cell md:hidden">
|
||||
<div className="flex flex-col items-end">
|
||||
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<BridgePhaseIndicator phase={withdrawalStatus} />
|
||||
<div className="text-cds-background-gray-60">{withdrawalPhaseText[withdrawalStatus]}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="table-cell text-end text-cds-background-gray-60 md:text-start">
|
||||
{PHASE_TO_STATUS[withdrawalStatus]}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
@@ -1,23 +1,7 @@
|
||||
import { Dispatch, memo, SetStateAction, useState } from 'react';
|
||||
import { TransactionIcon } from 'apps/bridge/src/components/TransactionIcon/TransactionIcon';
|
||||
import {
|
||||
withdrawalPhaseStatusText,
|
||||
withdrawalPhaseText,
|
||||
} from 'apps/bridge/src/constants/phaseText';
|
||||
import { Dispatch, SetStateAction, memo } from 'react';
|
||||
import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction';
|
||||
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
|
||||
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
|
||||
import { useWithdrawalStatus } from 'apps/bridge/src/utils/hooks/useWithdrawalStatus';
|
||||
import { truncateMiddle } from 'apps/bridge/src/utils/string/truncateMiddle';
|
||||
import type { WithdrawalPhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import {
|
||||
blockExplorerUrlForL1Transaction,
|
||||
blockExplorerUrlForL2Transaction,
|
||||
} from 'apps/bridge/src/utils/url/blockExplorer';
|
||||
import { formatUnits } from 'viem';
|
||||
|
||||
import { FinalizeWithdrawalButton } from './FinalizeWithdrawalButton';
|
||||
import { ProveWithdrawalButton } from './ProveWithdrawalButton';
|
||||
import { CCTPBridgeRow } from 'apps/bridge/src/components/Transactions/CCTPBridgeRow/CCTPBridgeRow';
|
||||
import { OPWithdrawalRow } from 'apps/bridge/src/components/Transactions/WithdrawalRow/OPWithdrawalRow';
|
||||
|
||||
type WithdrawalRowProps = {
|
||||
transaction: BridgeTransaction;
|
||||
@@ -26,8 +10,11 @@ type WithdrawalRowProps = {
|
||||
onCloseProveWithdrawalModal: () => void;
|
||||
onOpenFinalizeWithdrawalModal: () => void;
|
||||
onCloseFinalizeWithdrawalModal: () => void;
|
||||
onOpenFinalizeCCTPBridgeModal: () => void;
|
||||
onCloseFinalizeCCTPBridgeModal: () => void;
|
||||
setModalProveTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
setModalFinalizeTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
setModalFinalizeOPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
setModalFinalizeCCTPTxHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
};
|
||||
|
||||
export const WithdrawalRow = memo(function WithdrawalRow({
|
||||
@@ -37,206 +24,34 @@ export const WithdrawalRow = memo(function WithdrawalRow({
|
||||
onCloseProveWithdrawalModal,
|
||||
onOpenFinalizeWithdrawalModal,
|
||||
onCloseFinalizeWithdrawalModal,
|
||||
onOpenFinalizeCCTPBridgeModal,
|
||||
onCloseFinalizeCCTPBridgeModal,
|
||||
setModalProveTxHash,
|
||||
setModalFinalizeTxHash,
|
||||
setModalFinalizeOPTxHash,
|
||||
setModalFinalizeCCTPTxHash,
|
||||
}: WithdrawalRowProps) {
|
||||
const isERC20Withdrawal = transaction.assetSymbol !== 'ETH';
|
||||
const [proveTxHash, setProveTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [finalizeTxHash, setFinalizeTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const { status: withdrawalStatus, challengeWindowEndTime } = useWithdrawalStatus({
|
||||
initializeTxHash: transaction.hash,
|
||||
blockNumberOfLatestL2OutputProposal,
|
||||
isERC20Withdrawal,
|
||||
proveTxHash,
|
||||
finalizeTxHash,
|
||||
});
|
||||
|
||||
const date = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined;
|
||||
const dateMonthDayOnly = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined;
|
||||
const time = transaction.blockTimestamp
|
||||
? new Date(Number(transaction.blockTimestamp) * 1000).toLocaleTimeString('en-US', {
|
||||
timeStyle: 'short',
|
||||
})
|
||||
: undefined;
|
||||
const withdrawalAmount = formatUnits(
|
||||
BigInt(transaction.amount),
|
||||
// TODO: get decimals from asset list
|
||||
transaction.assetSymbol === 'USDbC' ? 6 : 18,
|
||||
);
|
||||
|
||||
const conversionRateData = useConversionRate({
|
||||
asset: transaction.priceApiId,
|
||||
});
|
||||
const amountFiat =
|
||||
conversionRateData && withdrawalAmount
|
||||
? usdFormatter(conversionRateData * +withdrawalAmount)
|
||||
: '$0.00';
|
||||
|
||||
const explorerURL =
|
||||
transaction.type === 'Deposit'
|
||||
? blockExplorerUrlForL1Transaction(transaction.hash)
|
||||
: blockExplorerUrlForL2Transaction(transaction.hash);
|
||||
const abridgedHash = truncateMiddle(transaction.hash, 6, 4);
|
||||
|
||||
const generatePhaseIndicator = (phase: WithdrawalPhase): JSX.Element[] => {
|
||||
const PHASE_MAP = {
|
||||
PROPOSING_ON_CHAIN: 1,
|
||||
PROVE: 2,
|
||||
PROVE_TX_PENDING: 2,
|
||||
PROVE_TX_FAILURE: 2,
|
||||
CHALLENGE_WINDOW: 2,
|
||||
FINALIZE: 3,
|
||||
FINALIZE_TX_PENDING: 3,
|
||||
FINALIZE_TX_FAILURE: 3,
|
||||
FUNDS_WITHDRAWN: 4,
|
||||
};
|
||||
const rv: JSX.Element[] = [];
|
||||
for (let i = 0; i < PHASE_MAP[phase]; i += 1) {
|
||||
rv.push(<div className="border-gray-400 w-8 border-t-4" key={`fill-${i}`} />);
|
||||
}
|
||||
for (let i = 0; i < 4 - PHASE_MAP[phase]; i += 1) {
|
||||
rv.push(
|
||||
<div
|
||||
className="border-gray-400 w-8 border-t-4 text-cds-background-gray-60"
|
||||
key={`back-${i}`}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return rv;
|
||||
};
|
||||
|
||||
const pendingButton = (
|
||||
<button type="button" className="w-32 rounded bg-white py-2 font-sans text-sm text-black">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="text-gray-200 dark:text-gray-600 mr-2 inline h-4 w-4 animate-spin fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="grey"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
Pending
|
||||
</button>
|
||||
);
|
||||
const PHASE_TO_STATUS = {
|
||||
PROPOSING_ON_CHAIN: withdrawalPhaseStatusText.PROPOSING_ON_CHAIN,
|
||||
PROVE: (
|
||||
<ProveWithdrawalButton
|
||||
txHash={transaction.hash}
|
||||
isERC20Withdrawal={isERC20Withdrawal}
|
||||
onOpenProveWithdrawalModal={onOpenProveWithdrawalModal}
|
||||
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
|
||||
setProveTxHash={setProveTxHash}
|
||||
setModalProveTxHash={setModalProveTxHash}
|
||||
blockNumberOfLatestL2OutputProposal={blockNumberOfLatestL2OutputProposal}
|
||||
if (transaction.protocol === 'CCTP') {
|
||||
return (
|
||||
<CCTPBridgeRow
|
||||
transaction={transaction}
|
||||
bridgeDirection="withdraw"
|
||||
onOpenFinalizeCCTPBridgeModal={onOpenFinalizeCCTPBridgeModal}
|
||||
onCloseFinalizeCCTPBridgeModal={onCloseFinalizeCCTPBridgeModal}
|
||||
setModalFinalizeCCTPTxHash={setModalFinalizeCCTPTxHash}
|
||||
/>
|
||||
),
|
||||
PROVE_TX_PENDING: pendingButton,
|
||||
PROVE_TX_FAILURE: withdrawalPhaseStatusText.PROVE_TX_FAILURE,
|
||||
CHALLENGE_WINDOW: withdrawalPhaseStatusText.CHALLENGE_WINDOW(Number(challengeWindowEndTime)),
|
||||
FINALIZE: (
|
||||
<FinalizeWithdrawalButton
|
||||
txHash={transaction.hash}
|
||||
isERC20Withdrawal={isERC20Withdrawal}
|
||||
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
|
||||
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
|
||||
setFinalizeTxHash={setFinalizeTxHash}
|
||||
setModalFinalizeTxHash={setModalFinalizeTxHash}
|
||||
/>
|
||||
),
|
||||
FINALIZE_TX_PENDING: pendingButton,
|
||||
FINALIZE_TX_FAILURE: withdrawalPhaseStatusText.FINALIZE_TX_FAILURE,
|
||||
FUNDS_WITHDRAWN: withdrawalPhaseStatusText.FUNDS_WITHDRAWN,
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="mb-4 grid grid-cols-3 grid-rows-3 md:table-row">
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
|
||||
<div className="flex flex-col">
|
||||
<p>{date ?? ''}</p>
|
||||
<p>{time ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - left column */}
|
||||
<td className="md:hidden">
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<TransactionIcon bridgeDirection="withdraw" phase={withdrawalStatus} />
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="whitespace-nowrap font-sans text-sm"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
<div className="md:text-md flex flex-row text-xs text-cds-background-gray-60">
|
||||
{transaction.type}
|
||||
<h3>•</h3>
|
||||
<p>{dateMonthDayOnly ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
{transaction.type}
|
||||
<a
|
||||
target="_blank"
|
||||
href={explorerURL}
|
||||
rel="noreferrer noopener"
|
||||
className="text-cds-background-gray-60 underline"
|
||||
>
|
||||
{abridgedHash}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
{/* mobile design - right column */}
|
||||
<td className="table-cell md:hidden">
|
||||
<div className="flex flex-col items-end">
|
||||
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div>{`${withdrawalAmount} ${transaction.assetSymbol}`}</div>
|
||||
<div className="text-cds-background-gray-60">{`${amountFiat} USD`}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-6 grow flex-row items-center gap-1">
|
||||
{generatePhaseIndicator(withdrawalStatus)}
|
||||
</div>
|
||||
<div className="text-cds-background-gray-60">{withdrawalPhaseText[withdrawalStatus]}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="table-cell text-end text-cds-background-gray-60 md:text-start">
|
||||
{PHASE_TO_STATUS[withdrawalStatus]}
|
||||
</td>
|
||||
</tr>
|
||||
<OPWithdrawalRow
|
||||
transaction={transaction}
|
||||
blockNumberOfLatestL2OutputProposal={blockNumberOfLatestL2OutputProposal}
|
||||
onOpenProveWithdrawalModal={onOpenProveWithdrawalModal}
|
||||
onCloseProveWithdrawalModal={onCloseProveWithdrawalModal}
|
||||
onOpenFinalizeWithdrawalModal={onOpenFinalizeWithdrawalModal}
|
||||
onCloseFinalizeWithdrawalModal={onCloseFinalizeWithdrawalModal}
|
||||
setModalProveTxHash={setModalProveTxHash}
|
||||
setModalFinalizeTxHash={setModalFinalizeOPTxHash}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BridgeInput } from 'apps/bridge/src/components/BridgeInput/BridgeInput';
|
||||
import { BridgeToInput } from 'apps/bridge/src/components/BridgeToInput/BridgeToInput';
|
||||
import { ConnectWalletButton } from 'apps/bridge/src/components/ConnectWalletButton/ConnectWalletButton';
|
||||
@@ -7,7 +7,6 @@ import { BaseButton } from 'apps/bridge/src/components/SwitchNetworkButton/Switc
|
||||
import { TransactionSummary } from 'apps/bridge/src/components/TransactionSummary/TransactionSummary';
|
||||
import { WithdrawModal } from 'apps/bridge/src/components/WithdrawModal/WithdrawModal';
|
||||
import { Asset } from 'apps/bridge/src/types/Asset';
|
||||
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
|
||||
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
|
||||
import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
|
||||
import { useGetCode } from 'apps/bridge/src/utils/hooks/useGetCode';
|
||||
@@ -18,30 +17,69 @@ import { usePrepareERC20WithdrawalTo } from 'apps/bridge/src/utils/hooks/usePrep
|
||||
import { usePrepareETHWithdrawal } from 'apps/bridge/src/utils/hooks/usePrepareETHWithdrawal';
|
||||
import { isAddress } from 'viem';
|
||||
import getConfig from 'next/config';
|
||||
import { useAccount, useBalance, useContractWrite } from 'wagmi';
|
||||
import { useAccount, useBalance, useContractWrite, usePublicClient, useSwitchNetwork } from 'wagmi';
|
||||
import { writeContract } from 'wagmi/actions';
|
||||
import { useIsPermittedToBridgeTo } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridgeTo';
|
||||
import { getL2NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL2NetworkForChainEnv';
|
||||
import { getL1NetworkForChainEnv } from 'apps/bridge/src/utils/networks/getL1NetworkForChainEnv';
|
||||
import { getWithdrawalAssetsForChainEnv } from 'apps/bridge/src/utils/assets/getWithdrawalAssetsForChainEnv';
|
||||
import { usePrepareInitiateCCTPBridge } from 'apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge';
|
||||
import { useIsContractApproved } from 'apps/bridge/src/utils/hooks/useIsContractApproved';
|
||||
import { useApproveContract } from 'apps/bridge/src/utils/hooks/useApproveContract';
|
||||
import { BridgeButton } from 'apps/bridge/src/components/BridgeButton/BridgeButton';
|
||||
import { parseUnits } from 'viem';
|
||||
|
||||
const assetList = getAssetListForChainEnv();
|
||||
const activeAssets = getWithdrawalAssetsForChainEnv();
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const chainId = parseInt(publicRuntimeConfig.l2ChainID);
|
||||
|
||||
export function WithdrawContainer() {
|
||||
const [withdrawAmount, setWithdrawAmount] = useState('');
|
||||
const [L2TxHash, setL2TxHash] = useState('');
|
||||
const [L2ApproveTxHash, setL2ApproveTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [L2WithdrawTxHash, setL2WithdrawTxHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [withdrawTo, setWithdrawTo] = useState('');
|
||||
const [isApprovalTx, setIsApprovalTx] = useState(false);
|
||||
const isWalletConnected = useIsWalletConnected();
|
||||
const activeAssets = assetList.filter((asset) =>
|
||||
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
|
||||
);
|
||||
const [selectedAsset, setSelectedAsset] = useState<Asset>(assetList[0]);
|
||||
const [selectedAsset, setSelectedAsset] = useState<Asset>(activeAssets[0]);
|
||||
const publicClient = usePublicClient({ chainId });
|
||||
const { switchNetwork } = useSwitchNetwork();
|
||||
|
||||
useEffect(() => {
|
||||
switchNetwork?.(chainId);
|
||||
}, [switchNetwork]);
|
||||
|
||||
const { address } = useAccount();
|
||||
const codeAtAddress = useGetCode(chainId, address);
|
||||
const isSmartContractWallet = !!codeAtAddress && codeAtAddress !== '0x';
|
||||
|
||||
const erc20Spender = publicRuntimeConfig.l2CCTPTokenMessengerAddress;
|
||||
|
||||
const { data: readERC20Approval, error: readERC20ApprovalError } = useIsContractApproved({
|
||||
contactAddress: selectedAsset.L2contract,
|
||||
address,
|
||||
spender: erc20Spender,
|
||||
bridgeDirection: 'withdraw',
|
||||
});
|
||||
|
||||
const readApprovalResult = useMemo(() => {
|
||||
const withdrawAmountBN =
|
||||
withdrawAmount === '' || Number.isNaN(Number(withdrawAmount))
|
||||
? parseUnits('0', selectedAsset.decimals)
|
||||
: parseUnits(withdrawAmount, selectedAsset.decimals);
|
||||
return !readERC20ApprovalError && readERC20Approval && readERC20Approval >= withdrawAmountBN;
|
||||
}, [withdrawAmount, selectedAsset.decimals, readERC20ApprovalError, readERC20Approval]);
|
||||
|
||||
// approve erc20
|
||||
const approveConfig = useApproveContract({
|
||||
contractAddress: selectedAsset.L2contract,
|
||||
spender: erc20Spender,
|
||||
approveAmount: withdrawAmount,
|
||||
decimals: selectedAsset.decimals,
|
||||
bridgeDirection: 'withdraw',
|
||||
});
|
||||
const { writeAsync: approveWrite } = useContractWrite(approveConfig);
|
||||
|
||||
const { data: L2Balance } = useBalance({
|
||||
address,
|
||||
token: selectedAsset.L2contract,
|
||||
@@ -50,7 +88,7 @@ export function WithdrawContainer() {
|
||||
|
||||
const chainEnv = useChainEnv();
|
||||
const isMainnet = chainEnv === 'mainnet';
|
||||
const includeTosVersionByte = isMainnet && withdrawTo === '';
|
||||
const includeTosVersionByte = isMainnet;
|
||||
const isUserPermittedToBridge = useIsPermittedToBridge();
|
||||
const isPermittedToBridgeTo = useIsPermittedToBridgeTo(withdrawTo as `0x${string}`);
|
||||
const isPermittedToBridge = isSmartContractWallet
|
||||
@@ -82,6 +120,18 @@ export function WithdrawContainer() {
|
||||
});
|
||||
const { writeAsync: withdraw } = useContractWrite(withdrawConfig);
|
||||
|
||||
// withdraw using CCTP (eg USDC)
|
||||
const withdrawCCTPAssetConfig = usePrepareInitiateCCTPBridge({
|
||||
mintRecipient: isSmartContractWallet ? (withdrawTo as `0x${string}`) : address,
|
||||
asset: selectedAsset,
|
||||
amount: withdrawAmount,
|
||||
destinationDomain: parseInt(publicRuntimeConfig.l1CCTPDomain),
|
||||
isPermittedToBridge,
|
||||
includeTosVersionByte,
|
||||
bridgeDirection: 'withdraw',
|
||||
});
|
||||
const { writeAsync: withdrawCCTPAssetWrite } = useContractWrite(withdrawCCTPAssetConfig);
|
||||
|
||||
const {
|
||||
isOpen: isWithdrawModalOpen,
|
||||
onOpen: onOpenWithdrawModal,
|
||||
@@ -90,9 +140,58 @@ export function WithdrawContainer() {
|
||||
|
||||
const handleCloseWithdrawModal = useCallback(() => {
|
||||
onCloseWithdrawModal();
|
||||
setL2TxHash('');
|
||||
setL2WithdrawTxHash(undefined);
|
||||
setL2ApproveTxHash(undefined);
|
||||
setIsApprovalTx(false);
|
||||
}, [onCloseWithdrawModal]);
|
||||
|
||||
const initiateApproval = useCallback(() => {
|
||||
void (async () => {
|
||||
setIsApprovalTx(true);
|
||||
onOpenWithdrawModal();
|
||||
try {
|
||||
const approveResult = await approveWrite?.();
|
||||
if (approveResult?.hash) {
|
||||
const approveTxHash: `0x${string}` = approveResult.hash;
|
||||
setL2ApproveTxHash(approveTxHash);
|
||||
|
||||
// wait for confirmations
|
||||
await publicClient.waitForTransactionReceipt({ hash: approveResult.hash });
|
||||
|
||||
// next, call the transfer function
|
||||
setIsApprovalTx(false);
|
||||
|
||||
let withdrawMethod;
|
||||
if (selectedAsset.protocol === 'CCTP') {
|
||||
// because of how React works we need to use the writeContract wagmi/core action
|
||||
// here (the hook still thinks the approval has not been set)
|
||||
withdrawMethod = async () => await writeContract(withdrawCCTPAssetConfig);
|
||||
} else {
|
||||
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
|
||||
}
|
||||
const withdrawResult = await withdrawMethod?.();
|
||||
if (withdrawResult?.hash) {
|
||||
const withdrawTxHash = withdrawResult.hash;
|
||||
setL2WithdrawTxHash(withdrawTxHash);
|
||||
setWithdrawAmount('');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
onCloseWithdrawModal();
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
approveWrite,
|
||||
isSmartContractWallet,
|
||||
onCloseWithdrawModal,
|
||||
onOpenWithdrawModal,
|
||||
publicClient,
|
||||
selectedAsset.protocol,
|
||||
withdrawCCTPAssetConfig,
|
||||
withdrawERC20,
|
||||
withdrawERC20To,
|
||||
]);
|
||||
|
||||
const initiateWithdrawal = useCallback(() => {
|
||||
void (async () => {
|
||||
onOpenWithdrawModal();
|
||||
@@ -101,15 +200,19 @@ export function WithdrawContainer() {
|
||||
if (isPermittedToBridge) {
|
||||
let withdrawMethod;
|
||||
if (selectedAsset.L1contract) {
|
||||
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
|
||||
if (selectedAsset.protocol === 'CCTP') {
|
||||
withdrawMethod = withdrawCCTPAssetWrite;
|
||||
} else {
|
||||
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
|
||||
}
|
||||
} else {
|
||||
withdrawMethod = withdraw;
|
||||
}
|
||||
const withdrawalResult = await withdrawMethod?.();
|
||||
if (withdrawalResult?.hash) {
|
||||
const withdrawalTxHsh = withdrawalResult.hash;
|
||||
setL2TxHash(withdrawalTxHsh);
|
||||
setWithdrawAmount('0');
|
||||
setL2WithdrawTxHash(withdrawalTxHsh);
|
||||
setWithdrawAmount('');
|
||||
}
|
||||
} else {
|
||||
onCloseWithdrawModal();
|
||||
@@ -122,6 +225,8 @@ export function WithdrawContainer() {
|
||||
onOpenWithdrawModal,
|
||||
isPermittedToBridge,
|
||||
selectedAsset.L1contract,
|
||||
selectedAsset.protocol,
|
||||
withdrawCCTPAssetWrite,
|
||||
isSmartContractWallet,
|
||||
withdrawERC20To,
|
||||
withdrawERC20,
|
||||
@@ -136,6 +241,19 @@ export function WithdrawContainer() {
|
||||
button = (
|
||||
<ConnectWalletButton className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto" />
|
||||
);
|
||||
} else if (selectedAsset.protocol === 'CCTP' && !readApprovalResult) {
|
||||
withdrawDisabled =
|
||||
(isSmartContractWallet && !isAddress(withdrawTo ?? '')) || !isPermittedToBridge;
|
||||
|
||||
button = (
|
||||
<BridgeButton
|
||||
onClick={initiateApproval}
|
||||
disabled={withdrawDisabled}
|
||||
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
|
||||
>
|
||||
Approval
|
||||
</BridgeButton>
|
||||
);
|
||||
} else {
|
||||
withdrawDisabled =
|
||||
parseFloat(withdrawAmount) <= 0 ||
|
||||
@@ -162,7 +280,10 @@ export function WithdrawContainer() {
|
||||
<WithdrawModal
|
||||
isOpen={isWithdrawModalOpen}
|
||||
onClose={handleCloseWithdrawModal}
|
||||
L2TxHash={L2TxHash}
|
||||
L2ApproveTxHash={L2ApproveTxHash}
|
||||
L2WithdrawTxHash={L2WithdrawTxHash}
|
||||
isApprovalTx={isApprovalTx}
|
||||
protocol={selectedAsset.protocol}
|
||||
/>
|
||||
<BridgeInput
|
||||
inputNetwork={getL2NetworkForChainEnv()}
|
||||
|
||||
@@ -1,32 +1,112 @@
|
||||
import { CCTPBridgeProgressBar } from 'apps/bridge/src/components/CCTPBridgeProgressBar/CCTPBridgeProgressBar';
|
||||
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
|
||||
import { WithdrawProgressBar } from 'apps/bridge/src/components/WithdrawProgressBar/WithdrawProgressBar';
|
||||
import { BridgeProtocol } from 'apps/bridge/src/types/Asset';
|
||||
import getConfig from 'next/config';
|
||||
import { ReactNode } from 'react';
|
||||
import { useWaitForTransaction } from 'wagmi';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
type WithdrawModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
L2TxHash: string;
|
||||
L2ApproveTxHash: `0x${string}` | undefined;
|
||||
L2WithdrawTxHash: `0x${string}` | undefined;
|
||||
isApprovalTx: boolean;
|
||||
protocol: BridgeProtocol;
|
||||
};
|
||||
|
||||
const Titles = {
|
||||
WITHDRAW_NOT_STARTED: 'CONFIRM WITHDRAWAL IN WALLET',
|
||||
WITHDRAW_STARTED: 'WITHDRAWAL IN PROGRESS',
|
||||
type STATE =
|
||||
| 'APPROVAL_LOADING'
|
||||
| 'APPROVAL_CONFIRMED'
|
||||
| 'APPROVAL_NOT_STARTED'
|
||||
| 'WITHDRAWAL_LOADING'
|
||||
| 'OP_WITHDRAWAL_STARTED'
|
||||
| 'CCTP_WITHDRAWAL_STARTED'
|
||||
| 'WITHDRAWAL_NOT_STARTED';
|
||||
|
||||
function getState(
|
||||
isApprovalTx: boolean,
|
||||
isApproveLoading: boolean,
|
||||
isApproveSuccess: boolean,
|
||||
isWithdrawalLoading: boolean,
|
||||
isWithdrawalSuccess: boolean,
|
||||
protocol: BridgeProtocol,
|
||||
): STATE {
|
||||
if (isApprovalTx && isApproveLoading) {
|
||||
return 'APPROVAL_LOADING';
|
||||
}
|
||||
if (isApprovalTx && isApproveSuccess) {
|
||||
return 'APPROVAL_CONFIRMED';
|
||||
}
|
||||
if (isApprovalTx) {
|
||||
return 'APPROVAL_NOT_STARTED';
|
||||
}
|
||||
if (!isApprovalTx && isWithdrawalLoading) {
|
||||
return 'WITHDRAWAL_LOADING';
|
||||
}
|
||||
if (!isApprovalTx && isWithdrawalSuccess) {
|
||||
return protocol === 'CCTP' ? 'CCTP_WITHDRAWAL_STARTED' : 'OP_WITHDRAWAL_STARTED';
|
||||
}
|
||||
|
||||
return 'WITHDRAWAL_NOT_STARTED';
|
||||
}
|
||||
|
||||
const Titles: Record<STATE, string> = {
|
||||
APPROVAL_NOT_STARTED: 'APPROVE IN YOUR WALLET',
|
||||
APPROVAL_LOADING: 'APPROVING',
|
||||
APPROVAL_CONFIRMED: 'APPROVED',
|
||||
WITHDRAWAL_NOT_STARTED: 'CONFIRM WITHDRAWAL IN WALLET',
|
||||
WITHDRAWAL_LOADING: 'CONFIRMING',
|
||||
OP_WITHDRAWAL_STARTED: 'WITHDRAWAL IN PROGRESS',
|
||||
CCTP_WITHDRAWAL_STARTED: 'WITHDRAWAL IN PROGRESS',
|
||||
};
|
||||
|
||||
const Icons = {
|
||||
WITHDRAW_NOT_STARTED: 'wallet',
|
||||
WITHDRAW_STARTED: '',
|
||||
const Icons: Record<STATE, string> = {
|
||||
APPROVAL_NOT_STARTED: 'wallet',
|
||||
APPROVAL_LOADING: 'wallet',
|
||||
APPROVAL_CONFIRMED: 'confirm',
|
||||
WITHDRAWAL_NOT_STARTED: 'wallet',
|
||||
WITHDRAWAL_LOADING: 'wallet',
|
||||
OP_WITHDRAWAL_STARTED: '',
|
||||
CCTP_WITHDRAWAL_STARTED: '',
|
||||
};
|
||||
|
||||
const ModalContents = {
|
||||
WITHDRAW_NOT_STARTED: 'Withdrawal will begin after confirmation.',
|
||||
WITHDRAW_STARTED: <WithdrawProgressBar status="REQUEST_SENT" />,
|
||||
const ModalContents: Record<STATE, ReactNode> = {
|
||||
APPROVAL_NOT_STARTED: 'Approval will initiate after confirmation.',
|
||||
APPROVAL_LOADING: 'Waiting for confirmations...',
|
||||
APPROVAL_CONFIRMED: 'Transaction confirmed.',
|
||||
WITHDRAWAL_NOT_STARTED: 'Withdrawal will initiate after confirmation.',
|
||||
WITHDRAWAL_LOADING: 'Waiting for confirmations...',
|
||||
OP_WITHDRAWAL_STARTED: <WithdrawProgressBar status="REQUEST_SENT" />,
|
||||
CCTP_WITHDRAWAL_STARTED: <CCTPBridgeProgressBar status="REQUEST_SENT" />,
|
||||
};
|
||||
|
||||
export function WithdrawModal({ isOpen, onClose, L2TxHash }: WithdrawModalProps) {
|
||||
const state = L2TxHash === '' ? 'WITHDRAW_NOT_STARTED' : 'WITHDRAW_STARTED';
|
||||
export function WithdrawModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
L2ApproveTxHash,
|
||||
L2WithdrawTxHash,
|
||||
isApprovalTx,
|
||||
protocol,
|
||||
}: WithdrawModalProps) {
|
||||
const { isLoading: isApproveLoading, isSuccess: isApproveSuccess } = useWaitForTransaction({
|
||||
hash: L2ApproveTxHash,
|
||||
});
|
||||
|
||||
const { isLoading: isWithdrawalLoading, isSuccess: isWithdrawalSuccess } = useWaitForTransaction({
|
||||
hash: L2WithdrawTxHash,
|
||||
});
|
||||
|
||||
const state = getState(
|
||||
isApprovalTx,
|
||||
isApproveLoading,
|
||||
isApproveSuccess,
|
||||
isWithdrawalLoading,
|
||||
isWithdrawalSuccess,
|
||||
protocol,
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -36,11 +116,11 @@ export function WithdrawModal({ isOpen, onClose, L2TxHash }: WithdrawModalProps)
|
||||
content={ModalContents[state]}
|
||||
icon={Icons[state]}
|
||||
footer={
|
||||
L2TxHash !== '' && (
|
||||
L2WithdrawTxHash && (
|
||||
<div className="text-center">
|
||||
<a
|
||||
className="text-md font-sans text-cds-primary"
|
||||
href={`${publicRuntimeConfig.l2ExplorerURL}/tx/${L2TxHash}`}
|
||||
href={`${publicRuntimeConfig.l2ExplorerURL}/tx/${L2WithdrawTxHash}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
||||
@@ -54,9 +54,9 @@ const DisclaimerContent: Record<BarStatus, ReactNode> = {
|
||||
<>
|
||||
In order to minimize security risk, withdrawals using the official Base Bridge take up to{' '}
|
||||
{challengeWindow}. After your withdrawal request is proposed onchain (within an hour) you must
|
||||
verify and complete the transaction in order to access your funds, at{' '}
|
||||
verify and complete the transaction in order to access your funds, on{' '}
|
||||
<Link href="/transactions" className="underline">
|
||||
bridge.base.org/transactions
|
||||
the transactions page
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
@@ -64,9 +64,9 @@ const DisclaimerContent: Record<BarStatus, ReactNode> = {
|
||||
VERIFYING: (
|
||||
<>
|
||||
In order to keep attackers from withdrawing your funds, there is a {challengeWindow} waiting
|
||||
period until you can complete your withdrawal. Check back at{' '}
|
||||
period until you can complete your withdrawal. Check back on{' '}
|
||||
<Link href="/transactions" className="underline">
|
||||
bridge.base.org/transactions
|
||||
the transactions page
|
||||
</Link>{' '}
|
||||
to complete your withdrawal.
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import moment from 'moment';
|
||||
|
||||
const withdrawalPhaseText = {
|
||||
@@ -18,6 +19,15 @@ const depositPhaseText = {
|
||||
FUNDS_DEPOSITED: 'Funds deposited',
|
||||
};
|
||||
|
||||
const cctpBridgePhaseText: Record<CCTPBridgePhase, string> = {
|
||||
INITIATE_CCTP_BRIDGE_PENDING: 'Processing',
|
||||
INITIATE_CCTP_BRIDGE_FAILED: 'Failed',
|
||||
FINALIZE_CCTP_BRIDGE: 'Ready to complete',
|
||||
FINALIZE_CCTP_BRIDGE_PENDING: 'Processing',
|
||||
FINALIZE_CCTP_BRIDGE_FAILED: 'Failed',
|
||||
CCTP_BRIDGE_COMPLETE: 'Funds moved',
|
||||
};
|
||||
|
||||
const withdrawalPhaseStatusText = {
|
||||
PROPOSING_ON_CHAIN: 'Wait up to 1 hr',
|
||||
PROVE: '',
|
||||
@@ -38,4 +48,10 @@ const depositPhaseStatusText = {
|
||||
FUNDS_DEPOSITED: 'Complete',
|
||||
};
|
||||
|
||||
export { depositPhaseStatusText, depositPhaseText, withdrawalPhaseStatusText, withdrawalPhaseText };
|
||||
export {
|
||||
depositPhaseStatusText,
|
||||
depositPhaseText,
|
||||
withdrawalPhaseStatusText,
|
||||
withdrawalPhaseText,
|
||||
cctpBridgePhaseText,
|
||||
};
|
||||
|
||||
326
apps/bridge/src/contract-abis/MessageTransmitter.ts
Normal file
326
apps/bridge/src/contract-abis/MessageTransmitter.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint32', name: '_localDomain', type: 'uint32' },
|
||||
{ internalType: 'address', name: '_attester', type: 'address' },
|
||||
{ internalType: 'uint32', name: '_maxMessageBodySize', type: 'uint32' },
|
||||
{ internalType: 'uint32', name: '_version', type: 'uint32' },
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'attester', type: 'address' }],
|
||||
name: 'AttesterDisabled',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'attester', type: 'address' }],
|
||||
name: 'AttesterEnabled',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'previousAttesterManager', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newAttesterManager', type: 'address' },
|
||||
],
|
||||
name: 'AttesterManagerUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'uint256', name: 'newMaxMessageBodySize', type: 'uint256' },
|
||||
],
|
||||
name: 'MaxMessageBodySizeUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'caller', type: 'address' },
|
||||
{ indexed: false, internalType: 'uint32', name: 'sourceDomain', type: 'uint32' },
|
||||
{ indexed: true, internalType: 'uint64', name: 'nonce', type: 'uint64' },
|
||||
{ indexed: false, internalType: 'bytes32', name: 'sender', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'messageBody', type: 'bytes' },
|
||||
],
|
||||
name: 'MessageReceived',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: false, internalType: 'bytes', name: 'message', type: 'bytes' }],
|
||||
name: 'MessageSent',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferStarted',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event',
|
||||
},
|
||||
{ anonymous: false, inputs: [], name: 'Pause', type: 'event' },
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'newAddress', type: 'address' }],
|
||||
name: 'PauserChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'newRescuer', type: 'address' }],
|
||||
name: 'RescuerChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'uint256', name: 'oldSignatureThreshold', type: 'uint256' },
|
||||
{ indexed: false, internalType: 'uint256', name: 'newSignatureThreshold', type: 'uint256' },
|
||||
],
|
||||
name: 'SignatureThresholdUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{ anonymous: false, inputs: [], name: 'Unpause', type: 'event' },
|
||||
{
|
||||
inputs: [],
|
||||
name: 'acceptOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'attesterManager',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'attester', type: 'address' }],
|
||||
name: 'disableAttester',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newAttester', type: 'address' }],
|
||||
name: 'enableAttester',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint256', name: 'index', type: 'uint256' }],
|
||||
name: 'getEnabledAttester',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getNumEnabledAttesters',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'attester', type: 'address' }],
|
||||
name: 'isEnabledAttester',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'localDomain',
|
||||
outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'maxMessageBodySize',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'nextAvailableNonce',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{ inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
|
||||
{
|
||||
inputs: [],
|
||||
name: 'paused',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'pauser',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'pendingOwner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes', name: 'message', type: 'bytes' },
|
||||
{ internalType: 'bytes', name: 'attestation', type: 'bytes' },
|
||||
],
|
||||
name: 'receiveMessage',
|
||||
outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes', name: 'originalMessage', type: 'bytes' },
|
||||
{ internalType: 'bytes', name: 'originalAttestation', type: 'bytes' },
|
||||
{ internalType: 'bytes', name: 'newMessageBody', type: 'bytes' },
|
||||
{ internalType: 'bytes32', name: 'newDestinationCaller', type: 'bytes32' },
|
||||
],
|
||||
name: 'replaceMessage',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'contract IERC20', name: 'tokenContract', type: 'address' },
|
||||
{ internalType: 'address', name: 'to', type: 'address' },
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
],
|
||||
name: 'rescueERC20',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'rescuer',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
|
||||
{ internalType: 'bytes32', name: 'recipient', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'messageBody', type: 'bytes' },
|
||||
],
|
||||
name: 'sendMessage',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
|
||||
{ internalType: 'bytes32', name: 'recipient', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'destinationCaller', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'messageBody', type: 'bytes' },
|
||||
],
|
||||
name: 'sendMessageWithCaller',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint256', name: 'newMaxMessageBodySize', type: 'uint256' }],
|
||||
name: 'setMaxMessageBodySize',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint256', name: 'newSignatureThreshold', type: 'uint256' }],
|
||||
name: 'setSignatureThreshold',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'signatureThreshold',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{ inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newAttesterManager', type: 'address' }],
|
||||
name: 'updateAttesterManager',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: '_newPauser', type: 'address' }],
|
||||
name: 'updatePauser',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newRescuer', type: 'address' }],
|
||||
name: 'updateRescuer',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
|
||||
name: 'usedNonces',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'version',
|
||||
outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
] as const;
|
||||
254
apps/bridge/src/contract-abis/TokenMessenger.ts
Normal file
254
apps/bridge/src/contract-abis/TokenMessenger.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_messageTransmitter', type: 'address' },
|
||||
{ internalType: 'uint32', name: '_messageBodyVersion', type: 'uint32' },
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'uint64', name: 'nonce', type: 'uint64' },
|
||||
{ indexed: true, internalType: 'address', name: 'burnToken', type: 'address' },
|
||||
{ indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
{ indexed: true, internalType: 'address', name: 'depositor', type: 'address' },
|
||||
{ indexed: false, internalType: 'bytes32', name: 'mintRecipient', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'bytes32',
|
||||
name: 'destinationTokenMessenger',
|
||||
type: 'bytes32',
|
||||
},
|
||||
{ indexed: false, internalType: 'bytes32', name: 'destinationCaller', type: 'bytes32' },
|
||||
],
|
||||
name: 'DepositForBurn',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: false, internalType: 'address', name: 'localMinter', type: 'address' }],
|
||||
name: 'LocalMinterAdded',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: false, internalType: 'address', name: 'localMinter', type: 'address' }],
|
||||
name: 'LocalMinterRemoved',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'mintRecipient', type: 'address' },
|
||||
{ indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
{ indexed: true, internalType: 'address', name: 'mintToken', type: 'address' },
|
||||
],
|
||||
name: 'MintAndWithdraw',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferStarted',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'uint32', name: 'domain', type: 'uint32' },
|
||||
{ indexed: false, internalType: 'bytes32', name: 'tokenMessenger', type: 'bytes32' },
|
||||
],
|
||||
name: 'RemoteTokenMessengerAdded',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'uint32', name: 'domain', type: 'uint32' },
|
||||
{ indexed: false, internalType: 'bytes32', name: 'tokenMessenger', type: 'bytes32' },
|
||||
],
|
||||
name: 'RemoteTokenMessengerRemoved',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'newRescuer', type: 'address' }],
|
||||
name: 'RescuerChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'acceptOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newLocalMinter', type: 'address' }],
|
||||
name: 'addLocalMinter',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint32', name: 'domain', type: 'uint32' },
|
||||
{ internalType: 'bytes32', name: 'tokenMessenger', type: 'bytes32' },
|
||||
],
|
||||
name: 'addRemoteTokenMessenger',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
|
||||
{ internalType: 'bytes32', name: 'mintRecipient', type: 'bytes32' },
|
||||
{ internalType: 'address', name: 'burnToken', type: 'address' },
|
||||
],
|
||||
name: 'depositForBurn',
|
||||
outputs: [{ internalType: 'uint64', name: '_nonce', type: 'uint64' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
{ internalType: 'uint32', name: 'destinationDomain', type: 'uint32' },
|
||||
{ internalType: 'bytes32', name: 'mintRecipient', type: 'bytes32' },
|
||||
{ internalType: 'address', name: 'burnToken', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'destinationCaller', type: 'bytes32' },
|
||||
],
|
||||
name: 'depositForBurnWithCaller',
|
||||
outputs: [{ internalType: 'uint64', name: 'nonce', type: 'uint64' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'uint32', name: 'remoteDomain', type: 'uint32' },
|
||||
{ internalType: 'bytes32', name: 'sender', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'messageBody', type: 'bytes' },
|
||||
],
|
||||
name: 'handleReceiveMessage',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'localMessageTransmitter',
|
||||
outputs: [{ internalType: 'contract IMessageTransmitter', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'localMinter',
|
||||
outputs: [{ internalType: 'contract ITokenMinter', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'messageBodyVersion',
|
||||
outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'pendingOwner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint32', name: '', type: 'uint32' }],
|
||||
name: 'remoteTokenMessengers',
|
||||
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'removeLocalMinter',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint32', name: 'domain', type: 'uint32' }],
|
||||
name: 'removeRemoteTokenMessenger',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes', name: 'originalMessage', type: 'bytes' },
|
||||
{ internalType: 'bytes', name: 'originalAttestation', type: 'bytes' },
|
||||
{ internalType: 'bytes32', name: 'newDestinationCaller', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'newMintRecipient', type: 'bytes32' },
|
||||
],
|
||||
name: 'replaceDepositForBurn',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'contract IERC20', name: 'tokenContract', type: 'address' },
|
||||
{ internalType: 'address', name: 'to', type: 'address' },
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
],
|
||||
name: 'rescueERC20',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'rescuer',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newRescuer', type: 'address' }],
|
||||
name: 'updateRescuer',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
] as const;
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
|
||||
28
apps/bridge/src/utils/assets/getDepositAssetsForChainEnv.ts
Normal file
28
apps/bridge/src/utils/assets/getDepositAssetsForChainEnv.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
|
||||
import getConfig from 'next/config';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
// Get depositable assets on current chain. Note that we do not want to include
|
||||
// bridged USDC (USDbC on L2) because it is preferable to bridge native USDC using
|
||||
// CCTP. Returned list is sorted as [ETH, USDC, ...rest].
|
||||
export function getDepositAssetsForChainEnv() {
|
||||
const assetList = getAssetListForChainEnv();
|
||||
|
||||
const nonUSDCAssets = assetList.filter((asset) => asset.L1symbol !== 'USDC');
|
||||
const bridgedUSDC = assetList.filter(
|
||||
(asset) => asset.L1symbol === 'USDC' && asset.protocol === 'OP',
|
||||
);
|
||||
const nativeUSDC = assetList.filter(
|
||||
(asset) => asset.L1symbol === 'USDC' && asset.protocol === 'CCTP',
|
||||
);
|
||||
|
||||
const usdcForChainEnv = publicRuntimeConfig.cctpEnabled === 'true' ? nativeUSDC : bridgedUSDC;
|
||||
|
||||
return [...nonUSDCAssets, ...usdcForChainEnv].sort((a, b) => {
|
||||
if (a.L1symbol === 'ETH') return -1;
|
||||
if (b.L1symbol === 'ETH') return 1;
|
||||
|
||||
return a.L1symbol === 'USDC' ? -1 : 0;
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
131
apps/bridge/src/utils/hooks/useCCTPBridgeStatus.ts
Normal file
131
apps/bridge/src/utils/hooks/useCCTPBridgeStatus.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { usePublicClient, useWaitForTransaction } from 'wagmi';
|
||||
import getConfig from 'next/config';
|
||||
import { CCTPBridgePhase } from 'apps/bridge/src/utils/transactions/phase';
|
||||
import { useQuery } from 'react-query';
|
||||
import { keccak256, getEventSelector, decodeAbiParameters, parseAbiParameter } from 'viem';
|
||||
import MessageTransmitter from 'apps/bridge/src/contract-abis/MessageTransmitter';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const l1ChainID = parseInt(publicRuntimeConfig.l1ChainID);
|
||||
const l2ChainID = parseInt(publicRuntimeConfig.l2ChainID);
|
||||
|
||||
type BridgeAttestation = {
|
||||
attestation: `0x${string}`;
|
||||
status: string;
|
||||
};
|
||||
|
||||
async function fetchBridgeAttestation(messageHash: string): Promise<BridgeAttestation | undefined> {
|
||||
const response = await fetch(
|
||||
`${publicRuntimeConfig.cctpAttestationsAPIURL}/attestations/${messageHash}`,
|
||||
);
|
||||
|
||||
return (await response.json()) as BridgeAttestation;
|
||||
}
|
||||
|
||||
type UseCCTPBridgeStatusProps = {
|
||||
initiateTxHash: `0x${string}`;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
};
|
||||
|
||||
type CCTPBridgeStatus = {
|
||||
status: CCTPBridgePhase;
|
||||
message?: `0x${string}`;
|
||||
attestation?: `0x${string}`;
|
||||
setStatus: (newStatus: CCTPBridgePhase) => void;
|
||||
};
|
||||
|
||||
export function useCCTPBridgeStatus({
|
||||
initiateTxHash,
|
||||
bridgeDirection,
|
||||
}: UseCCTPBridgeStatusProps): CCTPBridgeStatus {
|
||||
const [status, setStatus] = useState<CCTPBridgePhase>('INITIATE_CCTP_BRIDGE_PENDING');
|
||||
const [message, setMessage] = useState<`0x${string}` | undefined>(undefined);
|
||||
const [messageHash, setMessageHash] = useState<`0x${string}` | undefined>(undefined);
|
||||
const isDeposit = bridgeDirection === 'deposit';
|
||||
|
||||
// The public client we will use to simulate finalizing the bridge
|
||||
// on the other chain. Ie if we are depositing this will be an L2 client,
|
||||
// and if we are withdrawing this will be an L1 client.
|
||||
const publicClient = usePublicClient({
|
||||
chainId: isDeposit ? l2ChainID : l1ChainID,
|
||||
});
|
||||
|
||||
// Waiting for the bridge initiation on the chain that the user is bridging from.
|
||||
// Ie if we are depositing this will be an L1 tx,
|
||||
// and if we are withdrawing this will be an L2 tx.
|
||||
const { data: initiateTxReceipt } = useWaitForTransaction({
|
||||
hash: initiateTxHash,
|
||||
chainId: isDeposit ? l1ChainID : l2ChainID,
|
||||
});
|
||||
|
||||
const { data: bridgeAttestation } = useQuery(
|
||||
['bridgeAttestation', { initiateTxHash, messageHash }],
|
||||
async () => fetchBridgeAttestation(messageHash as string),
|
||||
{
|
||||
enabled: !!messageHash,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initiateTxReceipt) {
|
||||
if (initiateTxReceipt.status === 'reverted') {
|
||||
setStatus('INITIATE_CCTP_BRIDGE_FAILED');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTopic = getEventSelector('MessageSent(bytes)');
|
||||
const log = initiateTxReceipt.logs.find((l) => l.topics[0] === eventTopic);
|
||||
const messageBytes = decodeAbiParameters(
|
||||
[parseAbiParameter('bytes message')],
|
||||
log?.data as `0x${string}`,
|
||||
)[0];
|
||||
const hash = keccak256(messageBytes);
|
||||
setMessage(messageBytes);
|
||||
setMessageHash(hash);
|
||||
}
|
||||
}, [initiateTxReceipt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bridgeAttestation?.attestation && message) {
|
||||
void (async () => {
|
||||
if (bridgeAttestation.status === 'pending_confirmations') {
|
||||
setStatus('INITIATE_CCTP_BRIDGE_PENDING');
|
||||
return;
|
||||
}
|
||||
|
||||
// We still need to check whether or not the message was received on the other chain.
|
||||
// If it hasn't been received yet, the user needs to call receiveMessage on the MessageTransmitter.
|
||||
// It it has been received, the bridge is complete.
|
||||
// We can check if the message has been received by simulating a call to receiveMessage.
|
||||
// If it fails, assume it's because the message has already been received.
|
||||
try {
|
||||
await publicClient.simulateContract({
|
||||
address: isDeposit
|
||||
? publicRuntimeConfig.l2CCTPMessageTransmitterAddress
|
||||
: publicRuntimeConfig.l1CCTPMessageTransmitterAddress,
|
||||
abi: MessageTransmitter,
|
||||
functionName: 'receiveMessage',
|
||||
args: [message, bridgeAttestation.attestation],
|
||||
});
|
||||
// Success, ie the message still needs to be received.
|
||||
setStatus('FINALIZE_CCTP_BRIDGE');
|
||||
} catch (e) {
|
||||
// Failed, ie the message has already been received.
|
||||
setStatus('CCTP_BRIDGE_COMPLETE');
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [bridgeAttestation, bridgeDirection, initiateTxReceipt, isDeposit, message, publicClient]);
|
||||
|
||||
// We need to be able to set the status from the FinzliaeCCTPBridgeButton, so we expose a setter.
|
||||
return {
|
||||
status,
|
||||
message,
|
||||
attestation: bridgeAttestation?.attestation,
|
||||
setStatus: setStatus,
|
||||
};
|
||||
}
|
||||
14
apps/bridge/src/utils/hooks/useGetUSDAmount.ts
Normal file
14
apps/bridge/src/utils/hooks/useGetUSDAmount.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
|
||||
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
|
||||
|
||||
export function useGetUSDAmount(priceAPIID: string, formattedAmount: string) {
|
||||
const conversionRateData = useConversionRate({
|
||||
asset: priceAPIID,
|
||||
});
|
||||
|
||||
const amountFiat = conversionRateData
|
||||
? usdFormatter(conversionRateData * +formattedAmount)
|
||||
: '$0.00';
|
||||
|
||||
return amountFiat;
|
||||
}
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
|
||||
44
apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge.ts
Normal file
44
apps/bridge/src/utils/hooks/usePrepareFinalizeCCTPBridge.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { usePrepareContractWrite } from 'wagmi';
|
||||
import getConfig from 'next/config';
|
||||
import MessageTransmitter from 'apps/bridge/src/contract-abis/MessageTransmitter';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const BRIDGE_DIRECTION_TO_MESSAGE_TRANSMITTER: Record<'deposit' | 'withdraw', `0x${string}`> = {
|
||||
deposit: publicRuntimeConfig.l2CCTPMessageTransmitterAddress,
|
||||
withdraw: publicRuntimeConfig.l1CCTPMessageTransmitterAddress,
|
||||
};
|
||||
|
||||
const BRIDGE_DIRECTION_TO_CHAIN_ID: Record<'deposit' | 'withdraw', number> = {
|
||||
deposit: parseInt(publicRuntimeConfig.l2ChainID),
|
||||
withdraw: parseInt(publicRuntimeConfig.l1ChainID),
|
||||
};
|
||||
|
||||
type UsePrepareFinalizeCCTPBridgeProps = {
|
||||
isPermittedToBridge: boolean;
|
||||
message?: `0x${string}`;
|
||||
attestation?: `0x${string}`;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
includeTosVersionByte: boolean;
|
||||
};
|
||||
|
||||
export function usePrepareFinalizeCCTPBridge({
|
||||
isPermittedToBridge,
|
||||
message,
|
||||
attestation,
|
||||
bridgeDirection,
|
||||
includeTosVersionByte,
|
||||
}: UsePrepareFinalizeCCTPBridgeProps) {
|
||||
const shouldPrepare = isPermittedToBridge && message && attestation;
|
||||
|
||||
const { config } = usePrepareContractWrite({
|
||||
address: shouldPrepare ? BRIDGE_DIRECTION_TO_MESSAGE_TRANSMITTER[bridgeDirection] : undefined,
|
||||
abi: MessageTransmitter,
|
||||
functionName: 'receiveMessage',
|
||||
chainId: BRIDGE_DIRECTION_TO_CHAIN_ID[bridgeDirection],
|
||||
args: shouldPrepare ? [message, attestation] : undefined,
|
||||
dataSuffix: includeTosVersionByte ? publicRuntimeConfig.tosVersion : undefined,
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
56
apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge.ts
Normal file
56
apps/bridge/src/utils/hooks/usePrepareInitiateCCTPBridge.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import TokenMessenger from 'apps/bridge/src/contract-abis/TokenMessenger';
|
||||
import { Asset } from 'apps/bridge/src/types/Asset';
|
||||
import { parseUnits, pad } from 'viem';
|
||||
import getConfig from 'next/config';
|
||||
import { Address, usePrepareContractWrite } from 'wagmi';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const BRIDGE_DIRECTION_TO_TOKEN_MESSENGER: Record<'deposit' | 'withdraw', `0x${string}`> = {
|
||||
deposit: publicRuntimeConfig.l1CCTPTokenMessengerAddress,
|
||||
withdraw: publicRuntimeConfig.l2CCTPTokenMessengerAddress,
|
||||
};
|
||||
|
||||
const BRIDGE_DIRECTION_TO_CHAIN_ID: Record<'deposit' | 'withdraw', number> = {
|
||||
deposit: parseInt(publicRuntimeConfig.l1ChainID),
|
||||
withdraw: parseInt(publicRuntimeConfig.l2ChainID),
|
||||
};
|
||||
|
||||
type UsePrepareInitiateCCTPBridgeProps = {
|
||||
mintRecipient?: Address;
|
||||
asset: Asset;
|
||||
amount: string;
|
||||
destinationDomain: number;
|
||||
isPermittedToBridge: boolean;
|
||||
includeTosVersionByte: boolean;
|
||||
bridgeDirection: 'deposit' | 'withdraw';
|
||||
};
|
||||
|
||||
export function usePrepareInitiateCCTPBridge({
|
||||
mintRecipient,
|
||||
asset,
|
||||
amount,
|
||||
destinationDomain,
|
||||
isPermittedToBridge,
|
||||
bridgeDirection,
|
||||
includeTosVersionByte,
|
||||
}: UsePrepareInitiateCCTPBridgeProps) {
|
||||
const shouldPrepare = isPermittedToBridge && amount !== '' && mintRecipient;
|
||||
|
||||
const { config } = usePrepareContractWrite({
|
||||
address: shouldPrepare ? BRIDGE_DIRECTION_TO_TOKEN_MESSENGER[bridgeDirection] : undefined,
|
||||
abi: TokenMessenger,
|
||||
functionName: 'depositForBurn',
|
||||
chainId: BRIDGE_DIRECTION_TO_CHAIN_ID[bridgeDirection],
|
||||
args: shouldPrepare
|
||||
? [
|
||||
amount !== '' ? parseUnits(amount, asset.decimals) : parseUnits('0', asset.decimals),
|
||||
destinationDomain,
|
||||
pad(mintRecipient),
|
||||
(bridgeDirection === 'deposit' ? asset.L1contract : asset.L2contract) as Address,
|
||||
]
|
||||
: undefined,
|
||||
dataSuffix: includeTosVersionByte ? publicRuntimeConfig.tosVersion : undefined,
|
||||
});
|
||||
return config;
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
24
apps/bridge/src/utils/transactions/formatBlockTimestamp.ts
Normal file
24
apps/bridge/src/utils/transactions/formatBlockTimestamp.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type FormattedTimestamp = {
|
||||
date: string;
|
||||
shortDate: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
export function formatTimestamp(timestamp: string): FormattedTimestamp {
|
||||
const date = new Date(Number(timestamp) * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const shortDate = new Date(Number(timestamp) * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const time = new Date(Number(timestamp) * 1000).toLocaleTimeString('en-US', {
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
return { date, shortDate, time };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
20
yarn.lock
20
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user