Skip to content

Commit a4f0df3

Browse files
authored
Merge pull request #593 from PotLock/staging
Staging to prod
2 parents 26942f5 + 75477c3 commit a4f0df3

File tree

4 files changed

+126
-8
lines changed

4 files changed

+126
-8
lines changed

src/features/donation/components/cross-chain-amount-entry.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,10 +488,24 @@ export const CrossChainAmountEntry: React.FC<CrossChainAmountEntryProps> = ({
488488
</div>
489489
</div>
490490
</div>
491-
{isDisabled && (
492-
<div className="text-sm text-red-500">
493-
Please enter a valid amount in {selectedTokenData?.symbol || "USDC"} greater than an
494-
equivalent of 0.1 NEAR.
491+
{isDisabled && price > 0 && nearPrice > 0 && (
492+
<div className="flex items-center gap-2 text-sm text-red-500">
493+
<span>
494+
Please enter a valid amount in {selectedTokenData?.symbol || "USDC"} greater than an
495+
equivalent of 0.1 NEAR (min: {((0.1 * nearPrice) / price).toFixed(4)}{" "}
496+
{selectedTokenData?.symbol || "USDC"}).
497+
</span>
498+
<button
499+
type="button"
500+
title="Set to minimum amount"
501+
className="inline-flex shrink-0 items-center justify-center rounded-md border border-red-300 bg-red-50 px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-100 hover:text-red-700"
502+
onClick={() => {
503+
const minAmount = ((0.1 * nearPrice) / price) * 1.01; // add 1% buffer to safely clear the threshold
504+
form.setValue("amount", parseFloat(minAmount.toFixed(4)));
505+
}}
506+
>
507+
✏️ Update
508+
</button>
495509
</div>
496510
)}
497511
{/* Action Button */}

src/features/donation/components/modal-content.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,16 @@ export const DonationModalContent: React.FC<DonationModalContentProps> = ({
8686
const { isLoading: isDonationConfigLoading, data: donationConfig } =
8787
donationContractHooks.useConfig();
8888

89-
const { form, matchingPots, isDisabled, onSubmit, totalAmountFloat, isGroupDonation } =
90-
useDonationForm(props);
89+
const {
90+
form,
91+
matchingPots,
92+
isDisabled,
93+
onSubmit,
94+
totalAmountFloat,
95+
isGroupDonation,
96+
crossChainMinAmount,
97+
crossChainTokenSymbol,
98+
} = useDonationForm(props);
9199

92100
const isCampaignDonation = "campaignId" in props;
93101
const isPotDonation = "potId" in props;
@@ -141,6 +149,8 @@ export const DonationModalContent: React.FC<DonationModalContentProps> = ({
141149
matchingPots={matchingPots}
142150
{...props}
143151
onTokenDataChange={setSelectedTokenData}
152+
crossChainMinAmount={crossChainMinAmount}
153+
crossChainTokenSymbol={crossChainTokenSymbol}
144154
/>
145155
);
146156
} else if ("potId" in props || "listId" in props) {

src/features/donation/components/single-recipient-allocation.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ export type DonationSingleRecipientAllocationProps = Partial<ByAccountId> &
4040
DonationAllocationInputs & {
4141
matchingPots?: Pot[];
4242
onTokenDataChange?: (data: { blockchain: string; tokenData?: any } | null) => void;
43+
crossChainMinAmount?: number;
44+
crossChainTokenSymbol?: string;
4345
};
4446

4547
export const DonationSingleRecipientAllocation: React.FC<
4648
DonationSingleRecipientAllocationProps
47-
> = ({ form, accountId, matchingPots, campaignId, onTokenDataChange }) => {
49+
> = ({
50+
form,
51+
accountId,
52+
matchingPots,
53+
campaignId,
54+
onTokenDataChange,
55+
crossChainMinAmount,
56+
crossChainTokenSymbol,
57+
}) => {
4858
const walletUser = useWalletUserSession();
4959

5060
const [selectedTokenData, setSelectedTokenData] = useState<{
@@ -332,6 +342,35 @@ export const DonationSingleRecipientAllocation: React.FC<
332342
)}
333343
/>
334344
)}
345+
346+
{/* Cross-chain minimum amount warning with quick-fix button */}
347+
{isCrossChainToken &&
348+
crossChainMinAmount !== undefined &&
349+
crossChainMinAmount > 0 &&
350+
amount !== undefined &&
351+
parseFloat(amount.toString()) > 0 &&
352+
parseFloat(amount.toString()) < crossChainMinAmount && (
353+
<div className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-600">
354+
<span>
355+
Minimum amount is{" "}
356+
<strong>
357+
{crossChainMinAmount.toFixed(4)} {crossChainTokenSymbol || "tokens"}
358+
</strong>{" "}
359+
(equivalent to 0.1 NEAR).
360+
</span>
361+
<button
362+
type="button"
363+
title="Set to minimum amount"
364+
className="inline-flex shrink-0 items-center justify-center rounded-md border border-red-300 bg-white px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-100 hover:text-red-700"
365+
onClick={() => {
366+
const minAmount = crossChainMinAmount * 1.01; // add 1% buffer
367+
form.setValue("amount", parseFloat(minAmount.toFixed(4)));
368+
}}
369+
>
370+
✏️ Update
371+
</button>
372+
</div>
373+
)}
335374
</DialogDescription>
336375
</>
337376
);

src/features/donation/hooks/form.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useFungibleToken } from "@/entities/_shared/token";
1717
import { extractMatchingPots } from "@/entities/pot";
1818
import { useDispatch } from "@/store/hooks";
1919

20+
import { useCrossChainTokens } from "./cross-chain-tokens";
2021
import {
2122
DONATION_DEFAULT_MIN_AMOUNT_FLOAT,
2223
DONATION_INSUFFICIENT_BALANCE_ERROR,
@@ -145,6 +146,42 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams
145146
enabled: !isCrossChainToken,
146147
});
147148

149+
// Fetch cross-chain token list to get NEAR price and selected token price
150+
const { data: crossChainTokenList } = useCrossChainTokens();
151+
152+
const { crossChainNearPrice, crossChainTokenPrice, crossChainTokenSymbol } = useMemo(() => {
153+
if (!isCrossChainToken || !crossChainTokenList || !values.tokenId) {
154+
return { crossChainNearPrice: 0, crossChainTokenPrice: 0, crossChainTokenSymbol: "" };
155+
}
156+
157+
const nearToken = crossChainTokenList.find(
158+
(t) => t.symbol === "wNEAR" || t.assetId === "nep141:wrap.near",
159+
);
160+
161+
const parts = values.tokenId.split(":");
162+
const blockchain = parts[0];
163+
const assetId = parts.slice(1).join(":");
164+
165+
const selectedToken = crossChainTokenList.find(
166+
(t) => t.assetId === assetId && t.blockchain.toLowerCase() === blockchain.toLowerCase(),
167+
);
168+
169+
return {
170+
crossChainNearPrice: nearToken?.price ?? 0,
171+
crossChainTokenPrice: selectedToken?.price ?? 0,
172+
crossChainTokenSymbol: selectedToken?.symbol ?? "",
173+
};
174+
}, [isCrossChainToken, crossChainTokenList, values.tokenId]);
175+
176+
// Minimum amount in the selected cross-chain token equivalent to 0.1 NEAR
177+
const crossChainMinAmount = useMemo(() => {
178+
if (crossChainTokenPrice > 0 && crossChainNearPrice > 0) {
179+
return (0.1 * crossChainNearPrice) / crossChainTokenPrice;
180+
}
181+
182+
return 0;
183+
}, [crossChainNearPrice, crossChainTokenPrice]);
184+
148185
const { data: pot } = indexer.usePot({
149186
enabled: isGroupPotDonation || isSingleRecipientPotDonation,
150187
potId: groupDonationPotId ?? values.potAccountId ?? NOOP_STRING,
@@ -296,8 +333,21 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams
296333
}
297334
}
298335

336+
//* Cross-chain minimum amount validation (0.1 NEAR equivalent)
337+
else if (
338+
isCrossChainToken &&
339+
crossChainMinAmount > 0 &&
340+
Big(parsedAmount).lt(crossChainMinAmount)
341+
) {
342+
const errorMessage = `Amount must be at least ${crossChainMinAmount.toFixed(4)} ${crossChainTokenSymbol || "tokens"} (equivalent to 0.1 NEAR).`;
343+
344+
if (customErrors?.amount?.message !== errorMessage || self.formState.isValid) {
345+
setCustomErrors({ amount: { message: errorMessage } });
346+
}
347+
}
348+
299349
//* Addressing single-recipient and group donation scenarios with evenly distributed funds
300-
//* Skip minimum amount validation for cross-chain tokens (they don't have min requirements)
350+
//* Skip minimum amount validation for cross-chain tokens (handled above)
301351
else if (
302352
!isCrossChainToken &&
303353
minTotalAmountFloat !== undefined &&
@@ -344,6 +394,8 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams
344394
}
345395
}, [
346396
customErrors,
397+
crossChainMinAmount,
398+
crossChainTokenSymbol,
347399
isCrossChainToken,
348400
isFtDonation,
349401
isGroupDonation,
@@ -375,5 +427,8 @@ export const useDonationForm = ({ cachedTokenId, ...params }: DonationFormParams
375427
// TODO: Likely not needed to be exposed anymore, try using `amount` everywhere
376428
// TODO: in the consuming code instead and remove this if no issues detected.
377429
totalAmountFloat,
430+
crossChainMinAmount,
431+
crossChainTokenSymbol,
432+
isCrossChainToken,
378433
};
379434
};

0 commit comments

Comments
 (0)