Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reward aggregator #875

Merged
merged 11 commits into from
Oct 14, 2024
194 changes: 91 additions & 103 deletions components/discover/claimModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,145 +10,114 @@ import AppIcon from "./appIcon";
import TokenIcon from "./tokenIcon";
import { useNotification } from "@context/NotificationProvider";
import Loading from "@app/loading";
import { useAccount, useContractWrite } from "@starknet-react/core";
import { RewardsPerProtocol } from "../../types/backTypes";
import { getRewards } from "@services/apiService";
import { gweiToEth } from "@utils/feltService";

type RewardItem = {
appName: string,
currencies: { currencyName: string, value: number }[]
}
appName: string;
currencies: { currencyName: string; value: number }[];
};

type CurrencyRowProps = {
currencyName: string;
currencyValue: number;
}
};

type ClaimModalProps = {
closeModal: () => void;
showSuccess: () => void;
open: boolean;
};

const RewardComponent: FunctionComponent<RewardItem> = ({ appName, currencies }) => (
<div className="flex w-full justify-between items-center bg-background px-2 py-3 my-1 rounded-lg">
const RewardComponent: FunctionComponent<RewardItem> = ({
appName,
currencies,
}) => (
<div className="flex w-full justify-between items-center bg-background px-4 py-3 my-1 rounded-lg">
<div className="flex flex-row gap-4">
<AppIcon app={appName} />
<AppIcon
app={appName}
imageDimensions={{
width: 25,
height: 25,
}}
customStyle={{
border: ".5px solid #fff",
}}
/>
<Typography type={TEXT_TYPE.BODY_MIDDLE}>
{appName}
<span className="capitalize">{appName}</span>
</Typography>
</div>
<div className="flex w-fit flex-col items-end">
{currencies.map((currency, idx) => (
<CurrencyRow key={idx} currencyName={currency.currencyName} currencyValue={currency.value} />
<CurrencyRow
key={idx}
currencyName={currency.currencyName}
currencyValue={currency.value}
/>
Marchand-Nicolas marked this conversation as resolved.
Show resolved Hide resolved
))}
</div>
</div>
);

const CurrencyRow: FunctionComponent<CurrencyRowProps> = ({ currencyName, currencyValue }) => (
const CurrencyRow: FunctionComponent<CurrencyRowProps> = ({
currencyName,
currencyValue,
}) => (
<div className="flex flex-row items-center gap-4">
<Typography type={TEXT_TYPE.BODY_SMALL}>
{currencyValue < 1000 ? currencyValue : `${currencyValue / 1000}K`}
</Typography>
<Typography type={TEXT_TYPE.BODY_SMALL}>
{currencyName}
</Typography>
<Typography type={TEXT_TYPE.BODY_SMALL}>{currencyName}</Typography>
<TokenIcon token={currencyName} />
</div>
);

const ClaimModal: FunctionComponent<ClaimModalProps> = ({
closeModal,
showSuccess,
open
open,
}) => {
const [claimRewards, setClaimRewards] = useState<RewardItem[]>();
const [loading, setLoading] = useState<boolean>(true);
const { showNotification } = useNotification();
const { address } = useAccount();
const [rewards, setRewards] = useState<RewardsPerProtocol | null>(null);
const [calls, setCalls] = useState<Call[]>([]);
Marchand-Nicolas marked this conversation as resolved.
Show resolved Hide resolved
const { writeAsync: execute } = useContractWrite({
calls: calls,
});

const getClaimRewards = useCallback(async () => {
// TODO: Implement fetch from backend. Returning mock values.
try {
setLoading(true);
const rewards = [
{
appName: "EKUBO",
currencies: [
{ currencyName: "STRK", value: 11570 }
],
},
{
appName: "NOSTRA",
currencies: [
{ currencyName: "STRK", value: 12.124 },
{ currencyName: "ETH", value: 1.1245 }
],
},
{
appName: "zkLend",
currencies: [
{ currencyName: "USDT", value: 124.12 }
],
},
{
appName: "VESU",
currencies: [
{ currencyName: "STRK", value: 36 }
],
},
{
appName: "Nimbora",
currencies: [
{ currencyName: "STRK", value: 70.145 }
],
},
{
appName: "zkLend",
currencies: [
{ currencyName: "USDT", value: 124.12 }
],
},
{
appName: "VESU",
currencies: [
{ currencyName: "STRK", value: 36 }
],
},
{
appName: "Nimbora",
currencies: [
{ currencyName: "STRK", value: 70.145 }
],
},
];
const res = await new Promise<RewardItem[]>(resolve => setTimeout(() => resolve(rewards), 2000));
setClaimRewards(res);
setLoading(false);
} catch (error) {
setLoading(false);
showNotification("Error while fetching rewards", "error");
console.log("Error while fetching rewards", error);
}
}, []);
useEffect(() => {
if (!address) return;
getRewards(address)
.then((res) => {
setRewards(res?.rewards);
setCalls(res?.calls);
setLoading(false);
})
.catch((error) => {
showNotification("Error while fetching rewards", "error");
console.log("Error while fetching rewards", error);
});
}, [address]);
Comment on lines +93 to +105
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure loading state is properly managed in error scenarios

In the useEffect hook, if an error occurs while fetching rewards, the setLoading(false) function is not called. This may cause the loading spinner to remain indefinitely.

Apply this diff to set the loading state in the catch block:

  .catch((error) => {
    showNotification("Error while fetching rewards", "error");
    console.log("Error while fetching rewards", error);
+   setLoading(false);
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!address) return;
getRewards(address)
.then((res) => {
setRewards(res?.rewards);
setCalls(res?.calls);
setLoading(false);
})
.catch((error) => {
showNotification("Error while fetching rewards", "error");
console.log("Error while fetching rewards", error);
});
}, [address]);
useEffect(() => {
if (!address) return;
getRewards(address)
.then((res) => {
setRewards(res?.rewards);
setCalls(res?.calls);
setLoading(false);
})
.catch((error) => {
showNotification("Error while fetching rewards", "error");
console.log("Error while fetching rewards", error);
setLoading(false);
});
}, [address]);


const doClaimRewards = useCallback(async () => {
// TODO: Implement logic to claim rewards
try {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 2000));
await execute();
setLoading(false);
closeModal();
showSuccess();
} catch (error) {
setLoading(false);
showNotification("Error while claiming rewards", "error");
console.log("Error while claiming rewards", error);
console.log("Error while claiming rewards", error, calls);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid logging sensitive data in the console

Including calls in the console log may expose sensitive information. It's recommended to log only the error.

Apply this diff to remove calls from the log statement:

- console.log("Error while claiming rewards", error, calls);
+ console.log("Error while claiming rewards", error);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log("Error while claiming rewards", error, calls);
console.log("Error while claiming rewards", error);

}
}, []);

