mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 04:08:01 -05:00
[Feat] Bridge UIv2 - CCTPV2 claims for USDC & refactors (#792)
* did refreshCCTPMessageIfNeeded function * added BridgeTransaction to own @types file * successful claim tx * remove constants for cctp contracts * half way through useClaimingTx * fix * did cases for erc20 and eth transfer * moved claim tx to useClaimingTx * refactor for abievent types * add getNativeBridgeMessageClaimedTxHash * did useBridgeTransactionMessage * fix build * fix lint * implement useCCTPFee * removed auto-claim for usdc, move messageProof to TransactionDetail * add loading ui for Claim button * refactor history.ts file in utils * fix linting errors * implement caching of completed tx -> in empirical test cut requests from 24->9, ~2200ms -> ~1600ms response cycle time * ci fix * add comment for won't do * remove 1 redundant cctp api call for claim params * remove CCTP_TRANSFER_FEE_BUFFER * did badge * change linea main logo * change linea main logo * address edge case where reattest cause revert to pending * small pr fixes * Revert "small pr fixes" This reverts commit 2404f98419b1b7221bda617ea39f20ab6cad5830. * fixes applied again * Update bridge-ui/src/utils/chains.ts Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com> * simplified code to handle expired cctp message edge case * fix cctp casing * fix casing again * fix git casing issue * remove comments * add dynamiccontextprovider param * change mobileprovider --------- Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com> Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com>
This commit is contained in:
14
bridge-ui/public/images/logo/cctp.svg
Normal file
14
bridge-ui/public/images/logo/cctp.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="102 50 234 236" width="10em">
|
||||
<defs>
|
||||
<linearGradient id="linearGradient" x1="177.86" y1="291.18" x2="341.06" y2="127.98" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b090f5"></stop>
|
||||
<stop offset="1" stop-color="#5fbfff"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient2" x1="96.43" y1="207.75" x2="259.64" y2="44.55" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#68d7fa"></stop>
|
||||
<stop offset="1" stop-color="#4ee498"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#linearGradient)" d="M319.43,110.37l-5-8.78a5.14,5.14,0,0,0-8.11-1.08L294.82,112a5.17,5.17,0,0,0-.64,6.51,90.22,90.22,0,0,1,10,20.58l0,0a90.2,90.2,0,0,1-85.45,119,89.38,89.38,0,0,1-42.26-10.49l19.45-19.46a64.41,64.41,0,0,0,80.77-88.29,5.15,5.15,0,0,0-8.29-1.41L256.76,150a5.14,5.14,0,0,0-1.37,4.82l1,4.18a38.63,38.63,0,0,1-56.75,42.39l-5.13-2.94a5.13,5.13,0,0,0-6.2.83l-47.51,47.5a5.15,5.15,0,0,0,.51,7.73l7,5.37a114.86,114.86,0,0,0,70.46,23.88A116,116,0,0,0,319.43,110.37Z"></path>
|
||||
<path fill="url(#linearGradient2)" d="M289.21,75.82a114.83,114.83,0,0,0-70.46-23.89A116,116,0,0,0,118.06,225.37l5,8.77a5.16,5.16,0,0,0,8.12,1.09l11.48-11.48a5.19,5.19,0,0,0,.64-6.5,89.81,89.81,0,0,1-10-20.58l0,0a90.2,90.2,0,0,1,85.45-119A89.29,89.29,0,0,1,261,88.19l-19.46,19.45a64.39,64.39,0,0,0-87.21,60.23c0,1.07.29,5.95.38,6.79a64.76,64.76,0,0,0,6.07,21.27,5.16,5.16,0,0,0,8.3,1.41l11.64-11.65a5.15,5.15,0,0,0,1.38-4.81l-1-4.19a38.62,38.62,0,0,1,56.75-42.38l5.13,2.94a5.16,5.16,0,0,0,6.2-.83l47.5-47.5a5.16,5.16,0,0,0-.5-7.74Z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,30 @@
|
||||
.container {
|
||||
position: relative;
|
||||
font-family: var(--font-atyp-text);
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6.25rem;
|
||||
background-color: var(--v2-color-white);
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
line-height: 1;
|
||||
|
||||
img {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import styles from "./bridge-mode.module.scss";
|
||||
import Image from "next/image";
|
||||
import { useFormStore } from "@/stores";
|
||||
import { BridgeProvider } from "@/types";
|
||||
|
||||
export default function BridgeMode() {
|
||||
const token = useFormStore((state) => state.token);
|
||||
const label = token.bridgeProvider === BridgeProvider.NATIVE ? "Native bridge" : "CCTP";
|
||||
const logoSrc =
|
||||
token.bridgeProvider === BridgeProvider.NATIVE ? "/images/logo/linea-rounded.svg" : "/images/logo/cctp.svg";
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<button type="button" className={styles.button}>
|
||||
<div className={styles["selected-label"]}>
|
||||
<Image src={logoSrc} width={16} height={16} alt="{label}" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import Skeleton from "@/components/bridge/claiming/skeleton";
|
||||
import ReceivedAmount from "./received-amount";
|
||||
import Fees from "./fees";
|
||||
import { useFormStore, useChainStore } from "@/stores";
|
||||
import BridgeMode from "./bridge-mode";
|
||||
|
||||
export default function Claiming() {
|
||||
const fromChain = useChainStore.useFromChain();
|
||||
@@ -17,6 +18,7 @@ export default function Claiming() {
|
||||
|
||||
const amount = useFormStore((state) => state.amount);
|
||||
const balance = useFormStore((state) => state.balance);
|
||||
const isTokenCanonicalUSDC = useFormStore((state) => state.isTokenCanonicalUSDC);
|
||||
|
||||
const originChainBalanceTooLow = amount && balance < amount;
|
||||
|
||||
@@ -37,9 +39,15 @@ export default function Claiming() {
|
||||
<div className={styles.top}>
|
||||
<p className={styles.title}>Receive</p>
|
||||
<div className={styles.config}>
|
||||
<button className={styles.setting} type="button" onClick={() => setShowAdvancedSettingsModal(true)}>
|
||||
<SettingIcon />
|
||||
</button>
|
||||
<BridgeMode />
|
||||
{
|
||||
// There is no auto-claiming for USDC via CCTPV2
|
||||
!isTokenCanonicalUSDC() && (
|
||||
<button className={styles.setting} type="button" onClick={() => setShowAdvancedSettingsModal(true)}>
|
||||
<SettingIcon />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function TransactionConfirmed({ isModalOpen, transactionType, onC
|
||||
<p className={styles["text"]}>
|
||||
{transactionType === "approve"
|
||||
? "You have successfully approved the token. You can now bridge your token."
|
||||
: "You may now bridge another transaction, check your transaction history, or stay ahead of the curve with thelatest trending tokens."}
|
||||
: "You may now bridge another transaction, check your transaction history, or stay ahead of the curve with the latest trending tokens."}
|
||||
</p>
|
||||
<div className={styles["list-button"]}>
|
||||
<Link
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import styles from "./list-transaction.module.scss";
|
||||
import Transaction from "./item";
|
||||
import TransactionDetails from "@/components/bridge/transaction-history/modal/transaction-details";
|
||||
import { BridgeTransaction } from "@/utils";
|
||||
import { BridgeTransaction } from "@/types";
|
||||
|
||||
type Props = {
|
||||
transactions: BridgeTransaction[];
|
||||
|
||||
@@ -4,8 +4,8 @@ import styles from "./item.module.scss";
|
||||
import CheckIcon from "@/assets/icons/check.svg";
|
||||
import ClockIcon from "@/assets/icons/clock.svg";
|
||||
import BridgeTwoLogo from "@/components/bridge/bridge-two-logo";
|
||||
import { BridgeTransaction, getChainLogoPath, formatHex, formatTimestamp } from "@/utils";
|
||||
import { TransactionStatus } from "@/types";
|
||||
import { getChainLogoPath, formatHex, formatTimestamp } from "@/utils";
|
||||
import { BridgeTransaction, TransactionStatus } from "@/types";
|
||||
|
||||
type Props = BridgeTransaction & {
|
||||
onClick: (code: string) => void;
|
||||
|
||||
@@ -7,9 +7,9 @@ import Modal from "@/components/modal";
|
||||
import styles from "./transaction-details.module.scss";
|
||||
import Button from "@/components/ui/button";
|
||||
import ArrowRightIcon from "@/assets/icons/arrow-right.svg";
|
||||
import { useClaim } from "@/hooks";
|
||||
import { TransactionStatus } from "@/types";
|
||||
import { formatBalance, formatHex, formatTimestamp, BridgeTransaction } from "@/utils";
|
||||
import { useClaim, useClaimingTx, useBridgeTransactionMessage } from "@/hooks";
|
||||
import { BridgeTransaction, TransactionStatus } from "@/types";
|
||||
import { formatBalance, formatHex, formatTimestamp } from "@/utils";
|
||||
|
||||
type Props = {
|
||||
transaction: BridgeTransaction | undefined;
|
||||
@@ -24,14 +24,23 @@ export default function TransactionDetails({ transaction, isModalOpen, onCloseMo
|
||||
const formattedDate = transaction?.timestamp ? formatTimestamp(Number(transaction.timestamp), "MMM, dd, yyyy") : "";
|
||||
const formattedTime = transaction?.timestamp ? formatTimestamp(Number(transaction.timestamp), "ppp") : "";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
// Hydrate BridgeTransaction.message with params required for claim tx
|
||||
const { message, isLoading: isLoadingClaimTxParams } = useBridgeTransactionMessage(transaction);
|
||||
if (transaction && message) transaction.message = message;
|
||||
|
||||
// Hydrate BridgeTransaction.claimingTx
|
||||
const claimingTx = useClaimingTx(transaction);
|
||||
if (transaction && claimingTx && !transaction?.claimingTx) transaction.claimingTx = claimingTx;
|
||||
|
||||
const { claim, isConfirming, isPending, isConfirmed } = useClaim({
|
||||
status: transaction?.status,
|
||||
type: transaction?.type,
|
||||
fromChain: transaction?.fromChain,
|
||||
toChain: transaction?.toChain,
|
||||
args: transaction?.message,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
if (isConfirmed) {
|
||||
queryClient.invalidateQueries({ queryKey: ["transactionHistory"], exact: false });
|
||||
@@ -75,6 +84,10 @@ export default function TransactionDetails({ transaction, isModalOpen, onCloseMo
|
||||
}, [initialTransactionReceipt, claimingTransactionReceipt]);
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
if (isLoadingClaimTxParams) {
|
||||
return "Loading Claim Data...";
|
||||
}
|
||||
|
||||
if (isPending || isConfirming) {
|
||||
return "Waiting for confirmation...";
|
||||
}
|
||||
@@ -88,7 +101,15 @@ export default function TransactionDetails({ transaction, isModalOpen, onCloseMo
|
||||
}
|
||||
|
||||
return "Claim";
|
||||
}, [isPending, isConfirming, isSwitchingChain, chain?.id, transaction?.toChain.id, transaction?.toChain.name]);
|
||||
}, [
|
||||
isPending,
|
||||
isConfirming,
|
||||
isSwitchingChain,
|
||||
isLoadingClaimTxParams,
|
||||
chain?.id,
|
||||
transaction?.toChain.id,
|
||||
transaction?.toChain.name,
|
||||
]);
|
||||
|
||||
const handleClaim = () => {
|
||||
if (transaction?.toChain.id && chain?.id && chain.id !== transaction?.toChain.id) {
|
||||
@@ -100,6 +121,7 @@ export default function TransactionDetails({ transaction, isModalOpen, onCloseMo
|
||||
claim();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Transaction details" isOpen={isModalOpen} onClose={onCloseModal}>
|
||||
<div className={styles["modal-inner"]}>
|
||||
@@ -150,7 +172,11 @@ export default function TransactionDetails({ transaction, isModalOpen, onCloseMo
|
||||
)}
|
||||
</ul>
|
||||
{transaction?.status === TransactionStatus.READY_TO_CLAIM && (
|
||||
<Button disabled={isPending || isConfirming || isSwitchingChain} onClick={handleClaim} fullWidth>
|
||||
<Button
|
||||
disabled={isLoadingClaimTxParams || isPending || isConfirming || isSwitchingChain}
|
||||
onClick={handleClaim}
|
||||
fullWidth
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,12 @@ const chainConfigSchema = z.object({
|
||||
gasLimitSurplus: z.bigint().positive(),
|
||||
profitMargin: z.bigint().positive(),
|
||||
cctpDomain: z.number().gte(0).int(),
|
||||
cctpTokenMessengerV2Address: z.string().refine((val) => isAddress(val), {
|
||||
message: "Invalid Ethereum address",
|
||||
}),
|
||||
cctpMessageTransmitterV2Address: z.string().refine((val) => isAddress(val), {
|
||||
message: "Invalid Ethereum address",
|
||||
}),
|
||||
});
|
||||
|
||||
export const configSchema = z
|
||||
@@ -22,7 +28,7 @@ export const configSchema = z
|
||||
minVersion: z.number().positive().int(),
|
||||
}),
|
||||
// Feature toggle for CCTPV2 for USDC transfers
|
||||
isCCTPEnabled: z.boolean(),
|
||||
isCctpEnabled: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ export const config: Config = {
|
||||
? BigInt(process.env.NEXT_PUBLIC_MAINNET_PROFIT_MARGIN)
|
||||
: BigInt(1),
|
||||
cctpDomain: 0,
|
||||
cctpTokenMessengerV2Address: getAddress("0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d"),
|
||||
cctpMessageTransmitterV2Address: getAddress("0x81D40F21F12A8F0E3252Bccb954D722d4c464B64"),
|
||||
},
|
||||
59144: {
|
||||
iconPath: "/images/logo/linea-mainnet.svg",
|
||||
iconPath: "/images/logo/linea-rounded.svg",
|
||||
messageServiceAddress: getAddress(process.env.NEXT_PUBLIC_MAINNET_LINEA_MESSAGE_SERVICE ?? ""),
|
||||
tokenBridgeAddress: getAddress(process.env.NEXT_PUBLIC_MAINNET_LINEA_TOKEN_BRIDGE ?? ""),
|
||||
gasLimitSurplus: process.env.NEXT_PUBLIC_MAINNET_DEFAULT_GAS_LIMIT_SURPLUS
|
||||
@@ -26,6 +28,8 @@ export const config: Config = {
|
||||
? BigInt(process.env.NEXT_PUBLIC_MAINNET_PROFIT_MARGIN)
|
||||
: BigInt(1),
|
||||
cctpDomain: 11,
|
||||
cctpTokenMessengerV2Address: getAddress("0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d"),
|
||||
cctpMessageTransmitterV2Address: getAddress("0x81D40F21F12A8F0E3252Bccb954D722d4c464B64"),
|
||||
},
|
||||
11155111: {
|
||||
iconPath: "/images/logo/ethereum-rounded.svg",
|
||||
@@ -38,6 +42,8 @@ export const config: Config = {
|
||||
? BigInt(process.env.NEXT_PUBLIC_SEPOLIA_PROFIT_MARGIN)
|
||||
: BigInt(1),
|
||||
cctpDomain: 0,
|
||||
cctpTokenMessengerV2Address: getAddress("0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"),
|
||||
cctpMessageTransmitterV2Address: getAddress("0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"),
|
||||
},
|
||||
59141: {
|
||||
iconPath: "/images/logo/linea-sepolia.svg",
|
||||
@@ -50,6 +56,8 @@ export const config: Config = {
|
||||
? BigInt(process.env.NEXT_PUBLIC_SEPOLIA_PROFIT_MARGIN)
|
||||
: BigInt(1),
|
||||
cctpDomain: 11,
|
||||
cctpTokenMessengerV2Address: getAddress("0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"),
|
||||
cctpMessageTransmitterV2Address: getAddress("0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"),
|
||||
},
|
||||
},
|
||||
walletConnectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_ID ?? "",
|
||||
@@ -57,7 +65,7 @@ export const config: Config = {
|
||||
// The storage will be cleared if its version is smaller than the one configured
|
||||
minVersion: process.env.NEXT_PUBLIC_STORAGE_MIN_VERSION ? parseInt(process.env.NEXT_PUBLIC_STORAGE_MIN_VERSION) : 1,
|
||||
},
|
||||
isCCTPEnabled: process.env.NEXT_PUBLIC_IS_CCTP_ENABLED === "true",
|
||||
isCctpEnabled: process.env.NEXT_PUBLIC_IS_CCTP_ENABLED === "true",
|
||||
};
|
||||
|
||||
export async function getConfiguration(): Promise<Config> {
|
||||
|
||||
@@ -67,6 +67,7 @@ export function Web3Provider({ children }: Web3ProviderProps) {
|
||||
settings={{
|
||||
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
|
||||
walletConnectors: [EthereumWalletConnectors],
|
||||
mobileExperience: "redirect",
|
||||
appName: "Linea Bridge",
|
||||
cssOverrides,
|
||||
}}
|
||||
|
||||
@@ -2,8 +2,10 @@ export * from "./fees";
|
||||
export * from "./transaction-args";
|
||||
export { default as useAllowance } from "./useAllowance";
|
||||
export { default as useBridge } from "./useBridge";
|
||||
export { default as useBridgeTransactionMessage } from "./useBridgeTransactionMessage";
|
||||
export { default as useChains } from "./useChains";
|
||||
export { default as useClaim } from "./useClaim";
|
||||
export { default as useClaimingTx } from "./useClaimingTx";
|
||||
export { default as useDebounce } from "./useDebounce";
|
||||
export { default as useDevice } from "./useDevice";
|
||||
export { default as useInitialiseChain } from "./useInitialiseChain";
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useChainStore } from "@/stores";
|
||||
|
||||
const useCctpDestinationDomain = () => {
|
||||
const toChain = useChainStore.useToChain();
|
||||
return toChain.cctpDomain;
|
||||
};
|
||||
|
||||
export default useCctpDestinationDomain;
|
||||
@@ -0,0 +1,28 @@
|
||||
// Break pattern of 1 hook-1 file because TypeScript and CI were going nuts on filenames such as useCctpDestinationDomain.ts
|
||||
|
||||
import { useChainStore } from "@/stores";
|
||||
import { getCctpFee } from "@/services/cctp";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CCTP_TRANSFER_MAX_FEE_FALLBACK } from "@/utils/cctp";
|
||||
|
||||
const useCctpSrcDomain = () => {
|
||||
const fromChain = useChainStore.useFromChain();
|
||||
return fromChain.cctpDomain;
|
||||
};
|
||||
|
||||
export const useCctpDestinationDomain = () => {
|
||||
const toChain = useChainStore.useToChain();
|
||||
return toChain.cctpDomain;
|
||||
};
|
||||
|
||||
export const useCctpFee = (): bigint => {
|
||||
const fromChain = useChainStore.useFromChain();
|
||||
const srcDomain = useCctpSrcDomain();
|
||||
const dstDomain = useCctpDestinationDomain();
|
||||
const { data } = useQuery({
|
||||
queryKey: ["useCctpFee", srcDomain, dstDomain],
|
||||
queryFn: async () => getCctpFee(srcDomain, dstDomain, fromChain.testnet),
|
||||
});
|
||||
if (!data) return CCTP_TRANSFER_MAX_FEE_FALLBACK;
|
||||
return BigInt(data.minimumFee);
|
||||
};
|
||||
@@ -3,8 +3,8 @@ import { useAccount } from "wagmi";
|
||||
import { encodeFunctionData, padHex, zeroHash } from "viem";
|
||||
import { useFormStore, useChainStore } from "@/stores";
|
||||
import { isCctp } from "@/utils/tokens";
|
||||
import useCctpDestinationDomain from "./useCctpDestinationDomain";
|
||||
import { CCTP_MIN_FINALITY_THRESHOLD, CCTP_TOKEN_MESSENGER, CCTP_TRANSFER_MAX_FEE } from "@/utils/cctp";
|
||||
import { useCctpFee, useCctpDestinationDomain } from "./useCctpUtilHooks";
|
||||
import { CCTP_MIN_FINALITY_THRESHOLD } from "@/utils";
|
||||
|
||||
type UseDepositForBurnTxArgs = {
|
||||
allowance?: bigint;
|
||||
@@ -17,6 +17,7 @@ const useDepositForBurnTxArgs = ({ allowance }: UseDepositForBurnTxArgs) => {
|
||||
const token = useFormStore((state) => state.token);
|
||||
const amount = useFormStore((state) => state.amount);
|
||||
const recipient = useFormStore((state) => state.recipient);
|
||||
const fee = useCctpFee();
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
@@ -35,7 +36,7 @@ const useDepositForBurnTxArgs = ({ allowance }: UseDepositForBurnTxArgs) => {
|
||||
return {
|
||||
type: "depositForBurn",
|
||||
args: {
|
||||
to: CCTP_TOKEN_MESSENGER,
|
||||
to: fromChain.cctpTokenMessengerV2Address,
|
||||
data: encodeFunctionData({
|
||||
abi: [
|
||||
{
|
||||
@@ -61,7 +62,7 @@ const useDepositForBurnTxArgs = ({ allowance }: UseDepositForBurnTxArgs) => {
|
||||
padHex(recipient),
|
||||
token[fromChain.layer],
|
||||
zeroHash,
|
||||
CCTP_TRANSFER_MAX_FEE,
|
||||
fee,
|
||||
CCTP_MIN_FINALITY_THRESHOLD,
|
||||
],
|
||||
}),
|
||||
@@ -69,7 +70,7 @@ const useDepositForBurnTxArgs = ({ allowance }: UseDepositForBurnTxArgs) => {
|
||||
chainId: fromChain.id,
|
||||
},
|
||||
};
|
||||
}, [address, allowance, amount, cctpDestinationDomain, fromChain, recipient, token]);
|
||||
}, [address, allowance, amount, fee, cctpDestinationDomain, fromChain, recipient, token]);
|
||||
};
|
||||
|
||||
export default useDepositForBurnTxArgs;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { encodeFunctionData, erc20Abi } from "viem";
|
||||
import { useFormStore, useChainStore } from "@/stores";
|
||||
import { isEth } from "@/utils";
|
||||
import { isCctp } from "@/utils/tokens";
|
||||
import { CCTP_TOKEN_MESSENGER } from "@/utils/cctp";
|
||||
|
||||
type UseERC20ApproveTxArgsProps = {
|
||||
allowance?: bigint;
|
||||
@@ -21,7 +20,7 @@ const useApproveTxArgs = ({ allowance }: UseERC20ApproveTxArgsProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const spender = !isCctp(token) ? fromChain.tokenBridgeAddress : CCTP_TOKEN_MESSENGER;
|
||||
const spender = !isCctp(token) ? fromChain.tokenBridgeAddress : fromChain.cctpTokenMessengerV2Address;
|
||||
|
||||
return {
|
||||
type: "approve",
|
||||
|
||||
@@ -2,77 +2,109 @@ import { useMemo } from "react";
|
||||
import { useAccount } from "wagmi";
|
||||
import { encodeFunctionData, zeroAddress } from "viem";
|
||||
import MessageService from "@/abis/MessageService.json";
|
||||
import { Chain, ChainLayer, TransactionStatus } from "@/types";
|
||||
import { CCTPV2BridgeMessage, NativeBridgeMessage } from "@/utils/history";
|
||||
import { isNativeBridgeMessage } from "@/utils/message";
|
||||
import MessageTransmitterV2 from "@/abis/MessageTransmitterV2.json";
|
||||
import {
|
||||
BridgeTransactionType,
|
||||
CctpV2BridgeMessage,
|
||||
Chain,
|
||||
ChainLayer,
|
||||
NativeBridgeMessage,
|
||||
TransactionStatus,
|
||||
} from "@/types";
|
||||
import { isCctpV2BridgeMessage, isNativeBridgeMessage } from "@/utils/message";
|
||||
|
||||
type UseClaimTxArgsProps = {
|
||||
status?: TransactionStatus;
|
||||
type?: BridgeTransactionType;
|
||||
fromChain?: Chain;
|
||||
toChain?: Chain;
|
||||
args?: NativeBridgeMessage | CCTPV2BridgeMessage;
|
||||
args?: NativeBridgeMessage | CctpV2BridgeMessage;
|
||||
};
|
||||
|
||||
const useClaimTxArgs = ({ status, fromChain, toChain, args }: UseClaimTxArgsProps) => {
|
||||
const useClaimTxArgs = ({ status, type, fromChain, toChain, args }: UseClaimTxArgsProps) => {
|
||||
const { address } = useAccount();
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
!address ||
|
||||
!status ||
|
||||
status !== TransactionStatus.READY_TO_CLAIM ||
|
||||
!fromChain ||
|
||||
!toChain ||
|
||||
!args ||
|
||||
!isNativeBridgeMessage(args) ||
|
||||
!args.from ||
|
||||
!args.to ||
|
||||
args.fee === undefined ||
|
||||
args.value === undefined ||
|
||||
!args.nonce ||
|
||||
!args.calldata ||
|
||||
!args.messageHash ||
|
||||
(!args.proof && toChain.layer === ChainLayer.L1)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Missing props
|
||||
if (!address || !status || !type || !fromChain || !toChain || !args) return;
|
||||
|
||||
const encodedData =
|
||||
toChain.layer === ChainLayer.L1
|
||||
? encodeFunctionData({
|
||||
abi: MessageService.abi,
|
||||
functionName: "claimMessageWithProof",
|
||||
args: [
|
||||
{
|
||||
data: args.calldata,
|
||||
fee: args.fee,
|
||||
feeRecipient: zeroAddress,
|
||||
from: args.from,
|
||||
to: args.to,
|
||||
leafIndex: args.proof?.leafIndex,
|
||||
merkleRoot: args.proof?.root,
|
||||
messageNumber: args.nonce,
|
||||
proof: args.proof?.proof,
|
||||
value: args.value,
|
||||
},
|
||||
],
|
||||
})
|
||||
: encodeFunctionData({
|
||||
abi: MessageService.abi,
|
||||
functionName: "claimMessage",
|
||||
args: [args.from, args.to, args.fee, args.value, zeroAddress, args.calldata, args.nonce],
|
||||
});
|
||||
// Must be 'READY_TO_CLAIM' status
|
||||
if (status !== TransactionStatus.READY_TO_CLAIM) return;
|
||||
|
||||
let toAddress: `0x${string}`;
|
||||
let encodedData: `0x${string}`;
|
||||
|
||||
switch (type) {
|
||||
case BridgeTransactionType.ERC20:
|
||||
case BridgeTransactionType.ETH:
|
||||
if (
|
||||
!isNativeBridgeMessage(args) ||
|
||||
!args.from ||
|
||||
!args.to ||
|
||||
args.fee === undefined ||
|
||||
args.value === undefined ||
|
||||
!args.nonce ||
|
||||
!args.calldata ||
|
||||
!args.messageHash ||
|
||||
(!args.proof && toChain.layer === ChainLayer.L1)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
toAddress = toChain.messageServiceAddress;
|
||||
|
||||
encodedData =
|
||||
toChain.layer === ChainLayer.L1
|
||||
? encodeFunctionData({
|
||||
abi: MessageService.abi,
|
||||
functionName: "claimMessageWithProof",
|
||||
args: [
|
||||
{
|
||||
data: args.calldata,
|
||||
fee: args.fee,
|
||||
feeRecipient: zeroAddress,
|
||||
from: args.from,
|
||||
to: args.to,
|
||||
leafIndex: args.proof?.leafIndex,
|
||||
merkleRoot: args.proof?.root,
|
||||
messageNumber: args.nonce,
|
||||
proof: args.proof?.proof,
|
||||
value: args.value,
|
||||
},
|
||||
],
|
||||
})
|
||||
: encodeFunctionData({
|
||||
abi: MessageService.abi,
|
||||
functionName: "claimMessage",
|
||||
args: [args.from, args.to, args.fee, args.value, zeroAddress, args.calldata, args.nonce],
|
||||
});
|
||||
|
||||
break;
|
||||
case BridgeTransactionType.USDC:
|
||||
if (!isCctpV2BridgeMessage(args) || !args.attestation || !args.message) {
|
||||
return;
|
||||
}
|
||||
toAddress = toChain.cctpMessageTransmitterV2Address;
|
||||
encodedData = encodeFunctionData({
|
||||
abi: MessageTransmitterV2.abi,
|
||||
functionName: "receiveMessage",
|
||||
args: [args.message, args.attestation],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "claim",
|
||||
args: {
|
||||
to: toChain.messageServiceAddress,
|
||||
to: toAddress,
|
||||
data: encodedData,
|
||||
value: 0n,
|
||||
chainId: toChain.id,
|
||||
},
|
||||
};
|
||||
}, [address, args, fromChain, status, toChain]);
|
||||
}, [address, status, type, fromChain, toChain, args]);
|
||||
};
|
||||
|
||||
export default useClaimTxArgs;
|
||||
|
||||
@@ -3,13 +3,12 @@ import { erc20Abi } from "viem";
|
||||
import { useFormStore, useChainStore } from "@/stores";
|
||||
import { isEth } from "@/utils";
|
||||
import { isCctp } from "@/utils/tokens";
|
||||
import { CCTP_TOKEN_MESSENGER } from "@/utils/cctp";
|
||||
|
||||
const useAllowance = () => {
|
||||
const { address } = useAccount();
|
||||
const token = useFormStore((state) => state.token);
|
||||
const fromChain = useChainStore.useFromChain();
|
||||
const spender = !isCctp(token) ? fromChain.tokenBridgeAddress : CCTP_TOKEN_MESSENGER;
|
||||
const spender = !isCctp(token) ? fromChain.tokenBridgeAddress : fromChain.cctpTokenMessengerV2Address;
|
||||
|
||||
const {
|
||||
data: allowance,
|
||||
|
||||
61
bridge-ui/src/hooks/useBridgeTransactionMessage.ts
Normal file
61
bridge-ui/src/hooks/useBridgeTransactionMessage.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
BridgeTransaction,
|
||||
BridgeTransactionType,
|
||||
CctpV2BridgeMessage,
|
||||
ChainLayer,
|
||||
NativeBridgeMessage,
|
||||
TransactionStatus,
|
||||
} from "@/types";
|
||||
import { isNativeBridgeMessage } from "@/utils/message";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import useLineaSDK from "./useLineaSDK";
|
||||
|
||||
const useBridgeTransactionMessage = (
|
||||
transaction: BridgeTransaction | undefined,
|
||||
): { message: CctpV2BridgeMessage | NativeBridgeMessage | undefined; isLoading: boolean } => {
|
||||
const { lineaSDK } = useLineaSDK();
|
||||
|
||||
// queryFn for useQuery cannot return undefined - https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
|
||||
async function getBridgeTransactionMessage(
|
||||
transaction: BridgeTransaction | undefined,
|
||||
): Promise<CctpV2BridgeMessage | NativeBridgeMessage> {
|
||||
const { status, type, fromChain, toChain, message } = transaction as BridgeTransaction;
|
||||
if (!status || !type || !fromChain || !toChain || !message) return message;
|
||||
// Cannot claim, so don't waste time getting claim parameters
|
||||
if (status !== TransactionStatus.READY_TO_CLAIM) return message;
|
||||
|
||||
switch (type) {
|
||||
case BridgeTransactionType.ETH: {
|
||||
if (toChain.layer === ChainLayer.L2) return message;
|
||||
if (!isNativeBridgeMessage(message) || !message?.messageHash) return message;
|
||||
const { messageHash } = message;
|
||||
message.proof = await lineaSDK.getL1ClaimingService().getMessageProof(messageHash);
|
||||
return message;
|
||||
}
|
||||
case BridgeTransactionType.ERC20: {
|
||||
if (toChain.layer === ChainLayer.L2) return message;
|
||||
if (!isNativeBridgeMessage(message) || !message?.messageHash) return message;
|
||||
const { messageHash } = message;
|
||||
message.proof = await lineaSDK.getL1ClaimingService().getMessageProof(messageHash);
|
||||
return message;
|
||||
}
|
||||
case BridgeTransactionType.USDC: {
|
||||
// If message is READY_TO_CLAIM, then we will have already gotten the required params in TransactionList component.
|
||||
return message;
|
||||
}
|
||||
default: {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
// TODO - Do we need to account for undefined props here? Otherwise caching behaviour is not as expected?
|
||||
queryKey: ["useBridgeTransactionMessage", transaction?.bridgingTx, transaction?.toChain?.id],
|
||||
queryFn: async () => getBridgeTransactionMessage(transaction),
|
||||
});
|
||||
|
||||
return { message: data, isLoading };
|
||||
};
|
||||
|
||||
export default useBridgeTransactionMessage;
|
||||
@@ -1,18 +1,17 @@
|
||||
import { useSendTransaction, useWaitForTransactionReceipt } from "wagmi";
|
||||
import useClaimTxArgs from "./transaction-args/useClaimTransactionTxArgs";
|
||||
import { Chain, TransactionStatus } from "@/types";
|
||||
import { CCTPV2BridgeMessage, NativeBridgeMessage } from "@/utils/history";
|
||||
import { BridgeTransactionType, CctpV2BridgeMessage, Chain, NativeBridgeMessage, TransactionStatus } from "@/types";
|
||||
|
||||
type UseClaimProps = {
|
||||
status?: TransactionStatus;
|
||||
type?: BridgeTransactionType;
|
||||
fromChain?: Chain;
|
||||
toChain?: Chain;
|
||||
args?: NativeBridgeMessage | CCTPV2BridgeMessage;
|
||||
args?: NativeBridgeMessage | CctpV2BridgeMessage;
|
||||
};
|
||||
|
||||
const useClaim = (props: UseClaimProps) => {
|
||||
const transactionArgs = useClaimTxArgs(props);
|
||||
|
||||
const { data: hash, sendTransaction, isPending, error, isSuccess } = useSendTransaction();
|
||||
|
||||
const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
|
||||
|
||||
69
bridge-ui/src/hooks/useClaimingTx.ts
Normal file
69
bridge-ui/src/hooks/useClaimingTx.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { BridgeTransaction, BridgeTransactionType, TransactionStatus, CctpMessageReceivedAbiEvent } from "@/types";
|
||||
import { getPublicClient } from "@wagmi/core";
|
||||
import { config as wagmiConfig } from "@/lib/wagmi";
|
||||
import { isNativeBridgeMessage, isCctpV2BridgeMessage } from "@/utils/message";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getNativeBridgeMessageClaimedTxHash } from "@/utils";
|
||||
|
||||
const useClaimingTx = (transaction: BridgeTransaction | undefined): string | undefined => {
|
||||
// queryFn for useQuery cannot return undefined - https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
|
||||
const { data } = useQuery({
|
||||
// TODO - Do we need to account for undefined props here? Otherwise caching behaviour is not as expected?
|
||||
queryKey: ["useClaimingTx", transaction?.bridgingTx, transaction?.toChain?.id],
|
||||
queryFn: async () => getClaimTx(transaction),
|
||||
});
|
||||
|
||||
if (!data || data === "") return;
|
||||
return data;
|
||||
};
|
||||
|
||||
export default useClaimingTx;
|
||||
|
||||
async function getClaimTx(transaction: BridgeTransaction | undefined): Promise<string> {
|
||||
if (!transaction) return "";
|
||||
if (transaction?.claimingTx) return "";
|
||||
const { status, type, toChain, message } = transaction;
|
||||
if (!status || !type || !toChain || !message) return "";
|
||||
// Not completed -> no existing claim tx
|
||||
if (status !== TransactionStatus.COMPLETED) return "";
|
||||
|
||||
const toChainClient = getPublicClient(wagmiConfig, {
|
||||
chainId: toChain.id,
|
||||
});
|
||||
|
||||
switch (type) {
|
||||
case BridgeTransactionType.ETH: {
|
||||
if (!isNativeBridgeMessage(message)) return "";
|
||||
return await getNativeBridgeMessageClaimedTxHash(
|
||||
toChainClient,
|
||||
toChain.messageServiceAddress,
|
||||
message?.messageHash as `0x${string}`,
|
||||
);
|
||||
}
|
||||
case BridgeTransactionType.ERC20: {
|
||||
if (!isNativeBridgeMessage(message)) return "";
|
||||
return await getNativeBridgeMessageClaimedTxHash(
|
||||
toChainClient,
|
||||
toChain.messageServiceAddress,
|
||||
message?.messageHash as `0x${string}`,
|
||||
);
|
||||
}
|
||||
case BridgeTransactionType.USDC: {
|
||||
if (!isCctpV2BridgeMessage(message) || !message.nonce) return "";
|
||||
const messageReceivedEvents = await toChainClient.getLogs({
|
||||
event: CctpMessageReceivedAbiEvent,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: toChain.cctpMessageTransmitterV2Address,
|
||||
args: {
|
||||
nonce: message?.nonce,
|
||||
},
|
||||
});
|
||||
if (messageReceivedEvents.length === 0) return "";
|
||||
return messageReceivedEvents[0].transactionHash;
|
||||
}
|
||||
default: {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
||||
import { useChainStore, useTokenStore } from "@/stores";
|
||||
import { ChainLayer, Token } from "@/types";
|
||||
import { config } from "@/config";
|
||||
import { USDC_SYMBOL } from "@/utils";
|
||||
|
||||
const useTokens = (): Token[] => {
|
||||
const tokensList = useTokenStore((state) => state.tokensList);
|
||||
@@ -13,19 +14,23 @@ const useTokens = (): Token[] => {
|
||||
if (fromChain.testnet) {
|
||||
if (fromChain.layer !== ChainLayer.L2) {
|
||||
return tokensList.SEPOLIA.filter(
|
||||
(token) => !token.type.includes("native") && (config.isCCTPEnabled || token.symbol !== "USDC"),
|
||||
(token) => !token.type.includes("native") && (config.isCctpEnabled || token.symbol !== USDC_SYMBOL),
|
||||
);
|
||||
}
|
||||
return config.isCCTPEnabled ? tokensList.SEPOLIA : tokensList.SEPOLIA.filter((token) => token.symbol !== "USDC");
|
||||
return config.isCctpEnabled
|
||||
? tokensList.SEPOLIA
|
||||
: tokensList.SEPOLIA.filter((token) => token.symbol !== USDC_SYMBOL);
|
||||
}
|
||||
|
||||
if (fromChain.layer !== ChainLayer.L2) {
|
||||
return tokensList.MAINNET.filter(
|
||||
(token) => !token.type.includes("native") && (config.isCCTPEnabled || token.symbol !== "USDC"),
|
||||
(token) => !token.type.includes("native") && (config.isCctpEnabled || token.symbol !== USDC_SYMBOL),
|
||||
);
|
||||
}
|
||||
|
||||
return config.isCCTPEnabled ? tokensList.MAINNET : tokensList.MAINNET.filter((token) => token.symbol !== "USDC");
|
||||
return config.isCctpEnabled
|
||||
? tokensList.MAINNET
|
||||
: tokensList.MAINNET.filter((token) => token.symbol !== USDC_SYMBOL);
|
||||
}, [fromChain, tokensList.MAINNET, tokensList.SEPOLIA]);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
import { useAccount } from "wagmi";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useChainStore } from "@/stores";
|
||||
import { fetchTransactionsHistory } from "@/utils";
|
||||
import { HistoryActionsForCompleteTxCaching, useChainStore } from "@/stores";
|
||||
import useLineaSDK from "./useLineaSDK";
|
||||
import useTokens from "./useTokens";
|
||||
import { useHistoryStore } from "@/stores";
|
||||
import { fetchTransactionsHistory } from "@/utils";
|
||||
|
||||
const useTransactionHistory = () => {
|
||||
const { address } = useAccount();
|
||||
const fromChain = useChainStore.useFromChain();
|
||||
const toChain = useChainStore.useToChain();
|
||||
const { lineaSDK, lineaSDKContracts } = useLineaSDK();
|
||||
const { lineaSDK } = useLineaSDK();
|
||||
const tokens = useTokens();
|
||||
const { setCompleteTx, getCompleteTx } = useHistoryStore((state) => ({
|
||||
setCompleteTx: state.setCompleteTx,
|
||||
getCompleteTx: state.getCompleteTx,
|
||||
}));
|
||||
const historyStoreActions: HistoryActionsForCompleteTxCaching = {
|
||||
setCompleteTx,
|
||||
getCompleteTx,
|
||||
};
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
enabled: !!address && !!fromChain && !!toChain,
|
||||
queryKey: ["transactionHistory", address, fromChain.id, toChain.id],
|
||||
queryFn: () =>
|
||||
fetchTransactionsHistory({ lineaSDK, lineaSDKContracts, fromChain, toChain, address: address!, tokens }),
|
||||
fetchTransactionsHistory({
|
||||
lineaSDK,
|
||||
fromChain,
|
||||
toChain,
|
||||
address: address!,
|
||||
tokens,
|
||||
historyStoreActions,
|
||||
}),
|
||||
staleTime: 1000 * 60 * 2,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,23 +1,83 @@
|
||||
import { CctpAttestationApiResponse } from "@/types/cctp";
|
||||
import { CctpAttestationApiResponse, CctpV2ReattestationApiResponse, CctpFeeApiResponse } from "@/types/cctp";
|
||||
|
||||
export async function fetchCctpAttestation(
|
||||
transactionHash: string,
|
||||
export async function fetchCctpAttestationByTxHash(
|
||||
cctpDomain: number,
|
||||
transactionHash: string,
|
||||
isTestnet: boolean,
|
||||
): Promise<CctpAttestationApiResponse> {
|
||||
const response = await fetch(
|
||||
`https://iris-api-sandbox.circle.com/v2/messages/${cctpDomain}?transactionHash=${transactionHash}`,
|
||||
`https://iris-api${isTestnet ? "-sandbox" : ""}.circle.com/v2/messages/${cctpDomain}?transactionHash=${transactionHash}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error in fetchCctpAttestation: transactionHash=${transactionHash} cctpDomain=${cctpDomain}`);
|
||||
throw new Error(
|
||||
`Error in fetchCctpAttestationByTxHash: isTestnet=${isTestnet} cctpDomain=${cctpDomain} transactionHash=${transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data: CctpAttestationApiResponse = await response.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchCctpAttestationByNonce(
|
||||
cctpDomain: number,
|
||||
nonce: string,
|
||||
isTestnet: boolean,
|
||||
): Promise<CctpAttestationApiResponse> {
|
||||
const response = await fetch(
|
||||
`https://iris-api${isTestnet ? "-sandbox" : ""}.circle.com/v2/messages/${cctpDomain}?nonce=${nonce}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Error in fetchCctpAttestationByNonce: isTestnet=${isTestnet} cctpDomain=${cctpDomain} nonce=${nonce}`,
|
||||
);
|
||||
}
|
||||
const data: CctpAttestationApiResponse = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
// https://developers.circle.com/api-reference/stablecoins/common/reattest-message
|
||||
export async function reattestCctpV2PreFinalityMessage(
|
||||
nonce: string,
|
||||
isTestnet: boolean,
|
||||
): Promise<CctpV2ReattestationApiResponse> {
|
||||
const response = await fetch(`https://iris-api${isTestnet ? "-sandbox" : ""}.circle.com/v2/reattest/${nonce}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error in reattestCctpV2PreFinalityMessage: isTestnet=${isTestnet} nonce=${nonce}`);
|
||||
}
|
||||
const data: CctpV2ReattestationApiResponse = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCctpFee(
|
||||
srcDomain: number,
|
||||
dstDomain: number,
|
||||
isTestnet: boolean,
|
||||
): Promise<CctpFeeApiResponse> {
|
||||
const response = await fetch(
|
||||
`https://iris-api${isTestnet ? "-sandbox" : ""}.circle.com/v2/fastBurn/USDC/fees/${srcDomain}/${dstDomain}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error in getCctpFee: isTestnet=${isTestnet} srcDomain=${srcDomain} dstDomain=${dstDomain}`);
|
||||
}
|
||||
const data: CctpFeeApiResponse = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -88,20 +88,20 @@ export async function getTokenConfig(): Promise<NetworkTokens> {
|
||||
const updatedTokensConfig = { ...defaultTokensConfig };
|
||||
|
||||
// Feature toggle, remove when feature toggle no longer needed
|
||||
const filterOutUSDCWhenCCTPNotEnabled = (token: Token) => config.isCCTPEnabled || token.symbol !== "USDC";
|
||||
const filterOutUSDCWhenCctpNotEnabled = (token: Token) => config.isCctpEnabled || token.symbol !== "USDC";
|
||||
|
||||
updatedTokensConfig.MAINNET = [
|
||||
...defaultTokensConfig.MAINNET,
|
||||
...(await Promise.all(mainnetTokens.map(async (token: GithubTokenListToken): Promise<Token> => formatToken(token))))
|
||||
// Feature toggle, remove .filter expression when feature toggle no longer needed
|
||||
.filter(filterOutUSDCWhenCCTPNotEnabled),
|
||||
.filter(filterOutUSDCWhenCctpNotEnabled),
|
||||
];
|
||||
|
||||
updatedTokensConfig.SEPOLIA = [
|
||||
...defaultTokensConfig.SEPOLIA,
|
||||
...(await Promise.all(sepoliaTokens.map((token: GithubTokenListToken): Promise<Token> => formatToken(token))))
|
||||
// Feature toggle, remove .filter expression when feature toggle no longer needed
|
||||
.filter(filterOutUSDCWhenCCTPNotEnabled),
|
||||
.filter(filterOutUSDCWhenCctpNotEnabled),
|
||||
];
|
||||
|
||||
return updatedTokensConfig;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defaultTokensConfig } from "./tokenStore";
|
||||
import { createWithEqualityFn } from "zustand/traditional";
|
||||
import { shallow } from "zustand/vanilla/shallow";
|
||||
import { Token } from "@/types";
|
||||
import { isCctp } from "@/utils";
|
||||
|
||||
export type FormState = {
|
||||
token: Token;
|
||||
@@ -25,6 +26,8 @@ export type FormActions = {
|
||||
setBridgingFees: (bridgingFees: bigint) => void;
|
||||
setMinimumFees: (minimumFees: bigint) => void;
|
||||
resetForm(): void;
|
||||
// Custom getter function
|
||||
isTokenCanonicalUSDC: () => boolean;
|
||||
};
|
||||
|
||||
export type FormStore = FormState & FormActions;
|
||||
@@ -41,11 +44,15 @@ export const defaultInitState: FormState = {
|
||||
};
|
||||
|
||||
export const createFormStore = (defaultValues?: FormState) =>
|
||||
createWithEqualityFn<FormStore>((set) => {
|
||||
createWithEqualityFn<FormStore>((set, get) => {
|
||||
return {
|
||||
...defaultInitState,
|
||||
...defaultValues,
|
||||
setToken: (token) => set({ token }),
|
||||
setToken: (token) => {
|
||||
set({ token });
|
||||
// No auto-claim for CCTP
|
||||
isCctp(token) ? set({ claim: "manual" }) : set({ claim: "auto" });
|
||||
},
|
||||
setRecipient: (recipient) => set({ recipient }),
|
||||
setAmount: (amount) => set({ amount }),
|
||||
setBalance: (balance) => set({ balance }),
|
||||
@@ -54,5 +61,7 @@ export const createFormStore = (defaultValues?: FormState) =>
|
||||
setBridgingFees: (bridgingFees) => set({ bridgingFees }),
|
||||
setMinimumFees: (minimumFees) => set({ minimumFees }),
|
||||
resetForm: () => set(defaultInitState),
|
||||
// Custom getter function
|
||||
isTokenCanonicalUSDC: () => isCctp(get().token),
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
@@ -2,33 +2,40 @@ import { createWithEqualityFn } from "zustand/traditional";
|
||||
import { shallow } from "zustand/vanilla/shallow";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import { config } from "@/config";
|
||||
import { isEmptyObject, BridgeTransaction } from "@/utils";
|
||||
import { BridgeTransaction, TransactionStatus } from "@/types";
|
||||
import { getCompleteTxStoreKeyForTx } from "@/utils/history";
|
||||
|
||||
export type HistoryState = {
|
||||
isLoading: boolean;
|
||||
history: Record<
|
||||
string,
|
||||
{ transactions: BridgeTransaction[]; lastL1FetchedBlockNumber: bigint; lastL2FetchedBlockNumber: bigint }
|
||||
>;
|
||||
// history: Record<
|
||||
// string,
|
||||
// { transactions: BridgeTransaction[]; lastL1FetchedBlockNumber: bigint; lastL2FetchedBlockNumber: bigint }
|
||||
// >;
|
||||
completeTxHistory: Record<string, BridgeTransaction>;
|
||||
};
|
||||
|
||||
export type HistoryActions = {
|
||||
type HistoryActions = {
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
setTransactions: (
|
||||
key: string,
|
||||
transactions: BridgeTransaction[],
|
||||
lastL1FetchedBlockNumber?: bigint,
|
||||
lastL2FetchedBlockNumber?: bigint,
|
||||
) => void;
|
||||
getTransactionsByKey: (key: string) => BridgeTransaction[];
|
||||
getFromBlockNumbers: (key: string) => { l1FromBlock: bigint; l2FromBlock: bigint };
|
||||
// setTransactions: (
|
||||
// key: string,
|
||||
// transactions: BridgeTransaction[],
|
||||
// lastL1FetchedBlockNumber?: bigint,
|
||||
// lastL2FetchedBlockNumber?: bigint,
|
||||
// ) => void;
|
||||
setCompleteTx: (transaction: BridgeTransaction) => void;
|
||||
// getTransactionsByKey: (key: string) => BridgeTransaction[];
|
||||
// getFromBlockNumbers: (key: string) => { l1FromBlock: bigint; l2FromBlock: bigint };
|
||||
getCompleteTx: (key: string) => BridgeTransaction | undefined;
|
||||
};
|
||||
|
||||
export type HistoryActionsForCompleteTxCaching = Pick<HistoryActions, "setCompleteTx" | "getCompleteTx">;
|
||||
|
||||
export type HistoryStore = HistoryState & HistoryActions;
|
||||
|
||||
export const defaultInitState: HistoryState = {
|
||||
history: {},
|
||||
isLoading: false,
|
||||
// history: {},
|
||||
completeTxHistory: {},
|
||||
};
|
||||
|
||||
export const useHistoryStore = createWithEqualityFn<HistoryStore>()(
|
||||
@@ -36,36 +43,51 @@ export const useHistoryStore = createWithEqualityFn<HistoryStore>()(
|
||||
(set, get) => ({
|
||||
...defaultInitState,
|
||||
setIsLoading: (isLoading) => set({ isLoading }),
|
||||
setTransactions: (key, transactions, lastL1FetchedBlockNumber, lastL2FetchedBlockNumber) =>
|
||||
set((state) => ({
|
||||
history: {
|
||||
...state.history,
|
||||
[key]: {
|
||||
transactions,
|
||||
lastL1FetchedBlockNumber: lastL1FetchedBlockNumber || 0n,
|
||||
lastL2FetchedBlockNumber: lastL2FetchedBlockNumber || 0n,
|
||||
},
|
||||
},
|
||||
})),
|
||||
getTransactionsByKey: (key) => {
|
||||
const { history } = get();
|
||||
return history[key]?.transactions ?? [];
|
||||
},
|
||||
getFromBlockNumbers: (key) => {
|
||||
const { history } = get();
|
||||
|
||||
if (isEmptyObject(history) || !history[key]) {
|
||||
// setTransactions: (key, transactions, lastL1FetchedBlockNumber, lastL2FetchedBlockNumber) =>
|
||||
// set((state) => ({
|
||||
// history: {
|
||||
// ...state.history,
|
||||
// [key]: {
|
||||
// transactions,
|
||||
// lastL1FetchedBlockNumber: lastL1FetchedBlockNumber || 0n,
|
||||
// lastL2FetchedBlockNumber: lastL2FetchedBlockNumber || 0n,
|
||||
// },
|
||||
// },
|
||||
// })),
|
||||
setCompleteTx: (transaction) =>
|
||||
set((state) => {
|
||||
if (transaction.status !== TransactionStatus.COMPLETED) return state;
|
||||
const key = getCompleteTxStoreKeyForTx(transaction);
|
||||
return {
|
||||
l1FromBlock: 0n,
|
||||
l2FromBlock: 0n,
|
||||
completeTxHistory: {
|
||||
...state.completeTxHistory,
|
||||
[key]: transaction,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
l1FromBlock: history[key].lastL1FetchedBlockNumber,
|
||||
l2FromBlock: history[key].lastL2FetchedBlockNumber,
|
||||
};
|
||||
}),
|
||||
getCompleteTx: (key) => {
|
||||
const { completeTxHistory } = get();
|
||||
return completeTxHistory[key];
|
||||
},
|
||||
// getTransactionsByKey: (key) => {
|
||||
// const { history } = get();
|
||||
// return history[key]?.transactions ?? [];
|
||||
// },
|
||||
// getFromBlockNumbers: (key) => {
|
||||
// const { history } = get();
|
||||
|
||||
// if (isEmptyObject(history) || !history[key]) {
|
||||
// return {
|
||||
// l1FromBlock: 0n,
|
||||
// l2FromBlock: 0n,
|
||||
// };
|
||||
// }
|
||||
|
||||
// return {
|
||||
// l1FromBlock: history[key].lastL1FetchedBlockNumber,
|
||||
// l2FromBlock: history[key].lastL2FetchedBlockNumber,
|
||||
// };
|
||||
// },
|
||||
}),
|
||||
{
|
||||
name: "history-storage",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { useChainStore } from "./chainStore";
|
||||
export { FormStoreProvider, useFormStore } from "./formStoreProvider";
|
||||
export { type FormState } from "./formStore";
|
||||
export { useHistoryStore } from "./historyStore";
|
||||
export { useHistoryStore, type HistoryActionsForCompleteTxCaching } from "./historyStore";
|
||||
export { useNativeBridgeNavigationStore } from "./nativeBridgeNavigationStore";
|
||||
export { useTokenStore, TokenStoreProvider } from "./tokenStoreProvider";
|
||||
export { defaultTokensConfig } from "./tokenStore";
|
||||
|
||||
42
bridge-ui/src/types/bridge.ts
Normal file
42
bridge-ui/src/types/bridge.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Proof } from "@consensys/linea-sdk/dist/lib/sdk/merkleTree/types";
|
||||
import { Chain, Token, TransactionStatus } from "@/types";
|
||||
import { Address } from "viem";
|
||||
|
||||
export type NativeBridgeMessage = {
|
||||
from: Address;
|
||||
to: Address;
|
||||
fee: bigint;
|
||||
value: bigint;
|
||||
nonce: bigint;
|
||||
calldata: string;
|
||||
messageHash: string;
|
||||
proof?: Proof;
|
||||
amountSent: bigint;
|
||||
};
|
||||
|
||||
// Params expected for `receiveMessage` as per https://developers.circle.com/stablecoins/transfer-usdc-on-testnet-from-ethereum-to-avalanche
|
||||
export type CctpV2BridgeMessage = {
|
||||
message?: string;
|
||||
attestation?: string;
|
||||
amountSent: bigint;
|
||||
nonce: `0x${string}`;
|
||||
};
|
||||
|
||||
export enum BridgeTransactionType {
|
||||
ETH = "ETH",
|
||||
ERC20 = "ERC20",
|
||||
USDC = "USDC",
|
||||
}
|
||||
|
||||
// BridgeTransaction object that is populated when user opens "TransactionHistory" component, and is passed to child components.
|
||||
export interface BridgeTransaction {
|
||||
type: BridgeTransactionType;
|
||||
status: TransactionStatus;
|
||||
timestamp: bigint;
|
||||
fromChain: Chain;
|
||||
toChain: Chain;
|
||||
token: Token;
|
||||
message: NativeBridgeMessage | CctpV2BridgeMessage;
|
||||
bridgingTx: string;
|
||||
claimingTx?: string;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export type CctpAttestationMessageStatus = "pending_confirmations" | "complete";
|
||||
|
||||
type CctpAttestationMessage = {
|
||||
export enum CctpAttestationMessageStatus {
|
||||
PENDING_CONFIRMATIONS = "pending_confirmations",
|
||||
COMPLETE = "complete",
|
||||
}
|
||||
export type CctpAttestationMessage = {
|
||||
attestation: `0x${string}`;
|
||||
message: `0x${string}`;
|
||||
eventNonce: `0x${string}`;
|
||||
@@ -11,3 +13,12 @@ type CctpAttestationMessage = {
|
||||
export type CctpAttestationApiResponse = {
|
||||
messages: CctpAttestationMessage[];
|
||||
};
|
||||
|
||||
export type CctpV2ReattestationApiResponse = {
|
||||
message: string;
|
||||
nonce: `0x${string}`;
|
||||
};
|
||||
|
||||
export type CctpFeeApiResponse = {
|
||||
minimumFee: number;
|
||||
};
|
||||
|
||||
@@ -23,11 +23,13 @@ export type Chain = {
|
||||
apiUrl?: string | undefined;
|
||||
};
|
||||
};
|
||||
testnet?: boolean;
|
||||
testnet: boolean;
|
||||
layer: ChainLayer;
|
||||
messageServiceAddress: Address;
|
||||
tokenBridgeAddress: Address;
|
||||
gasLimitSurplus: bigint;
|
||||
profitMargin: bigint;
|
||||
cctpDomain: number;
|
||||
cctpTokenMessengerV2Address: Address;
|
||||
cctpMessageTransmitterV2Address: Address;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Address, Log } from "viem";
|
||||
import { Address, Log, AbiEvent } from "viem";
|
||||
|
||||
export type MessageSentEvent = Log & {
|
||||
export type MessageSentLogEvent = Log & {
|
||||
blockNumber: bigint;
|
||||
transactionHash: Address;
|
||||
args: {
|
||||
@@ -14,7 +14,7 @@ export type MessageSentEvent = Log & {
|
||||
};
|
||||
};
|
||||
|
||||
export type BridgingInitiatedV2Event = Log & {
|
||||
export type BridgingInitiatedV2LogEvent = Log & {
|
||||
blockNumber: bigint;
|
||||
transactionHash: Address;
|
||||
args: {
|
||||
@@ -25,7 +25,7 @@ export type BridgingInitiatedV2Event = Log & {
|
||||
};
|
||||
};
|
||||
|
||||
export type DepositForBurnEvent = Log & {
|
||||
export type DepositForBurnLogEvent = Log & {
|
||||
blockNumber: bigint;
|
||||
transactionHash: Address;
|
||||
args: {
|
||||
@@ -42,15 +42,205 @@ export type DepositForBurnEvent = Log & {
|
||||
};
|
||||
};
|
||||
|
||||
export type CCTPMessageReceivedEvent = Log & {
|
||||
blockNumber: bigint;
|
||||
transactionHash: Address;
|
||||
args: {
|
||||
caller: Address;
|
||||
sourceDomain: number;
|
||||
nonce: `0x${string}`;
|
||||
sender: `0x${string}`;
|
||||
finalityThresholdExecuted: number;
|
||||
messageBody: `0x${string}`;
|
||||
};
|
||||
export const CctpMessageReceivedAbiEvent: AbiEvent = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: "address", name: "caller", type: "address" },
|
||||
{ indexed: false, internalType: "uint32", name: "sourceDomain", type: "uint32" },
|
||||
{ indexed: true, internalType: "bytes32", name: "nonce", type: "bytes32" },
|
||||
{ indexed: false, internalType: "bytes32", name: "sender", type: "bytes32" },
|
||||
{ indexed: true, internalType: "uint32", name: "finalityThresholdExecuted", type: "uint32" },
|
||||
{ indexed: false, internalType: "bytes", name: "messageBody", type: "bytes" },
|
||||
],
|
||||
name: "MessageReceived",
|
||||
type: "event",
|
||||
};
|
||||
|
||||
export const BridgingInitiatedABIEvent: AbiEvent = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "sender",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "address",
|
||||
name: "recipient",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "token",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "uint256",
|
||||
name: "amount",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
name: "BridgingInitiated",
|
||||
type: "event",
|
||||
};
|
||||
|
||||
export const BridgingInitiatedV2ABIEvent: AbiEvent = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "sender",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "recipient",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "token",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "amount",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
name: "BridgingInitiatedV2",
|
||||
type: "event",
|
||||
};
|
||||
|
||||
export const MessageSentABIEvent: AbiEvent = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "_from",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "_to",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "_fee",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "_value",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "_nonce",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "bytes",
|
||||
name: "_calldata",
|
||||
type: "bytes",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "bytes32",
|
||||
name: "_messageHash",
|
||||
type: "bytes32",
|
||||
},
|
||||
],
|
||||
name: "MessageSent",
|
||||
type: "event",
|
||||
};
|
||||
|
||||
export const MessageClaimedABIEvent: AbiEvent = {
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: "bytes32", name: "_messageHash", type: "bytes32" }],
|
||||
name: "MessageClaimed",
|
||||
type: "event",
|
||||
};
|
||||
|
||||
export const CctpDepositForBurnAbiEvent: AbiEvent = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "maxFee",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "uint32",
|
||||
name: "minFinalityThreshold",
|
||||
type: "uint32",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "bytes",
|
||||
name: "hookData",
|
||||
type: "bytes",
|
||||
},
|
||||
],
|
||||
name: "DepositForBurn",
|
||||
type: "event",
|
||||
};
|
||||
|
||||
@@ -4,9 +4,26 @@ export { type TransactionType, TransactionStatus } from "./transaction";
|
||||
export { type Token, type GithubTokenListToken, type NetworkTokens } from "./token";
|
||||
export { BridgeProvider } from "./providers";
|
||||
export {
|
||||
type MessageSentEvent,
|
||||
type BridgingInitiatedV2Event,
|
||||
type DepositForBurnEvent,
|
||||
type CCTPMessageReceivedEvent,
|
||||
type MessageSentLogEvent,
|
||||
type BridgingInitiatedV2LogEvent,
|
||||
type DepositForBurnLogEvent,
|
||||
CctpMessageReceivedAbiEvent,
|
||||
BridgingInitiatedABIEvent,
|
||||
BridgingInitiatedV2ABIEvent,
|
||||
MessageSentABIEvent,
|
||||
MessageClaimedABIEvent,
|
||||
CctpDepositForBurnAbiEvent,
|
||||
} from "./events";
|
||||
export { type CctpAttestationApiResponse, type CctpAttestationMessageStatus } from "./cctp";
|
||||
export {
|
||||
type CctpAttestationApiResponse,
|
||||
type CctpAttestationMessage,
|
||||
type CctpV2ReattestationApiResponse,
|
||||
type CctpFeeApiResponse,
|
||||
CctpAttestationMessageStatus,
|
||||
} from "./cctp";
|
||||
export {
|
||||
type NativeBridgeMessage,
|
||||
type CctpV2BridgeMessage,
|
||||
type BridgeTransaction,
|
||||
BridgeTransactionType,
|
||||
} from "./bridge";
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import MessageTransmitterV2 from "@/abis/MessageTransmitterV2.json";
|
||||
import { CCTPMessageReceivedEvent, CctpAttestationMessageStatus, TransactionStatus } from "@/types";
|
||||
import { CctpAttestationMessage, Chain, TransactionStatus, CctpAttestationMessageStatus } from "@/types";
|
||||
import { GetPublicClientReturnType } from "@wagmi/core";
|
||||
import { getAddress } from "viem";
|
||||
import { eventCCTPMessageReceived } from "./events";
|
||||
import { fetchCctpAttestationByTxHash, reattestCctpV2PreFinalityMessage } from "@/services/cctp";
|
||||
import { getPublicClient } from "@wagmi/core";
|
||||
import { config as wagmiConfig } from "@/lib/wagmi";
|
||||
|
||||
// Contract for sending CCTP message, appears constant for each chain
|
||||
export const CCTP_TOKEN_MESSENGER = getAddress("0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA");
|
||||
// TODO - Find optimal value
|
||||
export const CCTP_TRANSFER_MAX_FEE = 500n;
|
||||
export const CCTP_TRANSFER_MAX_FEE_FALLBACK = 5n;
|
||||
// 1000 Fast transfer, 2000 Standard transfer
|
||||
export const CCTP_MIN_FINALITY_THRESHOLD = 1000;
|
||||
// Contract for receiving CCTP message, appears constant for each chain
|
||||
export const CCTP_MESSAGE_TRANSMITTER = getAddress("0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"); // Appears constant for each chain
|
||||
|
||||
export const isCCTPNonceUsed = async (client: GetPublicClientReturnType, nonce: string): Promise<boolean> => {
|
||||
const isCctpNonceUsed = async (
|
||||
client: GetPublicClientReturnType,
|
||||
nonce: string,
|
||||
cctpMessageTransmitterV2Address: `0x${string}`,
|
||||
): Promise<boolean> => {
|
||||
const resp = await client?.readContract({
|
||||
address: CCTP_MESSAGE_TRANSMITTER,
|
||||
address: cctpMessageTransmitterV2Address,
|
||||
abi: MessageTransmitterV2.abi,
|
||||
functionName: "usedNonces",
|
||||
args: [nonce],
|
||||
@@ -24,35 +25,62 @@ export const isCCTPNonceUsed = async (client: GetPublicClientReturnType, nonce:
|
||||
return resp === 1n;
|
||||
};
|
||||
|
||||
export const getCCTPTransactionStatus = (
|
||||
cctpMessageStatus: CctpAttestationMessageStatus,
|
||||
isNonceUsed: boolean,
|
||||
): TransactionStatus => {
|
||||
if (cctpMessageStatus === "pending_confirmations") return TransactionStatus.PENDING;
|
||||
if (!isNonceUsed) return TransactionStatus.READY_TO_CLAIM;
|
||||
return TransactionStatus.COMPLETED;
|
||||
const getCctpMessageExpiryBlock = (message: string): bigint => {
|
||||
// See CCTPV2 message format at https://developers.circle.com/stablecoins/message-format
|
||||
const expiryInHex = message.substring(2 + 344 * 2, 2 + 376 * 2);
|
||||
// Return bigint because this is also returned by Viem client.getBlockNumber()
|
||||
return BigInt(parseInt(expiryInHex, 16));
|
||||
};
|
||||
|
||||
export const getCCTPClaimTx = async (
|
||||
client: GetPublicClientReturnType,
|
||||
status: CctpAttestationMessageStatus,
|
||||
isNonceUsed: boolean,
|
||||
nonce: `0x${string}`,
|
||||
): Promise<string | undefined> => {
|
||||
if (!client) return undefined;
|
||||
if (!isNonceUsed) return undefined;
|
||||
|
||||
const messageReceivedEvents = <CCTPMessageReceivedEvent[]>await client.getLogs({
|
||||
event: eventCCTPMessageReceived,
|
||||
// TODO - Find more efficient `fromBlock` param than 'earliest'
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: CCTP_MESSAGE_TRANSMITTER,
|
||||
args: {
|
||||
nonce: nonce,
|
||||
},
|
||||
export const getCctpTransactionStatus = async (
|
||||
toChain: Chain,
|
||||
cctpAttestationMessage: CctpAttestationMessage,
|
||||
nonce: string,
|
||||
): Promise<TransactionStatus> => {
|
||||
const toChainClient = getPublicClient(wagmiConfig, {
|
||||
chainId: toChain.id,
|
||||
});
|
||||
if (!toChainClient) return TransactionStatus.PENDING;
|
||||
const isNonceUsed = await isCctpNonceUsed(toChainClient, nonce, toChain.cctpMessageTransmitterV2Address);
|
||||
if (isNonceUsed) return TransactionStatus.COMPLETED;
|
||||
const messageExpiryBlock = getCctpMessageExpiryBlock(cctpAttestationMessage.message);
|
||||
// Message has no expiry
|
||||
if (messageExpiryBlock === 0n)
|
||||
return cctpAttestationMessage.status === CctpAttestationMessageStatus.PENDING_CONFIRMATIONS
|
||||
? TransactionStatus.PENDING
|
||||
: TransactionStatus.READY_TO_CLAIM;
|
||||
|
||||
if (messageReceivedEvents.length === 0) return undefined;
|
||||
return messageReceivedEvents[0].transactionHash;
|
||||
// Message not expired
|
||||
const currentToBlock = await toChainClient.getBlockNumber();
|
||||
if (currentToBlock < messageExpiryBlock)
|
||||
return cctpAttestationMessage.status === CctpAttestationMessageStatus.PENDING_CONFIRMATIONS
|
||||
? TransactionStatus.PENDING
|
||||
: TransactionStatus.READY_TO_CLAIM;
|
||||
|
||||
// Message has expired, must reattest
|
||||
await reattestCctpV2PreFinalityMessage(nonce, toChain.testnet);
|
||||
|
||||
/**
|
||||
* We will not re-query to get a new message/attestation set here:
|
||||
*
|
||||
* 1.) There is a concrete possibility that the new message status will be 'pending_confirmations', which will be a regression from the old message status of 'completed'
|
||||
* - To avoid this edge case, we will simply deem the transaction status as 'TransactionStatus.PENDING'
|
||||
* - TransactionStatus.PENDING means the user will not be able to claim, hence they have no need for a valid message/attestation set for this Transaction
|
||||
*
|
||||
* 2.) We avoid a CCTP API call that has a concrete possibility of returning a result consistent with TransactionStatus.PENDING anyway
|
||||
* - Even in the case that the new message status is 'complete', the only cost to the user is a page refresh
|
||||
*/
|
||||
return TransactionStatus.PENDING;
|
||||
};
|
||||
|
||||
export const getCctpMessageByTxHash = async (
|
||||
transactionHash: string,
|
||||
fromChainCctpDomain: number,
|
||||
isTestnet: boolean,
|
||||
): Promise<CctpAttestationMessage | undefined> => {
|
||||
const attestationApiResp = await fetchCctpAttestationByTxHash(fromChainCctpDomain, transactionHash, isTestnet);
|
||||
if (!attestationApiResp) return;
|
||||
const message = attestationApiResp.messages[0];
|
||||
if (!message) return;
|
||||
return message;
|
||||
};
|
||||
|
||||
@@ -11,13 +11,16 @@ export const generateChain = (chain: ViemChain): Chain => {
|
||||
iconPath: config.chains[chain.id].iconPath,
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
blockExplorers: chain.blockExplorers,
|
||||
testnet: chain.testnet,
|
||||
// Possibly the wrong assumption to fallback to 'false', but fallback to 'true' makes the app crash mysteriously
|
||||
testnet: Boolean(chain.testnet),
|
||||
layer: getChainNetworkLayer(chain.id),
|
||||
messageServiceAddress: config.chains[chain.id].messageServiceAddress as Address,
|
||||
tokenBridgeAddress: config.chains[chain.id].tokenBridgeAddress as Address,
|
||||
gasLimitSurplus: config.chains[chain.id].gasLimitSurplus,
|
||||
profitMargin: config.chains[chain.id].profitMargin,
|
||||
cctpDomain: config.chains[chain.id].cctpDomain,
|
||||
cctpTokenMessengerV2Address: config.chains[chain.id].cctpTokenMessengerV2Address as Address,
|
||||
cctpMessageTransmitterV2Address: config.chains[chain.id].cctpMessageTransmitterV2Address as Address,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -41,7 +44,7 @@ export const getChainNetworkLayer = (chainId: number) => {
|
||||
export const getChainLogoPath = (chainId: number) => {
|
||||
switch (chainId) {
|
||||
case linea.id:
|
||||
return "/images/logo/linea-mainnet.svg";
|
||||
return "/images/logo/linea-rounded.svg";
|
||||
case lineaSepolia.id:
|
||||
return "/images/logo/linea-sepolia.svg";
|
||||
case mainnet.id:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const eventCCTPMessageReceived = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: "address", name: "caller", type: "address" },
|
||||
{ indexed: false, internalType: "uint32", name: "sourceDomain", type: "uint32" },
|
||||
{ indexed: true, internalType: "bytes32", name: "nonce", type: "bytes32" },
|
||||
{ indexed: false, internalType: "bytes32", name: "sender", type: "bytes32" },
|
||||
{ indexed: true, internalType: "uint32", name: "finalityThresholdExecuted", type: "uint32" },
|
||||
{ indexed: false, internalType: "bytes", name: "messageBody", type: "bytes" },
|
||||
],
|
||||
name: "MessageReceived",
|
||||
type: "event",
|
||||
} as const;
|
||||
|
||||
export default eventCCTPMessageReceived;
|
||||
@@ -1,33 +0,0 @@
|
||||
const eventERC20 = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "sender",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "address",
|
||||
name: "recipient",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "token",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "uint256",
|
||||
name: "amount",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
name: "BridgingInitiated",
|
||||
type: "event",
|
||||
} as const;
|
||||
|
||||
export default eventERC20;
|
||||
@@ -1,33 +0,0 @@
|
||||
const eventERC20V2 = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "sender",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "recipient",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "token",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "amount",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
name: "BridgingInitiatedV2",
|
||||
type: "event",
|
||||
} as const;
|
||||
|
||||
export default eventERC20V2;
|
||||
@@ -1,51 +0,0 @@
|
||||
const eventETH = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "_from",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "address",
|
||||
name: "_to",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "_fee",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "_value",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "_nonce",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "bytes",
|
||||
name: "_calldata",
|
||||
type: "bytes",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "bytes32",
|
||||
name: "_messageHash",
|
||||
type: "bytes32",
|
||||
},
|
||||
],
|
||||
name: "MessageSent",
|
||||
type: "event",
|
||||
} as const;
|
||||
|
||||
export default eventETH;
|
||||
@@ -1,69 +0,0 @@
|
||||
const eventUSDC = {
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
name: "maxFee",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: "uint32",
|
||||
name: "minFinalityThreshold",
|
||||
type: "uint32",
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: "bytes",
|
||||
name: "hookData",
|
||||
type: "bytes",
|
||||
},
|
||||
],
|
||||
name: "DepositForBurn",
|
||||
type: "event",
|
||||
} as const;
|
||||
|
||||
export default eventUSDC;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PublicClient } from "viem";
|
||||
import { MessageClaimedABIEvent } from "@/types";
|
||||
|
||||
export const getNativeBridgeMessageClaimedTxHash = async (
|
||||
chainClient: PublicClient,
|
||||
messageServiceAddress: `0x${string}`,
|
||||
messageHash: `0x${string}`,
|
||||
): Promise<string> => {
|
||||
const messageClaimedEvents = await chainClient.getLogs({
|
||||
event: MessageClaimedABIEvent,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: messageServiceAddress,
|
||||
args: {
|
||||
_messageHash: messageHash,
|
||||
},
|
||||
});
|
||||
if (messageClaimedEvents.length === 0) return "";
|
||||
return messageClaimedEvents[0].transactionHash;
|
||||
};
|
||||
@@ -1,5 +1 @@
|
||||
export { default as eventERC20 } from "./eventERC20";
|
||||
export { default as eventERC20V2 } from "./eventERC20V2";
|
||||
export { default as eventETH } from "./eventETH";
|
||||
export { default as eventUSDC } from "./eventUSDC";
|
||||
export { default as eventCCTPMessageReceived } from "./eventCCTPMessageReceived";
|
||||
export { getNativeBridgeMessageClaimedTxHash } from "./getNativeBridgeMessageClaimedTxHash";
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
import { Address, decodeAbiParameters } from "viem";
|
||||
import { compareAsc, fromUnixTime, subDays } from "date-fns";
|
||||
import { getPublicClient } from "@wagmi/core";
|
||||
import { LineaSDK, OnChainMessageStatus } from "@consensys/linea-sdk";
|
||||
import { config as wagmiConfig } from "@/lib/wagmi";
|
||||
import { config } from "@/config";
|
||||
import { Proof } from "@consensys/linea-sdk/dist/lib/sdk/merkleTree/types";
|
||||
import { defaultTokensConfig } from "@/stores";
|
||||
import { LineaSDKContracts } from "@/hooks";
|
||||
import { Chain, ChainLayer, Token, TransactionStatus, BridgingInitiatedV2Event, MessageSentEvent } from "@/types";
|
||||
import {
|
||||
CCTP_TOKEN_MESSENGER,
|
||||
eventETH,
|
||||
eventERC20V2,
|
||||
eventUSDC,
|
||||
getCCTPClaimTx,
|
||||
isCCTPNonceUsed,
|
||||
getCCTPTransactionStatus,
|
||||
} from "@/utils";
|
||||
import { DepositForBurnEvent } from "@/types/events";
|
||||
import { fetchCctpAttestation } from "@/services/cctp";
|
||||
|
||||
type TransactionHistoryParams = {
|
||||
lineaSDK: LineaSDK;
|
||||
lineaSDKContracts: LineaSDKContracts;
|
||||
fromChain: Chain;
|
||||
toChain: Chain;
|
||||
address: Address;
|
||||
tokens: Token[];
|
||||
};
|
||||
|
||||
export type NativeBridgeMessage = {
|
||||
from: Address;
|
||||
to: Address;
|
||||
fee: bigint;
|
||||
value: bigint;
|
||||
nonce: bigint;
|
||||
calldata: string;
|
||||
messageHash: string;
|
||||
proof?: Proof;
|
||||
amountSent: bigint;
|
||||
};
|
||||
|
||||
// Params expected for `receiveMessage` as per https://developers.circle.com/stablecoins/transfer-usdc-on-testnet-from-ethereum-to-avalanche
|
||||
export type CCTPV2BridgeMessage = {
|
||||
message: string;
|
||||
attestation: string;
|
||||
amountSent: bigint;
|
||||
};
|
||||
export interface BridgeTransaction {
|
||||
type: "ETH" | "ERC20";
|
||||
status: TransactionStatus;
|
||||
timestamp: bigint;
|
||||
fromChain: Chain;
|
||||
toChain: Chain;
|
||||
token: Token;
|
||||
message: NativeBridgeMessage | CCTPV2BridgeMessage;
|
||||
bridgingTx: string;
|
||||
claimingTx?: string;
|
||||
}
|
||||
|
||||
export async function fetchTransactionsHistory({
|
||||
lineaSDK,
|
||||
lineaSDKContracts,
|
||||
fromChain,
|
||||
toChain,
|
||||
address,
|
||||
tokens,
|
||||
}: TransactionHistoryParams): Promise<BridgeTransaction[]> {
|
||||
const events = await Promise.all([
|
||||
fetchBridgeEvents(lineaSDK, lineaSDKContracts, fromChain, toChain, address, tokens),
|
||||
fetchBridgeEvents(lineaSDK, lineaSDKContracts, toChain, fromChain, address, tokens),
|
||||
]);
|
||||
return events.flat().sort((a, b) => Number(b.timestamp.toString()) - Number(a.timestamp.toString()));
|
||||
}
|
||||
|
||||
async function fetchBridgeEvents(
|
||||
lineaSDK: LineaSDK,
|
||||
lineaSDKContracts: LineaSDKContracts,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
address: Address,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const [ethEvents, erc20Events, cctpEvents] = await Promise.all([
|
||||
fetchETHBridgeEvents(lineaSDK, lineaSDKContracts, address, fromChain, toChain, tokens),
|
||||
fetchERC20BridgeEvents(lineaSDK, lineaSDKContracts, address, fromChain, toChain, tokens),
|
||||
// Feature toggle for CCTP, will filter out USDC transactions if isCCTPEnabled == false
|
||||
config.isCCTPEnabled ? fetchCCTPBridgeEvents(address, fromChain, toChain, tokens) : [],
|
||||
]);
|
||||
|
||||
return [...ethEvents, ...erc20Events, ...cctpEvents];
|
||||
}
|
||||
|
||||
async function fetchETHBridgeEvents(
|
||||
lineaSDK: LineaSDK,
|
||||
lineaSDKContracts: LineaSDKContracts,
|
||||
address: Address,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const transactionsMap = new Map<string, BridgeTransaction>();
|
||||
|
||||
const client = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain.id,
|
||||
});
|
||||
|
||||
const contract = fromChain.layer === ChainLayer.L1 ? lineaSDK.getL2Contract() : lineaSDK.getL1Contract();
|
||||
|
||||
const messageServiceAddress = fromChain.messageServiceAddress;
|
||||
const [ethLogsForSender, ethLogsForRecipient] = await Promise.all([<Promise<MessageSentEvent[]>>client.getLogs({
|
||||
event: eventETH,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: messageServiceAddress,
|
||||
args: {
|
||||
_from: address,
|
||||
},
|
||||
}), <Promise<MessageSentEvent[]>>client.getLogs({
|
||||
event: eventETH,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: messageServiceAddress,
|
||||
args: {
|
||||
_to: address,
|
||||
},
|
||||
})]);
|
||||
|
||||
const messageSentLogs = [...ethLogsForSender, ...ethLogsForRecipient];
|
||||
|
||||
const uniqueLogsMap = new Map<string, (typeof messageSentLogs)[0]>();
|
||||
for (const log of messageSentLogs) {
|
||||
const uniqueKey = `${log.args._from}-${log.args._to}-${log.transactionHash}`;
|
||||
if (!uniqueLogsMap.has(uniqueKey)) {
|
||||
uniqueLogsMap.set(uniqueKey, log);
|
||||
}
|
||||
}
|
||||
|
||||
const currentTimestamp = new Date();
|
||||
|
||||
await Promise.all(
|
||||
Array.from(uniqueLogsMap.values()).map(async (log) => {
|
||||
const messageHash = log.args._messageHash;
|
||||
|
||||
const block = await client.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
|
||||
|
||||
if (compareAsc(fromUnixTime(Number(block.timestamp.toString())), subDays(currentTimestamp, 90)) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [messageStatus, [messageClaimedEvent]] = await Promise.all([
|
||||
contract.getMessageStatus(messageHash),
|
||||
contract.getEvents(lineaSDKContracts[toChain.layer].contract.filters.MessageClaimed(messageHash)),
|
||||
]);
|
||||
|
||||
const messageProof =
|
||||
toChain.layer === ChainLayer.L1 && messageStatus === OnChainMessageStatus.CLAIMABLE
|
||||
? await lineaSDK.getL1ClaimingService().getMessageProof(messageHash)
|
||||
: undefined;
|
||||
|
||||
const token = tokens.find((token) => token.type.includes("eth"));
|
||||
const uniqueKey = `${log.args._from}-${log.args._to}-${log.transactionHash}`;
|
||||
transactionsMap.set(uniqueKey, {
|
||||
type: "ETH",
|
||||
status: formatStatus(messageStatus),
|
||||
token: token || defaultTokensConfig.MAINNET[0],
|
||||
fromChain,
|
||||
toChain,
|
||||
timestamp: block.timestamp,
|
||||
bridgingTx: log.transactionHash,
|
||||
claimingTx: messageClaimedEvent?.transactionHash,
|
||||
message: {
|
||||
from: log.args._from,
|
||||
to: log.args._to,
|
||||
fee: log.args._fee,
|
||||
value: log.args._value,
|
||||
nonce: log.args._nonce,
|
||||
calldata: log.args._calldata,
|
||||
messageHash: log.args._messageHash,
|
||||
proof: messageProof,
|
||||
amountSent: log.args._value,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(transactionsMap.values());
|
||||
}
|
||||
|
||||
async function fetchERC20BridgeEvents(
|
||||
lineaSDK: LineaSDK,
|
||||
lineaSDKContracts: LineaSDKContracts,
|
||||
address: Address,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const transactionsMap = new Map<string, BridgeTransaction>();
|
||||
|
||||
const client = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain.id,
|
||||
});
|
||||
|
||||
const [originContract, destinationContract] =
|
||||
fromChain.layer === ChainLayer.L1
|
||||
? [lineaSDK.getL1Contract(), lineaSDK.getL2Contract()]
|
||||
: [lineaSDK.getL2Contract(), lineaSDK.getL1Contract()];
|
||||
|
||||
const tokenBridgeAddress = fromChain.tokenBridgeAddress;
|
||||
const [erc20LogsForSender, erc20LogsForRecipient] = await Promise.all([
|
||||
<Promise<BridgingInitiatedV2Event[]>>client.getLogs({
|
||||
event: eventERC20V2,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: tokenBridgeAddress,
|
||||
args: {
|
||||
sender: address,
|
||||
},
|
||||
}),
|
||||
<Promise<BridgingInitiatedV2Event[]>>client.getLogs({
|
||||
event: eventERC20V2,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: tokenBridgeAddress,
|
||||
args: {
|
||||
recipient: address,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const erc20Logs = [...erc20LogsForSender, ...erc20LogsForRecipient];
|
||||
|
||||
const uniqueLogsMap = new Map<string, (typeof erc20Logs)[0]>();
|
||||
for (const log of erc20Logs) {
|
||||
const transactionHash = log.transactionHash;
|
||||
if (!uniqueLogsMap.has(transactionHash)) {
|
||||
uniqueLogsMap.set(transactionHash, log);
|
||||
}
|
||||
}
|
||||
|
||||
const currentTimestamp = new Date();
|
||||
|
||||
await Promise.all(
|
||||
Array.from(uniqueLogsMap.values()).map(async (log) => {
|
||||
const transactionHash = log.transactionHash;
|
||||
|
||||
const block = await client.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
|
||||
|
||||
if (compareAsc(fromUnixTime(Number(block.timestamp.toString())), subDays(currentTimestamp, 90)) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await originContract.getMessagesByTransactionHash(transactionHash);
|
||||
if (!message || message.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [messageStatus, [messageClaimedEvent]] = await Promise.all([
|
||||
destinationContract.getMessageStatus(message[0].messageHash),
|
||||
destinationContract.getEvents(
|
||||
lineaSDKContracts[toChain.layer].contract.filters.MessageClaimed(message[0].messageHash),
|
||||
),
|
||||
]);
|
||||
|
||||
const messageProof =
|
||||
toChain.layer === ChainLayer.L1 && messageStatus === OnChainMessageStatus.CLAIMABLE
|
||||
? await lineaSDK.getL1ClaimingService().getMessageProof(message[0].messageHash)
|
||||
: undefined;
|
||||
|
||||
const token = tokens.find(
|
||||
(token) =>
|
||||
token.L1?.toLowerCase() === log.args.token.toLowerCase() ||
|
||||
token.L2?.toLowerCase() === log.args.token.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [amount] = decodeAbiParameters([{ type: "uint256", name: "amount" }], log.data);
|
||||
|
||||
transactionsMap.set(transactionHash, {
|
||||
type: "ERC20",
|
||||
status: formatStatus(messageStatus),
|
||||
token,
|
||||
fromChain,
|
||||
toChain,
|
||||
timestamp: block.timestamp,
|
||||
bridgingTx: log.transactionHash,
|
||||
claimingTx: messageClaimedEvent?.transactionHash,
|
||||
message: {
|
||||
from: message[0].messageSender as Address,
|
||||
to: message[0].destination as Address,
|
||||
fee: message[0].fee,
|
||||
value: message[0].value,
|
||||
nonce: message[0].messageNonce,
|
||||
calldata: message[0].calldata,
|
||||
messageHash: message[0].messageHash,
|
||||
proof: messageProof,
|
||||
amountSent: amount,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(transactionsMap.values());
|
||||
}
|
||||
|
||||
async function fetchCCTPBridgeEvents(
|
||||
address: Address,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const transactionsMap = new Map<string, BridgeTransaction>();
|
||||
const fromChainClient = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain.id,
|
||||
});
|
||||
const toChainClient = getPublicClient(wagmiConfig, {
|
||||
chainId: toChain.id,
|
||||
});
|
||||
|
||||
const usdcLogs = <DepositForBurnEvent[]>await fromChainClient.getLogs({
|
||||
event: eventUSDC,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: CCTP_TOKEN_MESSENGER,
|
||||
args: {
|
||||
depositor: address,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO - Consider deduplication
|
||||
|
||||
const currentTimestamp = new Date();
|
||||
|
||||
await Promise.all(
|
||||
usdcLogs.map(async (log) => {
|
||||
const transactionHash = log.transactionHash;
|
||||
|
||||
const block = await fromChainClient.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
|
||||
|
||||
if (compareAsc(fromUnixTime(Number(block.timestamp.toString())), subDays(currentTimestamp, 90)) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = tokens.find((token) => token.symbol === "USDC" && token.type.includes("bridge-reserved"));
|
||||
if (!token) return;
|
||||
|
||||
const attestationApiResp = await fetchCctpAttestation(transactionHash, fromChain.cctpDomain);
|
||||
if (!attestationApiResp) return;
|
||||
// console.log("attestationApiResp:", attestationApiResp, "transactionHash:", transactionHash);
|
||||
|
||||
const message = attestationApiResp.messages[0];
|
||||
if (!message) return;
|
||||
|
||||
const nonce = message.eventNonce;
|
||||
// console.log("messageNonce:", nonce);
|
||||
|
||||
const isNonceUsed = await isCCTPNonceUsed(toChainClient, nonce);
|
||||
// console.log("isNonceUsed:", isNonceUsed);
|
||||
|
||||
const status = getCCTPTransactionStatus(message.status, isNonceUsed);
|
||||
// console.log("status:", status);
|
||||
|
||||
const claimTx = await getCCTPClaimTx(toChainClient, message.status, isNonceUsed, nonce);
|
||||
// console.log("claimTx:", claimTx);
|
||||
|
||||
transactionsMap.set(transactionHash, {
|
||||
type: "ERC20",
|
||||
status,
|
||||
token,
|
||||
fromChain,
|
||||
toChain,
|
||||
timestamp: block.timestamp,
|
||||
bridgingTx: log.transactionHash,
|
||||
claimingTx: claimTx,
|
||||
message: {
|
||||
attestation: message.attestation,
|
||||
message: message.message,
|
||||
amountSent: BigInt(log.args.amount),
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(transactionsMap.values());
|
||||
}
|
||||
|
||||
function formatStatus(status: OnChainMessageStatus): TransactionStatus {
|
||||
switch (status) {
|
||||
case OnChainMessageStatus.UNKNOWN:
|
||||
return TransactionStatus.PENDING;
|
||||
case OnChainMessageStatus.CLAIMABLE:
|
||||
return TransactionStatus.READY_TO_CLAIM;
|
||||
case OnChainMessageStatus.CLAIMED:
|
||||
return TransactionStatus.COMPLETED;
|
||||
default:
|
||||
return TransactionStatus.PENDING;
|
||||
}
|
||||
}
|
||||
79
bridge-ui/src/utils/history/fetchCctpBridgeEvents.ts
Normal file
79
bridge-ui/src/utils/history/fetchCctpBridgeEvents.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Address } from "viem";
|
||||
import { getPublicClient } from "@wagmi/core";
|
||||
import { config as wagmiConfig } from "@/lib/wagmi";
|
||||
import { BridgeTransaction, BridgeTransactionType, Chain, Token, CctpDepositForBurnAbiEvent } from "@/types";
|
||||
import { isCctp, getCctpMessageByTxHash, getCctpTransactionStatus } from "@/utils";
|
||||
import { DepositForBurnLogEvent } from "@/types/events";
|
||||
import { HistoryActionsForCompleteTxCaching } from "@/stores";
|
||||
import { getCompleteTxStoreKey } from "./getCompleteTxStoreKey";
|
||||
import { isBlockTooOld } from "./isBlockTooOld";
|
||||
|
||||
export async function fetchCctpBridgeEvents(
|
||||
historyStoreActions: HistoryActionsForCompleteTxCaching,
|
||||
address: Address,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const transactionsMap = new Map<string, BridgeTransaction>();
|
||||
const fromChainClient = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain.id,
|
||||
});
|
||||
|
||||
const usdcLogs = (await fromChainClient.getLogs({
|
||||
event: CctpDepositForBurnAbiEvent,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: fromChain.cctpTokenMessengerV2Address,
|
||||
args: {
|
||||
depositor: address,
|
||||
},
|
||||
})) as unknown as DepositForBurnLogEvent[];
|
||||
|
||||
await Promise.all(
|
||||
usdcLogs.map(async (log) => {
|
||||
const transactionHash = log.transactionHash;
|
||||
|
||||
// Search cache for completed tx for this txHash, if cache-hit can skip remaining logic
|
||||
const cacheKey = getCompleteTxStoreKey(fromChain.id, transactionHash);
|
||||
const cachedCompletedTx = historyStoreActions.getCompleteTx(cacheKey);
|
||||
if (cachedCompletedTx) {
|
||||
transactionsMap.set(transactionHash, cachedCompletedTx);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromBlock = await fromChainClient.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
|
||||
if (isBlockTooOld(fromBlock)) return;
|
||||
|
||||
const token = tokens.find((token) => isCctp(token));
|
||||
if (!token) return;
|
||||
|
||||
const cctpMessage = await getCctpMessageByTxHash(transactionHash, fromChain.cctpDomain, fromChain.testnet);
|
||||
if (!cctpMessage) return;
|
||||
const nonce = cctpMessage.eventNonce;
|
||||
const status = await getCctpTransactionStatus(toChain, cctpMessage, nonce);
|
||||
|
||||
const tx: BridgeTransaction = {
|
||||
type: BridgeTransactionType.USDC,
|
||||
status,
|
||||
token,
|
||||
fromChain,
|
||||
toChain,
|
||||
timestamp: fromBlock.timestamp,
|
||||
bridgingTx: log.transactionHash,
|
||||
message: {
|
||||
amountSent: BigInt(log.args.amount),
|
||||
nonce: nonce,
|
||||
attestation: cctpMessage.attestation,
|
||||
message: cctpMessage.message,
|
||||
},
|
||||
};
|
||||
|
||||
// Store COMPLETE tx in cache
|
||||
historyStoreActions.setCompleteTx(tx);
|
||||
transactionsMap.set(transactionHash, tx);
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(transactionsMap.values());
|
||||
}
|
||||
130
bridge-ui/src/utils/history/fetchERC20BridgeEvents.ts
Normal file
130
bridge-ui/src/utils/history/fetchERC20BridgeEvents.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Address, decodeAbiParameters } from "viem";
|
||||
import { getPublicClient } from "@wagmi/core";
|
||||
import { LineaSDK } from "@consensys/linea-sdk";
|
||||
import { config as wagmiConfig } from "@/lib/wagmi";
|
||||
import {
|
||||
BridgeTransaction,
|
||||
BridgeTransactionType,
|
||||
Chain,
|
||||
ChainLayer,
|
||||
Token,
|
||||
BridgingInitiatedV2LogEvent,
|
||||
BridgingInitiatedV2ABIEvent,
|
||||
} from "@/types";
|
||||
import { formatOnChainMessageStatus } from "./formatOnChainMessageStatus";
|
||||
import { HistoryActionsForCompleteTxCaching } from "@/stores";
|
||||
import { getCompleteTxStoreKey } from "./getCompleteTxStoreKey";
|
||||
import { isBlockTooOld } from "./isBlockTooOld";
|
||||
|
||||
export async function fetchERC20BridgeEvents(
|
||||
historyStoreActions: HistoryActionsForCompleteTxCaching,
|
||||
lineaSDK: LineaSDK,
|
||||
address: Address,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const transactionsMap = new Map<string, BridgeTransaction>();
|
||||
|
||||
const client = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain.id,
|
||||
});
|
||||
|
||||
const [originContract, destinationContract] =
|
||||
fromChain.layer === ChainLayer.L1
|
||||
? [lineaSDK.getL1Contract(), lineaSDK.getL2Contract()]
|
||||
: [lineaSDK.getL2Contract(), lineaSDK.getL1Contract()];
|
||||
|
||||
const tokenBridgeAddress = fromChain.tokenBridgeAddress;
|
||||
const [erc20LogsForSender, erc20LogsForRecipient] = await Promise.all([
|
||||
client.getLogs({
|
||||
event: BridgingInitiatedV2ABIEvent,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: tokenBridgeAddress,
|
||||
args: {
|
||||
sender: address,
|
||||
},
|
||||
}),
|
||||
client.getLogs({
|
||||
event: BridgingInitiatedV2ABIEvent,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: tokenBridgeAddress,
|
||||
args: {
|
||||
recipient: address,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const erc20Logs = [...erc20LogsForSender, ...erc20LogsForRecipient] as unknown as BridgingInitiatedV2LogEvent[];
|
||||
|
||||
const uniqueLogsMap = new Map<string, (typeof erc20Logs)[0]>();
|
||||
for (const log of erc20Logs) {
|
||||
const transactionHash = log.transactionHash;
|
||||
if (!uniqueLogsMap.has(transactionHash)) {
|
||||
uniqueLogsMap.set(transactionHash, log);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Array.from(uniqueLogsMap.values()).map(async (log) => {
|
||||
const transactionHash = log.transactionHash;
|
||||
|
||||
// Search cache for completed tx for this txHash, if cache-hit can skip remaining logic
|
||||
const cacheKey = getCompleteTxStoreKey(fromChain.id, transactionHash);
|
||||
const cachedCompletedTx = historyStoreActions.getCompleteTx(cacheKey);
|
||||
if (cachedCompletedTx) {
|
||||
transactionsMap.set(transactionHash, cachedCompletedTx);
|
||||
return;
|
||||
}
|
||||
|
||||
const block = await client.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
|
||||
if (isBlockTooOld(block)) return;
|
||||
|
||||
const message = await originContract.getMessagesByTransactionHash(transactionHash);
|
||||
if (!message || message.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageStatus = await destinationContract.getMessageStatus(message[0].messageHash);
|
||||
|
||||
const token = tokens.find(
|
||||
(token) =>
|
||||
token.L1?.toLowerCase() === log.args.token.toLowerCase() ||
|
||||
token.L2?.toLowerCase() === log.args.token.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [amount] = decodeAbiParameters([{ type: "uint256", name: "amount" }], log.data);
|
||||
const tx = {
|
||||
type: BridgeTransactionType.ERC20,
|
||||
status: formatOnChainMessageStatus(messageStatus),
|
||||
token,
|
||||
fromChain,
|
||||
toChain,
|
||||
timestamp: block.timestamp,
|
||||
bridgingTx: log.transactionHash,
|
||||
message: {
|
||||
from: message[0].messageSender as Address,
|
||||
to: message[0].destination as Address,
|
||||
fee: message[0].fee,
|
||||
value: message[0].value,
|
||||
nonce: message[0].messageNonce,
|
||||
calldata: message[0].calldata,
|
||||
messageHash: message[0].messageHash,
|
||||
amountSent: amount,
|
||||
},
|
||||
};
|
||||
|
||||
// Store COMPLETE tx in cache
|
||||
historyStoreActions.setCompleteTx(tx);
|
||||
transactionsMap.set(transactionHash, tx);
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(transactionsMap.values());
|
||||
}
|
||||
117
bridge-ui/src/utils/history/fetchETHBridgeEvents.ts
Normal file
117
bridge-ui/src/utils/history/fetchETHBridgeEvents.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Address } from "viem";
|
||||
import { getPublicClient } from "@wagmi/core";
|
||||
import { LineaSDK } from "@consensys/linea-sdk";
|
||||
import { config as wagmiConfig } from "@/lib/wagmi";
|
||||
import { defaultTokensConfig, HistoryActionsForCompleteTxCaching } from "@/stores";
|
||||
import {
|
||||
BridgeTransaction,
|
||||
BridgeTransactionType,
|
||||
Chain,
|
||||
ChainLayer,
|
||||
Token,
|
||||
MessageSentABIEvent,
|
||||
MessageSentLogEvent,
|
||||
} from "@/types";
|
||||
import { formatOnChainMessageStatus } from "./formatOnChainMessageStatus";
|
||||
import { getCompleteTxStoreKey } from "./getCompleteTxStoreKey";
|
||||
import { isBlockTooOld } from "./isBlockTooOld";
|
||||
|
||||
export async function fetchETHBridgeEvents(
|
||||
historyStoreActions: HistoryActionsForCompleteTxCaching,
|
||||
lineaSDK: LineaSDK,
|
||||
address: Address,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
tokens: Token[],
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const transactionsMap = new Map<string, BridgeTransaction>();
|
||||
|
||||
const client = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain.id,
|
||||
});
|
||||
|
||||
const contract = fromChain.layer === ChainLayer.L1 ? lineaSDK.getL2Contract() : lineaSDK.getL1Contract();
|
||||
|
||||
const messageServiceAddress = fromChain.messageServiceAddress;
|
||||
const [ethLogsForSender, ethLogsForRecipient] = await Promise.all([
|
||||
client.getLogs({
|
||||
event: MessageSentABIEvent,
|
||||
// No need to find more 'optimal' value than earliest.
|
||||
// Empirical testing showed no practical difference when using hardcoded block number (that was 90 days old).
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: messageServiceAddress,
|
||||
args: {
|
||||
_from: address,
|
||||
},
|
||||
}),
|
||||
client.getLogs({
|
||||
event: MessageSentABIEvent,
|
||||
fromBlock: "earliest",
|
||||
toBlock: "latest",
|
||||
address: messageServiceAddress,
|
||||
args: {
|
||||
_to: address,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const messageSentLogs = [...ethLogsForSender, ...ethLogsForRecipient] as unknown as MessageSentLogEvent[];
|
||||
|
||||
const uniqueLogsMap = new Map<string, (typeof messageSentLogs)[0]>();
|
||||
for (const log of messageSentLogs) {
|
||||
const uniqueKey = `${log.args._from}-${log.args._to}-${log.transactionHash}`;
|
||||
if (!uniqueLogsMap.has(uniqueKey)) {
|
||||
uniqueLogsMap.set(uniqueKey, log);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Array.from(uniqueLogsMap.values()).map(async (log) => {
|
||||
const uniqueKey = `${log.args._from}-${log.args._to}-${log.transactionHash}`;
|
||||
|
||||
// Search cache for completed tx for this txHash, if cache-hit can skip remaining logic
|
||||
const cacheKey = getCompleteTxStoreKey(fromChain.id, log.transactionHash);
|
||||
const cachedCompletedTx = historyStoreActions.getCompleteTx(cacheKey);
|
||||
if (cachedCompletedTx) {
|
||||
transactionsMap.set(uniqueKey, cachedCompletedTx);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageHash = log.args._messageHash;
|
||||
|
||||
const block = await client.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
|
||||
if (isBlockTooOld(block)) return;
|
||||
|
||||
const messageStatus = await contract.getMessageStatus(messageHash);
|
||||
|
||||
const token = tokens.find((token) => token.type.includes("eth"));
|
||||
|
||||
const tx = {
|
||||
type: BridgeTransactionType.ETH,
|
||||
status: formatOnChainMessageStatus(messageStatus),
|
||||
token: token || defaultTokensConfig.MAINNET[0],
|
||||
fromChain,
|
||||
toChain,
|
||||
timestamp: block.timestamp,
|
||||
bridgingTx: log.transactionHash,
|
||||
message: {
|
||||
from: log.args._from,
|
||||
to: log.args._to,
|
||||
fee: log.args._fee,
|
||||
value: log.args._value,
|
||||
nonce: log.args._nonce,
|
||||
calldata: log.args._calldata,
|
||||
messageHash: log.args._messageHash,
|
||||
amountSent: log.args._value,
|
||||
},
|
||||
};
|
||||
|
||||
// Store COMPLETE tx in cache
|
||||
historyStoreActions.setCompleteTx(tx);
|
||||
transactionsMap.set(uniqueKey, tx);
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(transactionsMap.values());
|
||||
}
|
||||
50
bridge-ui/src/utils/history/fetchTransactionsHistory.ts
Normal file
50
bridge-ui/src/utils/history/fetchTransactionsHistory.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Address } from "viem";
|
||||
import { LineaSDK } from "@consensys/linea-sdk";
|
||||
import { config } from "@/config";
|
||||
import { BridgeTransaction, Chain, Token } from "@/types";
|
||||
import { fetchETHBridgeEvents } from "./fetchETHBridgeEvents";
|
||||
import { fetchERC20BridgeEvents } from "./fetchERC20BridgeEvents";
|
||||
import { fetchCctpBridgeEvents } from "./fetchCctpBridgeEvents";
|
||||
import { HistoryActionsForCompleteTxCaching } from "@/stores";
|
||||
|
||||
type TransactionHistoryParams = {
|
||||
lineaSDK: LineaSDK;
|
||||
historyStoreActions: HistoryActionsForCompleteTxCaching;
|
||||
fromChain: Chain;
|
||||
toChain: Chain;
|
||||
address: Address;
|
||||
tokens: Token[];
|
||||
};
|
||||
|
||||
export async function fetchTransactionsHistory({
|
||||
lineaSDK,
|
||||
fromChain,
|
||||
toChain,
|
||||
address,
|
||||
tokens,
|
||||
historyStoreActions,
|
||||
}: TransactionHistoryParams): Promise<BridgeTransaction[]> {
|
||||
const events = await Promise.all([
|
||||
fetchBridgeEvents(lineaSDK, fromChain, toChain, address, tokens, historyStoreActions),
|
||||
fetchBridgeEvents(lineaSDK, toChain, fromChain, address, tokens, historyStoreActions),
|
||||
]);
|
||||
return events.flat().sort((a, b) => Number(b.timestamp.toString()) - Number(a.timestamp.toString()));
|
||||
}
|
||||
|
||||
async function fetchBridgeEvents(
|
||||
lineaSDK: LineaSDK,
|
||||
fromChain: Chain,
|
||||
toChain: Chain,
|
||||
address: Address,
|
||||
tokens: Token[],
|
||||
historyStoreActions: HistoryActionsForCompleteTxCaching,
|
||||
): Promise<BridgeTransaction[]> {
|
||||
const [ethEvents, erc20Events, cctpEvents] = await Promise.all([
|
||||
fetchETHBridgeEvents(historyStoreActions, lineaSDK, address, fromChain, toChain, tokens),
|
||||
fetchERC20BridgeEvents(historyStoreActions, lineaSDK, address, fromChain, toChain, tokens),
|
||||
// Feature toggle for CCTP, will filter out USDC transactions if isCctpEnabled == false
|
||||
config.isCctpEnabled ? fetchCctpBridgeEvents(historyStoreActions, address, fromChain, toChain, tokens) : [],
|
||||
]);
|
||||
|
||||
return [...ethEvents, ...erc20Events, ...cctpEvents];
|
||||
}
|
||||
15
bridge-ui/src/utils/history/formatOnChainMessageStatus.ts
Normal file
15
bridge-ui/src/utils/history/formatOnChainMessageStatus.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { OnChainMessageStatus } from "@consensys/linea-sdk";
|
||||
import { TransactionStatus } from "@/types";
|
||||
|
||||
export function formatOnChainMessageStatus(status: OnChainMessageStatus): TransactionStatus {
|
||||
switch (status) {
|
||||
case OnChainMessageStatus.UNKNOWN:
|
||||
return TransactionStatus.PENDING;
|
||||
case OnChainMessageStatus.CLAIMABLE:
|
||||
return TransactionStatus.READY_TO_CLAIM;
|
||||
case OnChainMessageStatus.CLAIMED:
|
||||
return TransactionStatus.COMPLETED;
|
||||
default:
|
||||
return TransactionStatus.PENDING;
|
||||
}
|
||||
}
|
||||
10
bridge-ui/src/utils/history/getCompleteTxStoreKey.ts
Normal file
10
bridge-ui/src/utils/history/getCompleteTxStoreKey.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { BridgeTransaction } from "@/types";
|
||||
import { SupportedChainId } from "@/lib/wagmi";
|
||||
|
||||
export function getCompleteTxStoreKeyForTx(transaction: BridgeTransaction): string {
|
||||
return getCompleteTxStoreKey(transaction.fromChain.id, transaction.bridgingTx);
|
||||
}
|
||||
|
||||
export function getCompleteTxStoreKey(fromChainId: SupportedChainId, bridgingTxHash: string): string {
|
||||
return `${fromChainId}-${bridgingTxHash}`;
|
||||
}
|
||||
2
bridge-ui/src/utils/history/index.ts
Normal file
2
bridge-ui/src/utils/history/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { fetchTransactionsHistory } from "./fetchTransactionsHistory";
|
||||
export { getCompleteTxStoreKey, getCompleteTxStoreKeyForTx } from "./getCompleteTxStoreKey";
|
||||
15
bridge-ui/src/utils/history/isBlockTooOld.ts
Normal file
15
bridge-ui/src/utils/history/isBlockTooOld.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { GetBlockReturnType } from "viem";
|
||||
import { compareAsc, fromUnixTime, subDays } from "date-fns";
|
||||
|
||||
// Transactions with an age > TOO_OLD_IN_DAYS_THRESHOLD will not be shown in the TransactionHistory
|
||||
const TOO_OLD_IN_DAYS_THRESHOLD = 90;
|
||||
|
||||
export function isBlockTooOld(block: GetBlockReturnType): boolean {
|
||||
const currentTimestamp = new Date();
|
||||
return (
|
||||
compareAsc(
|
||||
fromUnixTime(Number(block.timestamp.toString())),
|
||||
subDays(currentTimestamp, TOO_OLD_IN_DAYS_THRESHOLD),
|
||||
) === -1
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
export { eventETH, eventERC20, eventERC20V2, eventUSDC } from "./events";
|
||||
export { getNativeBridgeMessageClaimedTxHash } from "./events";
|
||||
export { generateChain, generateChains, getChainLogoPath, getChainNetworkLayer } from "./chains";
|
||||
export { estimateEthGasFee, estimateERC20GasFee } from "./fees";
|
||||
export { formatAddress, formatBalance, formatHex, formatTimestamp, safeGetAddress } from "./format";
|
||||
export { fetchTransactionsHistory, type BridgeTransaction } from "./history";
|
||||
export { computeMessageHash, computeMessageStorageSlot } from "./message";
|
||||
export { isEth } from "./tokens";
|
||||
export { fetchTransactionsHistory } from "./history";
|
||||
export { computeMessageHash, computeMessageStorageSlot, isCctpV2BridgeMessage, isNativeBridgeMessage } from "./message";
|
||||
export { isEth, isCctp, USDC_SYMBOL } from "./tokens";
|
||||
export { isEmptyObject } from "./utils";
|
||||
export { CCTP_TOKEN_MESSENGER, getCCTPClaimTx, isCCTPNonceUsed, getCCTPTransactionStatus } from "./cctp";
|
||||
export {
|
||||
CCTP_TRANSFER_MAX_FEE_FALLBACK,
|
||||
CCTP_MIN_FINALITY_THRESHOLD,
|
||||
getCctpTransactionStatus,
|
||||
getCctpMessageByTxHash,
|
||||
} from "./cctp";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { keccak256, encodeAbiParameters, Address } from "viem";
|
||||
import { CCTPV2BridgeMessage, NativeBridgeMessage } from "./history";
|
||||
import { CctpV2BridgeMessage, NativeBridgeMessage } from "@/types";
|
||||
|
||||
const INBOX_L1L2_MESSAGE_STATUS_MAPPING_SLOT = 176n;
|
||||
|
||||
@@ -38,7 +38,7 @@ export function computeMessageStorageSlot(messageHash: `0x${string}`) {
|
||||
);
|
||||
}
|
||||
|
||||
export function isNativeBridgeMessage(msg: NativeBridgeMessage | CCTPV2BridgeMessage): msg is NativeBridgeMessage {
|
||||
export function isNativeBridgeMessage(msg: NativeBridgeMessage | CctpV2BridgeMessage): msg is NativeBridgeMessage {
|
||||
return (
|
||||
typeof (msg as NativeBridgeMessage).from === "string" &&
|
||||
typeof (msg as NativeBridgeMessage).to === "string" &&
|
||||
@@ -49,9 +49,10 @@ export function isNativeBridgeMessage(msg: NativeBridgeMessage | CCTPV2BridgeMes
|
||||
);
|
||||
}
|
||||
|
||||
export function isCCTPV2BridgeMessage(msg: NativeBridgeMessage | CCTPV2BridgeMessage): msg is CCTPV2BridgeMessage {
|
||||
export function isCctpV2BridgeMessage(msg: NativeBridgeMessage | CctpV2BridgeMessage): msg is CctpV2BridgeMessage {
|
||||
return (
|
||||
typeof (msg as CCTPV2BridgeMessage).message === "string" &&
|
||||
typeof (msg as CCTPV2BridgeMessage).attestation === "string"
|
||||
typeof (msg as CctpV2BridgeMessage).nonce === "string" &&
|
||||
typeof (msg as CctpV2BridgeMessage).message === "string" &&
|
||||
typeof (msg as CctpV2BridgeMessage).attestation === "string"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,3 +19,5 @@ export const isCctp = (token: Token) => {
|
||||
isAddress(token.L2)
|
||||
);
|
||||
};
|
||||
|
||||
export const USDC_SYMBOL = "USDC";
|
||||
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -245,6 +245,8 @@ importers:
|
||||
specifier: 17.7.2
|
||||
version: 17.7.2
|
||||
|
||||
contracts/lib/forge-std: {}
|
||||
|
||||
e2e:
|
||||
devDependencies:
|
||||
'@jest/globals':
|
||||
|
||||
Reference in New Issue
Block a user