From 0f697f3e80b3e81d715b6aeeb62f009d886e5e66 Mon Sep 17 00:00:00 2001 From: Ebube Date: Tue, 17 Feb 2026 12:03:12 +0100 Subject: [PATCH 1/2] fixed cross chain --- .../components/cross-chain-amount-entry.tsx | 22 ++++++-- .../donation/components/modal-content.tsx | 4 +- .../single-recipient-allocation.tsx | 26 ++++++++- src/features/donation/hooks/form.ts | 56 ++++++++++++++++++- 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/features/donation/components/cross-chain-amount-entry.tsx b/src/features/donation/components/cross-chain-amount-entry.tsx index 8d4d9325..e2038620 100644 --- a/src/features/donation/components/cross-chain-amount-entry.tsx +++ b/src/features/donation/components/cross-chain-amount-entry.tsx @@ -488,10 +488,24 @@ export const CrossChainAmountEntry: React.FC = ({ - {isDisabled && ( -
- Please enter a valid amount in {selectedTokenData?.symbol || "USDC"} greater than an - equivalent of 0.1 NEAR. + {isDisabled && price > 0 && nearPrice > 0 && ( +
+ + Please enter a valid amount in {selectedTokenData?.symbol || "USDC"} greater than an + equivalent of 0.1 NEAR + {" "}(min: {((0.1 * nearPrice) / price).toFixed(4)} {selectedTokenData?.symbol || "USDC"}). + +
)} {/* Action Button */} diff --git a/src/features/donation/components/modal-content.tsx b/src/features/donation/components/modal-content.tsx index 5cb4cc7a..bfed8797 100644 --- a/src/features/donation/components/modal-content.tsx +++ b/src/features/donation/components/modal-content.tsx @@ -86,7 +86,7 @@ export const DonationModalContent: React.FC = ({ const { isLoading: isDonationConfigLoading, data: donationConfig } = donationContractHooks.useConfig(); - const { form, matchingPots, isDisabled, onSubmit, totalAmountFloat, isGroupDonation } = + const { form, matchingPots, isDisabled, onSubmit, totalAmountFloat, isGroupDonation, crossChainMinAmount, crossChainTokenSymbol } = useDonationForm(props); const isCampaignDonation = "campaignId" in props; @@ -141,6 +141,8 @@ export const DonationModalContent: React.FC = ({ matchingPots={matchingPots} {...props} onTokenDataChange={setSelectedTokenData} + crossChainMinAmount={crossChainMinAmount} + crossChainTokenSymbol={crossChainTokenSymbol} /> ); } else if ("potId" in props || "listId" in props) { diff --git a/src/features/donation/components/single-recipient-allocation.tsx b/src/features/donation/components/single-recipient-allocation.tsx index 11c8f9eb..9730d553 100644 --- a/src/features/donation/components/single-recipient-allocation.tsx +++ b/src/features/donation/components/single-recipient-allocation.tsx @@ -40,11 +40,13 @@ export type DonationSingleRecipientAllocationProps = Partial & DonationAllocationInputs & { matchingPots?: Pot[]; onTokenDataChange?: (data: { blockchain: string; tokenData?: any } | null) => void; + crossChainMinAmount?: number; + crossChainTokenSymbol?: string; }; export const DonationSingleRecipientAllocation: React.FC< DonationSingleRecipientAllocationProps -> = ({ form, accountId, matchingPots, campaignId, onTokenDataChange }) => { +> = ({ form, accountId, matchingPots, campaignId, onTokenDataChange, crossChainMinAmount, crossChainTokenSymbol }) => { const walletUser = useWalletUserSession(); const [selectedTokenData, setSelectedTokenData] = useState<{ @@ -332,6 +334,28 @@ export const DonationSingleRecipientAllocation: React.FC< )} /> )} + + {/* Cross-chain minimum amount warning with quick-fix button */} + {isCrossChainToken && crossChainMinAmount !== undefined && crossChainMinAmount > 0 && amount !== undefined && parseFloat(amount.toString()) > 0 && parseFloat(amount.toString()) < crossChainMinAmount && ( +
+ + Minimum amount is{" "} + {crossChainMinAmount.toFixed(4)} {crossChainTokenSymbol || "tokens"}{" "} + (equivalent to 0.1 NEAR). + + +
+ )} ); diff --git a/src/features/donation/hooks/form.ts b/src/features/donation/hooks/form.ts index b1e19a61..21aa1936 100644 --- a/src/features/donation/hooks/form.ts +++ b/src/features/donation/hooks/form.ts @@ -17,6 +17,8 @@ import { useFungibleToken } from "@/entities/_shared/token"; import { extractMatchingPots } from "@/entities/pot"; import { useDispatch } from "@/store/hooks"; +import { useCrossChainTokens } from "./cross-chain-tokens"; + import { DONATION_DEFAULT_MIN_AMOUNT_FLOAT, DONATION_INSUFFICIENT_BALANCE_ERROR, @@ -145,6 +147,40 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams enabled: !isCrossChainToken, }); + // Fetch cross-chain token list to get NEAR price and selected token price + const { data: crossChainTokenList } = useCrossChainTokens(); + + const { crossChainNearPrice, crossChainTokenPrice, crossChainTokenSymbol } = useMemo(() => { + if (!isCrossChainToken || !crossChainTokenList || !values.tokenId) { + return { crossChainNearPrice: 0, crossChainTokenPrice: 0, crossChainTokenSymbol: "" }; + } + + const nearToken = crossChainTokenList.find( + (t) => t.symbol === "wNEAR" || t.assetId === "nep141:wrap.near", + ); + + const parts = values.tokenId.split(":"); + const blockchain = parts[0]; + const assetId = parts.slice(1).join(":"); + const selectedToken = crossChainTokenList.find( + (t) => t.assetId === assetId && t.blockchain.toLowerCase() === blockchain.toLowerCase(), + ); + + return { + crossChainNearPrice: nearToken?.price ?? 0, + crossChainTokenPrice: selectedToken?.price ?? 0, + crossChainTokenSymbol: selectedToken?.symbol ?? "", + }; + }, [isCrossChainToken, crossChainTokenList, values.tokenId]); + + // Minimum amount in the selected cross-chain token equivalent to 0.1 NEAR + const crossChainMinAmount = useMemo(() => { + if (crossChainTokenPrice > 0 && crossChainNearPrice > 0) { + return (0.1 * crossChainNearPrice) / crossChainTokenPrice; + } + return 0; + }, [crossChainNearPrice, crossChainTokenPrice]); + const { data: pot } = indexer.usePot({ enabled: isGroupPotDonation || isSingleRecipientPotDonation, potId: groupDonationPotId ?? values.potAccountId ?? NOOP_STRING, @@ -296,8 +332,21 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams } } + //* Cross-chain minimum amount validation (0.1 NEAR equivalent) + else if ( + isCrossChainToken && + crossChainMinAmount > 0 && + Big(parsedAmount).lt(crossChainMinAmount) + ) { + const errorMessage = `Amount must be at least ${crossChainMinAmount.toFixed(4)} ${crossChainTokenSymbol || "tokens"} (equivalent to 0.1 NEAR).`; + + if (customErrors?.amount?.message !== errorMessage || self.formState.isValid) { + setCustomErrors({ amount: { message: errorMessage } }); + } + } + //* Addressing single-recipient and group donation scenarios with evenly distributed funds - //* Skip minimum amount validation for cross-chain tokens (they don't have min requirements) + //* Skip minimum amount validation for cross-chain tokens (handled above) else if ( !isCrossChainToken && minTotalAmountFloat !== undefined && @@ -344,6 +393,8 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams } }, [ customErrors, + crossChainMinAmount, + crossChainTokenSymbol, isCrossChainToken, isFtDonation, isGroupDonation, @@ -375,5 +426,8 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams // TODO: Likely not needed to be exposed anymore, try using `amount` everywhere // TODO: in the consuming code instead and remove this if no issues detected. totalAmountFloat, + crossChainMinAmount, + crossChainTokenSymbol, + isCrossChainToken, }; }; From 607e1d3fe912768339a6a4ab0ef4a72902673884 Mon Sep 17 00:00:00 2001 From: Ebube Date: Tue, 17 Feb 2026 12:05:16 +0100 Subject: [PATCH 2/2] done with changes --- .../components/cross-chain-amount-entry.tsx | 4 +- .../donation/components/modal-content.tsx | 12 +++- .../single-recipient-allocation.tsx | 57 ++++++++++++------- src/features/donation/hooks/form.ts | 3 +- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/features/donation/components/cross-chain-amount-entry.tsx b/src/features/donation/components/cross-chain-amount-entry.tsx index e2038620..03fffb8d 100644 --- a/src/features/donation/components/cross-chain-amount-entry.tsx +++ b/src/features/donation/components/cross-chain-amount-entry.tsx @@ -492,8 +492,8 @@ export const CrossChainAmountEntry: React.FC = ({
Please enter a valid amount in {selectedTokenData?.symbol || "USDC"} greater than an - equivalent of 0.1 NEAR - {" "}(min: {((0.1 * nearPrice) / price).toFixed(4)} {selectedTokenData?.symbol || "USDC"}). + equivalent of 0.1 NEAR (min: {((0.1 * nearPrice) / price).toFixed(4)}{" "} + {selectedTokenData?.symbol || "USDC"}). -
- )} + {isCrossChainToken && + crossChainMinAmount !== undefined && + crossChainMinAmount > 0 && + amount !== undefined && + parseFloat(amount.toString()) > 0 && + parseFloat(amount.toString()) < crossChainMinAmount && ( +
+ + Minimum amount is{" "} + + {crossChainMinAmount.toFixed(4)} {crossChainTokenSymbol || "tokens"} + {" "} + (equivalent to 0.1 NEAR). + + +
+ )} ); diff --git a/src/features/donation/hooks/form.ts b/src/features/donation/hooks/form.ts index 21aa1936..db2a2d32 100644 --- a/src/features/donation/hooks/form.ts +++ b/src/features/donation/hooks/form.ts @@ -18,7 +18,6 @@ import { extractMatchingPots } from "@/entities/pot"; import { useDispatch } from "@/store/hooks"; import { useCrossChainTokens } from "./cross-chain-tokens"; - import { DONATION_DEFAULT_MIN_AMOUNT_FLOAT, DONATION_INSUFFICIENT_BALANCE_ERROR, @@ -162,6 +161,7 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams const parts = values.tokenId.split(":"); const blockchain = parts[0]; const assetId = parts.slice(1).join(":"); + const selectedToken = crossChainTokenList.find( (t) => t.assetId === assetId && t.blockchain.toLowerCase() === blockchain.toLowerCase(), ); @@ -178,6 +178,7 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams if (crossChainTokenPrice > 0 && crossChainNearPrice > 0) { return (0.1 * crossChainNearPrice) / crossChainTokenPrice; } + return 0; }, [crossChainNearPrice, crossChainTokenPrice]);