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
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ yalc.lock
/dist/
.vercel
.continueignore
.agents/
e2e.log
docs/specs/**
codex-resume
skills-lock.json
codex-resume
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
Expand Up @@ -21,6 +21,7 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({
onTxDetailsPress
}) => {
const errorEntries = Object.entries(historyErrorsByChain || {});
const hasTransactionHistory = realTransactionHistory.length > 0;

return (
<VStack space={4} width="100%">
Expand Down Expand Up @@ -69,7 +70,7 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({
Loading transaction history...
</Text>
</Box>
) : realTransactionHistory.length > 0 ? (
) : hasTransactionHistory ? (
<Box maxH="400px" overflowY="auto">
<BridgeTransactionList transactions={realTransactionHistory} onTxDetailsPress={onTxDetailsPress} />
</Box>
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
Expand Up @@ -17,6 +17,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 @@ -134,6 +140,7 @@ export const useMPBBridgeViewController = ({
onBridgeStart,
onBridgeFailed,
onBridgeSuccess,
bridgeReadOnlyUrls,
bridgeProvider: propBridgeProvider,
onBridgeProviderChange
}: MPBBridgeProps): MPBBridgeViewModel => {
Expand Down Expand Up @@ -166,7 +173,7 @@ export const useMPBBridgeViewController = ({
} = useMPBBridgeUiState();

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

const gdValue = getBalanceForChain(sourceChain);
Expand Down
13 changes: 10 additions & 3 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,14 +175,21 @@ export const useChainBalances = () => {
return { getBalanceForChain };
};

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

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
};
};
5 changes: 3 additions & 2 deletions packages/sdk-v2/src/hooks/useMulticallAtChain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";

import { Result } from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber";
import { BaseProvider, JsonRpcProvider, Provider } from "@ethersproject/providers";
import { BaseProvider, JsonRpcProvider, Provider, StaticJsonRpcProvider } from "@ethersproject/providers";
import { Contract } from "ethers";
import { noop } from "lodash";

Expand Down Expand Up @@ -86,7 +86,8 @@ export const useReadOnlyProvider = (chainId: number) => {
return (factory as any)() as JsonRpcProvider;
}

const provider = new JsonRpcProvider(factory as any);
// The chain is already known here, so avoid an extra network-detection RPC on rate-limited endpoints.
const provider = new StaticJsonRpcProvider(factory as any, chainId);
Comment thread
L03TJ3 marked this conversation as resolved.

provider.pollingInterval = pollingInterval;
return provider;
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk-v2/src/sdk/mpbridge/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ export * from "./useBridgeMonitoring";
export * from "./useBridgeValidators";
export * from "./useGetMPBBridgeData";
export * from "./useLayerZeroFee";
export * from "./useMPBBridge.helpers";
export * from "./useMPBBridge";
export * from "./useMPBBridgeHistory.helpers";
export * from "./useMPBBridgeHistory";
export * from "./useMPBG$TokenContract";
export * from "./useProductionG$Balance";
16 changes: 16 additions & 0 deletions packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const getTransactionErrorMessage = (error: any): string => {
return (
error?.error?.data?.message ||
error?.error?.message ||
error?.reason ||
error?.data?.message ||
error?.message ||
"Transaction failed"
);
};

export const isTransientBlockReadError = (error: any): boolean => {
const message = getTransactionErrorMessage(error).toLowerCase();

return message.includes("unknown block") || message.includes("no block");
};
21 changes: 21 additions & 0 deletions packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getTransactionErrorMessage, isTransientBlockReadError } from "./useMPBBridge.helpers";

describe("useMPBBridge transaction error helpers", () => {
it("detects unknown block read errors from common wallet/provider shapes", () => {
expect(isTransientBlockReadError(new Error("Unknown block"))).toBe(true);
expect(isTransientBlockReadError({ reason: "no block found" })).toBe(true);
expect(isTransientBlockReadError({ error: { data: { message: "UNKNOWN BLOCK" } } })).toBe(true);
});

it("does not classify regular transaction failures as transient block reads", () => {
expect(isTransientBlockReadError(new Error("user rejected transaction"))).toBe(false);
expect(isTransientBlockReadError({ error: { message: "execution reverted" } })).toBe(false);
});

it("extracts a readable transaction error message", () => {
expect(getTransactionErrorMessage({ error: { data: { message: "execution reverted" } } })).toBe(
"execution reverted"
);
expect(getTransactionErrorMessage({ reason: "user rejected transaction" })).toBe("user rejected transaction");
});
});
Loading