import React, { FC, useCallback, useEffect, useState } from "react"; import { G$Amount, SupportedChains, useBridge, useBridgeHistory, useGetBridgeData, useGetEnvChainId, useRelayTx, useWithinBridgeLimits } from "@gooddollar/web3sdk-v2"; import moment from "moment"; import { ethers } from "ethers"; import { useEthers } from "@usedapp/core"; import { noop } from "lodash"; import { ArrowForwardIcon, Box, Button, FlatList, Flex, Heading, HStack, Skeleton, Spinner, Stack, VStack, Text } from "native-base"; import { GdAmount, Title } from "../../core/layout"; import { ExplorerLink } from "../../core/web3/ExplorerLink"; import { BridgeWizard } from "./wizard/BridgeWizard"; import { truncateMiddle } from "../../utils"; const FUSE_CHAIN_ID = 122; const CELO_CHAIN_ID = 42220; /** Maps bridge target chain id to source/target chain ids and display names. */ function getBridgeChainInfo(targetChainId: number) { const isTargetFuse = targetChainId === FUSE_CHAIN_ID; return { sourceChainId: isTargetFuse ? CELO_CHAIN_ID : FUSE_CHAIN_ID, targetChainId, sourceChainName: isTargetFuse ? "Celo" : "Fuse", targetChainName: isTargetFuse ? "Fuse" : "Celo" }; } const useCanBridge = (chain: "fuse" | "celo", amountWei: string) => { const { chainId } = useGetEnvChainId(chain === "fuse" ? SupportedChains.FUSE : SupportedChains.CELO); const { account } = useEthers(); const canBridge = useWithinBridgeLimits(chainId, account || "", amountWei); return canBridge; }; const BridgeHistoryWithRelay = () => { const { historySorted } = useBridgeHistory() ?? {}; const relayTx = useRelayTx(); const [relaying, setRelaying] = useState<{ [key: string]: boolean }>({}); const triggerRelay = useCallback( async (i: any) => { setRelaying({ ...relaying, [i.transactionHash]: true }); try { const { sourceChainId, targetChainId } = getBridgeChainInfo(i.data.targetChainId.toNumber()); const relayResult = await relayTx(sourceChainId, targetChainId, i.transactionHash); if (!relayResult.relayPromise) { setRelaying({ ...relaying, [i.transactionHash]: false }); } } catch (e) { //todo: add analytics error log setRelaying({ ...relaying, [i.transactionHash]: false }); } }, [setRelaying, relayTx] ); return ( Transactions History Transaction Hash From To Amount Status {historySorted ? ( historySorted.map(i => { const chainInfo = getBridgeChainInfo(i.data.targetChainId.toNumber()); return ( {chainInfo.sourceChainName} {chainInfo.targetChainName} {i.amount} G$ {(i as any).relayEvent ? ( ) : ( )} ); }) ) : ( )} ); }; /** Table column widths - fixed % so header and rows stay aligned */ const COL_WIDTH_DATE = "18%"; const COL_WIDTH_AMOUNT = "32%"; const COL_WIDTH_STATUS = "50%"; const TABLE_PADDING_X = 4; const HistoryRowItem = ({ item, env }: { item: any; env: string }) => { const { amount, data, relayEvent, transactionHash } = item; const { targetChainId, timestamp } = data || {}; const { fee } = relayEvent?.data || {}; const date = moment(ethers.BigNumber.from(timestamp).toNumber() * 1000).utc(); const dateFormatted = date.format("DD.MM.YY"); const dateHours = date.format("HH:mm"); const targetChain = parseInt(targetChainId); const { sourceChainId: sourceChain } = getBridgeChainInfo(targetChain); const feeFormatted = fee && targetChain ? G$Amount("G$", ethers.BigNumber.from(fee), targetChain, env) : null; return ( {dateFormatted} {dateHours} G$ {amount} {feeFormatted ? ( Fees:{" "} {`G$ `} ) : ( )} {relayEvent?.transactionHash && targetChain ? ( <> Completed {`(Bridged to ${SupportedChains[targetChain]})`} ) : ( <> {`Waiting for bridge relayers to relay to target chain... (Can take a few minutes)`} {`(Bridging from ${SupportedChains[sourceChain]})`} )} ); }; const BridgeHistory = ({ env }: { env: string }) => { const { historySorted } = useBridgeHistory() ?? {}; return ( Recent Transactions It may take up to 30 seconds to load transactions. {/* Table header - same widths and padding as rows for column alignment */} Date Amount Status {/* Table body - FlatList so each row aligns under the header */} {!historySorted ? ( ) : ( item.transactionHash ?? String(Math.random())} renderItem={({ item }) => ( )} maxH="250" scrollEnabled={true} _contentContainerStyle={{ width: "100%", minWidth: "100%" }} /> )} ); }; interface IMicroBridgeControllerProps { withRelay?: boolean; withHistory?: boolean; onBridgeStart?: () => void; onBridgeSuccess?: () => void; onBridgeFailed?: (e: Error) => void; } export const MicroBridgeController: FC = ({ withRelay = false, withHistory = true, // onBridgeStart = noop, onBridgeSuccess = noop, onBridgeFailed = noop }: IMicroBridgeControllerProps) => { const { chainId } = useEthers(); const { defaultEnv } = useGetEnvChainId(); const { sendBridgeRequest, bridgeRequestStatus, relayStatus, selfRelayStatus } = useBridge(); const { bridgeFees: fuseBridgeFees, bridgeLimits: fuseBridgeLimits } = useGetBridgeData(SupportedChains.FUSE, ""); const { bridgeFees: celoBridgeFees, bridgeLimits: celoBridgeLimits } = useGetBridgeData(SupportedChains.CELO, ""); const inputTransaction = useState("0"); const pendingTransaction = useState(false); const originChain = useState<"fuse" | "celo">(chainId === 122 ? "fuse" : "celo"); const [error, setError] = useState(null); const onBridgeStart = useCallback(async () => { const [inputWei] = inputTransaction; try { await sendBridgeRequest(inputWei, originChain[0]); } catch (e: any) { onBridgeFailed(e); } }, [inputTransaction, originChain]); useEffect(() => { void (async () => { if (bridgeRequestStatus.status === "Exception") { setError(bridgeRequestStatus.errorMessage as string); } })(); }, [bridgeRequestStatus]); if (!fuseBridgeFees || !celoBridgeFees) return ; return ( <> {withRelay && } {withHistory && } ); };