useEffect(() => {
if (open) {
getClaimRewards();
}
}, [open, getClaimRewards]);
}, [calls]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update dependency array to include all external dependencies

The dependency array for doClaimRewards is missing some external variables used within the function. This may lead to stale closures if those variables change.

Apply this diff to include all necessary dependencies:

- }, [calls]);
+ }, [calls, execute, closeModal, showSuccess]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, [calls]);
}, [calls, execute, closeModal, showSuccess]);


return (
<Modal
Expand All @@ -157,14 +126,19 @@ const ClaimModal: FunctionComponent<ClaimModalProps> = ({
aria-label="Reward claim success modal"
open={open}
>
<div className={`${styles.popup} !overflow-y-hidden !rounded-2xl !mt-0 !px-1 !py-1 md:!px-0 md:!py-0`}>
<div
className={`${styles.popup} !overflow-y-hidden !rounded-2xl !mt-0 !px-1 !py-1 md:!px-0 md:!py-0`}
>
<Loading isLoading={loading} loadingType="spinner">
<div className={`${styles.popupContent} !px-0 !pt-6 !pb-0 md:!px-12 md:!pt-14`}>
<div
className={`${styles.popupContent} !px-0 !pt-6 !pb-0 md:!px-12 md:!pt-14`}
>
<div className="flex w-full flex-col self-start">
<div className="flex w-full lg:flex-row flex-col-reverse lg:items-start items-center justify-between gap-4 md:pb-2">
<div className="flex flex-col gap-4 lg:text-left text-center">
<Typography type={TEXT_TYPE.BODY_MIDDLE}>
Collect your rewards from all supported protocols on Starknet
Collect your rewards from all supported protocols on
Starknet
</Typography>
<Typography type={TEXT_TYPE.H4}>
Claim Your Rewards
Expand All @@ -178,25 +152,39 @@ const ClaimModal: FunctionComponent<ClaimModalProps> = ({
style={{ transform: "rotateY(190deg)" }}
/>
</div>
<div className="flex w-full flex-col mt-4 max-h-80 overflow-auto">
{!claimRewards ?
<Typography type={TEXT_TYPE.BODY_DEFAULT}>No rewards available</Typography>
:
claimRewards.map((item, index) => (
<RewardComponent key={index} appName={item.appName} currencies={item.currencies} />
))}
<div className="flex w-full flex-col py-4 max-h-80 overflow-auto">
{rewards && calls.length > 0 ? (
Object.entries(rewards)
.filter(([key, rewardList]) => key && rewardList.length > 0)
.map(([key, rewardList]) => (
<RewardComponent
key={key}
appName={key}
currencies={rewardList.map((reward) => ({
currencyName: reward.token_symbol,
value:
Math.round(
parseFloat(gweiToEth(reward.amount)) * 100
) / 100,
}))}
/>
))
) : (
<Typography type={TEXT_TYPE.BODY_DEFAULT}>
No rewards available
</Typography>
)}
Marchand-Nicolas marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</div>
<div className={`${styles.bottomContent} !gap-6 !py-6 !px-5`}>
<div className="flex w-full justify-between items-center">
<button onClick={closeModal} aria-label="Cancel claiming rewards">
<Typography type={TEXT_TYPE.BODY_MIDDLE}>
Cancel
</Typography>
<Typography type={TEXT_TYPE.BODY_MIDDLE}>Cancel</Typography>
</button>
<div className="w-fit">
<Button disabled={claimRewards ? false : true}
<Button
disabled={!calls || calls.length === 0 || loading}
onClick={doClaimRewards}
>
Claim all
Expand All @@ -210,4 +198,4 @@ const ClaimModal: FunctionComponent<ClaimModalProps> = ({
);
};

export default ClaimModal;
export default ClaimModal;
Loading