Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ yalc.lock
.continueignore
e2e.log
docs/specs/**
codex-resume
codex-resume
.bounties
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,27 @@ import { VStack } from "native-base";

import { MPBBridge } from "./MPBBridge";
import { useMPBBridgeFeatureController } from "./feature/useMPBBridgeFeatureController";
import type { MPBBridgeReadOnlyUrls } from "./types";

interface IMPBBridgeControllerProps {
withHistory?: boolean;
onBridgeStart?: () => void;
onBridgeSuccess?: () => void;
onBridgeFailed?: (e: Error) => void;
bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls;
}

export const MPBBridgeController: React.FC<IMPBBridgeControllerProps> = ({
onBridgeStart,
onBridgeSuccess,
onBridgeFailed
onBridgeFailed,
bridgeReadOnlyUrls
}) => {
const bridgeProps = useMPBBridgeFeatureController({
onBridgeStart,
onBridgeSuccess,
onBridgeFailed
onBridgeFailed,
bridgeReadOnlyUrls
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,105 @@
import React from "react";
import { Box, Spinner, Text, VStack } from "native-base";
import { Box, Button, HStack, Spinner, Text, VStack } from "native-base";
import { ExplorerLink } from "../../../core";
import { BridgeTransactionList } from "./MPBBridgeTransactionCard";
import { capitalizeChain, getChainName } from "./utils";

interface TransactionHistoryProps {
realTransactionHistory: any[];
historyLoading: boolean;
historyRefreshing: boolean;
historyErrorsByChain: Record<number, string>;
explorerChainId?: number;
explorerAddress?: string;
onRefresh: () => void;
onTxDetailsPress: (tx: any) => void;
}

export const TransactionHistory: React.FC<TransactionHistoryProps> = ({
realTransactionHistory,
historyLoading,
historyRefreshing,
historyErrorsByChain,
explorerChainId,
explorerAddress,
onRefresh,
onTxDetailsPress
}) => {
const errorEntries = Object.entries(historyErrorsByChain || {});
const hasTransactionHistory = realTransactionHistory.length > 0;
Comment on lines +28 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against realTransactionHistory being undefined before accessing .length.

useDebouncedTransactionHistory can return undefined for realTransactionHistory on the first render, so realTransactionHistory.length will throw in that case.

Consider normalising to an array before use, e.g.:

const history = realTransactionHistory ?? [];
const hasTransactionHistory = history.length > 0;

and then use history in the JSX. Alternatively, update useDebouncedTransactionHistory so realTransactionHistory is always an array (default []).


return (
<VStack space={4} width="100%">
<Text fontFamily="heading" fontSize="xl" fontWeight="700" color="goodBlue.600">
Recent Transactions
</Text>
<HStack justifyContent="space-between" alignItems="center" space={3}>
<Text fontFamily="heading" fontSize="xl" fontWeight="700" color="goodBlue.600">
Recent Transactions
</Text>
<Button
variant="outline"
size="sm"
borderColor="goodBlue.500"
_text={{ color: "goodBlue.500", fontWeight: "600" }}
isDisabled={historyLoading || historyRefreshing}
isLoading={historyRefreshing}
onPress={onRefresh}
>
Refresh
</Button>
</HStack>
<VStack space={1}>
<Text fontSize="xs" color="goodGrey.600">
History builds as you use this bridge. We check up to the latest 5,000 blocks on each supported network.
</Text>
<Text fontSize="xs" color="goodGrey.500">
Older transactions or activity from another device may not appear.
</Text>
{explorerChainId && explorerAddress ? (
<ExplorerLink
chainId={explorerChainId}
addressOrTx={explorerAddress}
text="View this wallet on the connected network explorer"
fontStyle={{ fontSize: "xs", fontFamily: "subheading", fontWeight: 600 }}
/>
) : null}
</VStack>
{historyRefreshing && !historyLoading ? (
<HStack alignItems="center" space={2}>
<Spinner size="sm" color="goodBlue.500" />
<Text fontSize="xs" color="goodGrey.600">
Refreshing transaction history...
</Text>
</HStack>
) : null}
{errorEntries.length > 0 ? (
<Box p={4} bg="yellow.50" borderRadius="lg" borderWidth="1" borderColor="yellow.200">
<VStack space={2}>
<Text fontSize="sm" color="yellow.800" fontWeight="600">
Some transaction history could not be refreshed.
</Text>
{errorEntries.map(([chainId]) => (
<Text key={chainId} fontSize="xs" color="yellow.700">
Could not fetch history for {capitalizeChain(getChainName(Number(chainId)))} at this time. You can try
to reload or try later.
</Text>
))}
</VStack>
</Box>
) : null}
{historyLoading ? (
<Box p={6} bg="goodGrey.50" borderRadius="lg" alignItems="center">
<Spinner size="sm" color="goodBlue.500" />
<Text mt={3} fontSize="sm" color="goodGrey.600">
Loading transaction history...
</Text>
</Box>
) : realTransactionHistory.length > 0 ? (
) : hasTransactionHistory ? (
<Box maxH="400px" overflowY="auto">
<BridgeTransactionList transactions={realTransactionHistory} onTxDetailsPress={onTxDetailsPress} />
</Box>
) : (
<Box p={6} bg="goodGrey.50" borderRadius="lg" alignItems="center">
<Text fontSize="sm" color="goodGrey.600" textAlign="center">
No recent bridge transactions found
No bridge transactions found in the latest 5,000 blocks
</Text>
<Text fontSize="xs" color="goodGrey.500" mt={2} textAlign="center">
Make sure your wallet is connected to see your bridge transactions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ import { useEthers } from "@usedapp/core";
import { ethers } from "ethers";
import { useMPBBridgeFlow, useG$Decimals, SupportedChains, VALIDATION_REASONS } from "@gooddollar/web3sdk-v2";

import { BridgeProvider, MPBBridgeProps } from "../types";
import { BridgeProvider, MPBBridgeProps, MPBBridgeReadOnlyUrls } from "../types";
import { getDefaultTargetChain } from "../utils/chainHelpers";

interface UseMPBBridgeFeatureControllerParams {
onBridgeStart?: () => void;
onBridgeSuccess?: () => void;
onBridgeFailed?: (e: Error) => void;
bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls;
}

const ZERO_FEE = { nativeFee: ethers.BigNumber.from(0), zroFee: ethers.BigNumber.from(0) };

export const useMPBBridgeFeatureController = ({
onBridgeStart,
onBridgeSuccess,
onBridgeFailed
onBridgeFailed,
bridgeReadOnlyUrls
}: UseMPBBridgeFeatureControllerParams): MPBBridgeProps => {
const { chainId, account } = useEthers();
const [bridgeProvider, setBridgeProvider] = useState<BridgeProvider>("layerzero");
Expand Down Expand Up @@ -176,6 +178,7 @@ export const useMPBBridgeFeatureController = ({
onBridgeStart: onBridgeStartHandler,
onBridgeFailed,
onBridgeSuccess,
bridgeReadOnlyUrls,
bridgeProvider,
onBridgeProviderChange: setBridgeProvider
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useCallback, useState, useMemo, useRef } from "react";
import { useEthers } from "@usedapp/core";
import { SupportedChains, deriveMPBBridgeFlowState } from "@gooddollar/web3sdk-v2";
import { ethers } from "ethers";

Expand All @@ -17,6 +18,12 @@ import { useMPBBridgeUiState } from "./useMPBBridgeUiState";

const DEBOUNCE_MS = 300;
const TRANSACTION_HISTORY_DEBOUNCE_MS = 2000;
const BRIDGE_HISTORY_CHAIN_IDS: SupportedChains[] = [
SupportedChains.CELO,
SupportedChains.FUSE,
SupportedChains.MAINNET,
SupportedChains.XDC
];

const FLOW_PENDING_STATES = new Set([
"awaiting_network_switch",
Expand Down Expand Up @@ -113,6 +120,11 @@ export interface MPBBridgeViewModel {
transactionHistoryProps: {
realTransactionHistory: any[];
historyLoading: boolean;
historyRefreshing: boolean;
historyErrorsByChain: Record<number, string>;
explorerChainId?: number;
explorerAddress?: string;
onRefresh: () => void;
onTxDetailsPress: (tx: any) => void;
};
}
Expand All @@ -131,9 +143,11 @@ export const useMPBBridgeViewController = ({
onBridgeStart,
onBridgeFailed,
onBridgeSuccess,
bridgeReadOnlyUrls,
bridgeProvider: propBridgeProvider,
onBridgeProviderChange
}: MPBBridgeProps): MPBBridgeViewModel => {
const { account, chainId } = useEthers();
const [isBridging, setBridging] = useState(false);
const [localBridgeProvider, setLocalBridgeProvider] = useState<BridgeProvider>("axelar");
const bridgeProvider = propBridgeProvider || localBridgeProvider;
Expand Down Expand Up @@ -162,7 +176,8 @@ export const useMPBBridgeViewController = ({
closeAllDropdowns
} = useMPBBridgeUiState();

const { realTransactionHistory, historyLoading } = useDebouncedTransactionHistory(TRANSACTION_HISTORY_DEBOUNCE_MS);
const { realTransactionHistory, historyLoading, historyRefreshing, historyErrorsByChain, refreshHistory } =
useDebouncedTransactionHistory(TRANSACTION_HISTORY_DEBOUNCE_MS, bridgeReadOnlyUrls, BRIDGE_HISTORY_CHAIN_IDS);
const { getBalanceForChain } = useChainBalances();

const gdValue = getBalanceForChain(sourceChain);
Expand Down Expand Up @@ -380,6 +395,7 @@ export const useMPBBridgeViewController = ({
successHandled.current = true;
setBridgingStatus(effectiveFlow.statusLabel || "Bridge completed successfully!");
setBridging(false);
refreshHistory?.();

if (!successModalOpen && !successModalDismissedRef.current) {
setSuccessModalOpen(true);
Expand Down Expand Up @@ -435,6 +451,7 @@ export const useMPBBridgeViewController = ({
successModalOpen,
onBridgeSuccess,
onBridgeFailed,
refreshHistory,
setBridging,
setBridgingStatus,
setSuccessModalOpen,
Expand Down Expand Up @@ -598,6 +615,11 @@ export const useMPBBridgeViewController = ({
transactionHistoryProps: {
realTransactionHistory: recentTransactions,
historyLoading,
historyRefreshing,
historyErrorsByChain,
explorerChainId: chainId,
explorerAddress: account,
onRefresh: refreshHistory,
onTxDetailsPress
}
};
Expand Down
24 changes: 20 additions & 4 deletions packages/good-design/src/apps/bridge/mpbridge/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CurrencyValue } from "@usedapp/core";
import { useG$Amounts, useProductionG$Balance, G$Amount, useGetEnvChainId } from "@gooddollar/web3sdk-v2";
import { BigNumber } from "ethers";
import { fetchBridgeFees, useMPBBridgeHistory } from "@gooddollar/web3sdk-v2";
import type { IMPBFees, IMPBLimits } from "./types";
import type { IMPBFees, IMPBLimits, MPBBridgeHistoryChainIds, MPBBridgeReadOnlyUrls } from "./types";
import { convertTransaction } from "./utils";

const CACHE_KEY = "mpb-bridge-fees-cache";
Expand Down Expand Up @@ -175,8 +175,21 @@ export const useChainBalances = () => {
return { getBalanceForChain };
};

export const useDebouncedTransactionHistory = (delay = 1000) => {
const { historySorted: realTransactionHistory } = useMPBBridgeHistory() ?? {};
export const useDebouncedTransactionHistory = (
delay = 1000,
bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls,
bridgeHistoryChainIds?: MPBBridgeHistoryChainIds
) => {
const {
historySorted: realTransactionHistory,
initialLoading,
refreshing,
errorsByChain,
refreshHistory
} = useMPBBridgeHistory({
readOnlyUrls: bridgeReadOnlyUrls,
chainIds: bridgeHistoryChainIds
}) ?? {};
const [debouncedHistory, setDebouncedHistory] = useState(realTransactionHistory);
const timeoutRef = useRef<NodeJS.Timeout>();

Expand All @@ -190,7 +203,10 @@ export const useDebouncedTransactionHistory = (delay = 1000) => {

return {
realTransactionHistory: debouncedHistory,
historyLoading: !realTransactionHistory
historyLoading: Boolean(initialLoading),
historyRefreshing: Boolean(refreshing),
historyErrorsByChain: errorsByChain || {},
refreshHistory: refreshHistory || (() => undefined)
};
};

Expand Down
10 changes: 9 additions & 1 deletion packages/good-design/src/apps/bridge/mpbridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ export { MPBBridge } from "./MPBBridge";
export { MPBBridgeController } from "./MPBBridgeController";
export { useMPBBridgeFeatureController } from "./feature/useMPBBridgeFeatureController";
export { BridgeTransactionCard, BridgeTransactionList } from "./MPBBridgeTransactionCard";
export type { MPBBridgeProps, IMPBLimits, IMPBFees, BridgeProvider, BridgeTransaction } from "./types";
export type {
MPBBridgeProps,
MPBBridgeHistoryChainIds,
MPBBridgeReadOnlyUrls,
IMPBLimits,
IMPBFees,
BridgeProvider,
BridgeTransaction
} from "./types";

export { ChainSelector } from "./ChainSelector";
export { BridgeProviderSelector } from "./BridgeProviderSelector";
Expand Down
5 changes: 5 additions & 0 deletions packages/good-design/src/apps/bridge/mpbridge/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { BigNumber } from "ethers";
import type { SupportedChains } from "@gooddollar/web3sdk-v2";

export type BridgeProvider = "axelar" | "layerzero";

export type MPBBridgeReadOnlyUrls = Partial<Record<number, string>>;
export type MPBBridgeHistoryChainIds = SupportedChains[];

export type BridgeTransaction = {
id: string;
transactionHash: string;
Expand Down Expand Up @@ -62,6 +66,7 @@ export interface MPBBridgeProps {
onBridgeStart?: (sourceChain: string, targetChain: string) => Promise<void>;
onBridgeFailed?: (error: Error) => void;
onBridgeSuccess?: () => void;
bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls;
bridgeProvider?: BridgeProvider;
onBridgeProviderChange?: (provider: BridgeProvider) => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TransactionStatus } from "@usedapp/core";

import { createTransactionDetails } from "./transactionHelpers";

jest.mock("@gooddollar/web3sdk-v2", () => ({
getSourceChainId: jest.fn(() => 42220)
}));

describe("createTransactionDetails", () => {
it("sets a date for the submitted transaction details", () => {
const date = new Date("2026-07-07T10:00:00.000Z");

const transaction = createTransactionDetails({
amountWei: "10000000000000000000",
sourceChain: "celo",
targetChain: "xdc",
bridgeProvider: "layerzero",
bridgeStatus: {
status: "Success",
transaction: { hash: "0xbridge" }
} as Partial<TransactionStatus>,
bridgeToTxHash: undefined,
date
});

expect(transaction.date).toBe(date);
expect(transaction.transactionHash).toBe("0xbridge");
expect(transaction.amount).toBe("10.00");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ interface CreateTransactionDetailsParams {
bridgeProvider: string;
bridgeStatus: Partial<TransactionStatus> | undefined;
bridgeToTxHash: string | undefined;
date?: Date;
}

export const createTransactionDetails = (params: CreateTransactionDetailsParams): BridgeTransaction => {
const { amountWei, sourceChain, targetChain, bridgeProvider, bridgeStatus, bridgeToTxHash } = params;
const { amountWei, sourceChain, targetChain, bridgeProvider, bridgeStatus, bridgeToTxHash, date } = params;

const amountBN = ethers.BigNumber.from(amountWei || "0");
const amountFormatted = utils.formatEther(amountBN);
Expand All @@ -37,6 +38,7 @@ export const createTransactionDetails = (params: CreateTransactionDetailsParams)
amount: parseFloat(amountFormatted).toFixed(2),
bridgeProvider: bridgeProvider as "axelar" | "layerzero",
status,
date: date ?? new Date(),
chainId: sourceChainId
};
};
Loading