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

MEP 25/10/24 #907

Open
wants to merge 36 commits into
base: mainnet
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2debc7a
feat: add Opus protocol information (#897)
KevinLatino Oct 21, 2024
2cff6f8
Rename opus.ico to favicon.ico for standard favicon naming convention
KevinLatino Oct 22, 2024
50188ab
Merge pull request #898 from KevinLatino/feat/897-add-opus-protocol-info
fricoben Oct 23, 2024
f033f4b
fixing jotai persistent state
ikemHood Oct 24, 2024
5d771c4
fix: bug image fix
Otaiki1 Oct 24, 2024
134b7be
Merge pull request #905 from Otaiki1/Ot-dev-fix
fricoben Oct 25, 2024
4377f0e
fix boost quest progress display (#902)
Akshola00 Oct 25, 2024
c559051
fix: Add haiko_solvers to fix redirects issue (#900)
Abeeujah Oct 25, 2024
866c9a1
Update quizStep.tsx (#899)
GradleD Oct 25, 2024
286d3f1
feat: Add keplr wallet #901 (#906)
anonfedora Oct 25, 2024
749d5cb
Added okx Wallet
Oct 25, 2024
4db88f4
feat: Added okx Wallet
Emmanex01 Oct 25, 2024
3d4492a
feat: add API service, add fetch logic & add components
joeperpetua Oct 27, 2024
8d5b875
feat: make components responsive
joeperpetua Oct 27, 2024
44f3ba8
feat: rework Argent service & improve chart visuals
joeperpetua Oct 27, 2024
54b23ec
refactor: revert changes to appIcon.tsx and imports
joeperpetua Oct 27, 2024
43bda1a
refactor: rabbit suggestions
joeperpetua Oct 27, 2024
7dff03b
refactor: add tests & standarize tokenToDecimal fn
joeperpetua Oct 28, 2024
64b8bc4
Merge pull request #910 from Emmanex01/added-okxwallet
Marchand-Nicolas Oct 28, 2024
23fd51a
added wallet bitget
jaiminRaiyani Oct 28, 2024
d3feb7e
Merge pull request #912 from joeperpetua/feat/add-portfolio-dashboard
Marchand-Nicolas Oct 28, 2024
2b501e9
changed wallet links
jaiminRaiyani Oct 28, 2024
ff7ca95
feat: add delete question function
JosueBrenes Oct 28, 2024
e433dac
feat: add delete question button
JosueBrenes Oct 28, 2024
c196c10
added svg of bitget wallet in walleticons.tsx
jaiminRaiyani Oct 29, 2024
66d08e1
Merge pull request #904 from ikemHood/testnet
Marchand-Nicolas Oct 29, 2024
bf8e14a
Refactor wallet ID checks with INSTALLABLE_WALLETS constant
jaiminRaiyani Oct 29, 2024
77b49c8
feat: Changes in the delete question btn
JosueBrenes Oct 29, 2024
5ee98a6
feat: Changes in the delete question btn
JosueBrenes Oct 29, 2024
f2b90f7
Merge pull request #914 from JosueBrenes/feat/delete-question
Marchand-Nicolas Oct 29, 2024
2c92ce7
reward claim function added in dashboard
Aditya-040 Oct 30, 2024
a7fc942
edited pr removed copied components and package lock file
Aditya-040 Oct 31, 2024
e7a1df8
Restore package-lock.json without including it in the PR
Aditya-040 Oct 31, 2024
ed21905
Merge branch 'testnet' into feat/add-bitget-wallet-support
jaiminRaiyani Oct 31, 2024
a8f8ae8
Merge pull request #915 from jaiminRaiyani/feat/add-bitget-wallet-sup…
Marchand-Nicolas Oct 31, 2024
53069b8
Merge pull request #919 from Aditya-040/claim_reward
Marchand-Nicolas Oct 31, 2024
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
216 changes: 214 additions & 2 deletions app/[addressOrDomain]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { useAccount } from "@starknet-react/core";
import Blur from "@components/shapes/blur";
import { utils } from "starknetid.js";
import { StarknetIdJsContext } from "@context/StarknetIdJsProvider";
import { hexToDecimal } from "@utils/feltService";
import { hexToDecimal, tokenToDecimal } from "@utils/feltService";
import { isHexString, minifyAddress } from "@utils/stringService";
import ProfileCardSkeleton from "@components/skeletons/profileCardSkeleton";
import { getDataFromId } from "@services/starknetIdService";
import { usePathname, useRouter } from "next/navigation";
import ErrorScreen from "@components/UI/screens/errorScreen";
import { CompletedQuests } from "../../types/backTypes";
import { ArgentDappMap, ArgentTokenMap, ArgentUserDapp, ArgentUserToken, CompletedQuests } from "../../types/backTypes";
import QuestSkeleton from "@components/skeletons/questsSkeleton";
import QuestCardCustomised from "@components/dashboard/CustomisedQuestCard";
import QuestStyles from "@styles/Home.module.css";
Expand All @@ -31,16 +31,34 @@ import { TEXT_TYPE } from "@constants/typography";
import { a11yProps } from "@components/UI/tabs/a11y";
import { CustomTabPanel } from "@components/UI/tabs/customTab";
import SuggestedQuests from "@components/dashboard/SuggestedQuests";
import PortfolioSummary from "@components/dashboard/PortfolioSummary";
import { useNotification } from "@context/NotificationProvider";
import { calculateTokenPrice, fetchDapps, fetchTokens, fetchUserDapps, fetchUserTokens } from "@services/argentPortfolioService";
import PortfolioSummarySkeleton from "@components/skeletons/portfolioSummarySkeleton";

type AddressOrDomainProps = {
params: {
addressOrDomain: string;
};
};

type ChartItemMap = {
[dappId: string]: ChartItem
};

type DebtStatus = {
hasDebt: boolean;
tokens: {
dappId: string,
tokenAddress: string,
tokenBalance: number
}[];
};

export default function Page({ params }: AddressOrDomainProps) {
const router = useRouter();
const addressOrDomain = params.addressOrDomain;
const { showNotification } = useNotification();
const { address } = useAccount();
const { starknetIdNavigator } = useContext(StarknetIdJsContext);
const [initProfile, setInitProfile] = useState(false);
Expand All @@ -62,6 +80,9 @@ export default function Page({ params }: AddressOrDomainProps) {
const [questsLoading, setQuestsLoading] = useState(true);
const [tabIndex, setTabIndex] = React.useState(0);
const [claimableQuests, setClaimableQuests] = useState<Boost[]>([]);
const [portfolioAssets, setPortfolioAssets] = useState<ChartItem[]>([]);
const [portfolioProtocols, setPortfolioProtocols] = useState<ChartItem[]>([]);
const [loadingProtocols, setLoadingProtocols] = useState(true);

const handleChangeTab = useCallback(
(event: React.SyntheticEvent, newValue: number) => {
Expand Down Expand Up @@ -168,10 +189,177 @@ export default function Page({ params }: AddressOrDomainProps) {
setQuestsLoading(false);
}, []);

const fetchPortfolioAssets = useCallback(async (addr: string) => {

// TODO: Implement fetch from Argent API
const assets = [
{ color: "#1E2097", itemLabel: "USDC", itemValue: "46.68", itemValueSymbol: "%" },
{ color: "#637DEB", itemLabel: "USDT", itemValue: "27.94", itemValueSymbol: "%" },
{ color: "#2775CA", itemLabel: "STRK", itemValue: "22.78", itemValueSymbol: "%" },
{ color: "#5CE3FE", itemLabel: "ETH", itemValue: "0.36", itemValueSymbol: "%" },
{ color: "#F4FAFF", itemLabel: "Others", itemValue: "2.36", itemValueSymbol: "%" },
];
setPortfolioAssets(assets);

}, []);

const userHasDebt = (userDapps: ArgentUserDapp[]) => {
let debt: DebtStatus = { hasDebt: false, tokens: [] };

for (const dapp of userDapps) {
if (!dapp.products[0]) { continue; }
for (const position of dapp.products[0].positions) {
for (const tokenAddress of Object.keys(position.totalBalances)) {
const tokenBalance = Number(position.totalBalances[tokenAddress]);
if (tokenBalance < 0) {
debt.hasDebt = true;
debt.tokens.push({dappId: dapp.dappId, tokenAddress, tokenBalance});
}
}
}
}
return debt;
};

const handleDebt = async (protocolsMap: ChartItemMap, userDapps: ArgentUserDapp[], tokens: ArgentTokenMap) => {
const debtStatus = userHasDebt(userDapps);
if (!debtStatus || !debtStatus.hasDebt) { return; }

for await (const debt of debtStatus.tokens) {
let value = Number(protocolsMap[debt.dappId].itemValue);
value += await calculateTokenPrice(
debt.tokenAddress,
tokenToDecimal(debt.tokenBalance.toString(),
tokens[debt.tokenAddress].decimals),
"USD"
);

protocolsMap[debt.dappId].itemValue = value.toFixed(2);
}
};

const getProtocolsFromTokens = async (protocolsMap: ChartItemMap, userTokens: ArgentUserToken[], tokens: ArgentTokenMap, dapps: ArgentDappMap) => {
for await (const token of userTokens) {
const tokenInfo = tokens[token.tokenAddress];
if (tokenInfo.dappId && token.tokenBalance != "0") {
let itemValue = 0;
const currentTokenBalance = await calculateTokenPrice(token.tokenAddress, tokenToDecimal(token.tokenBalance, tokenInfo.decimals), "USD");

if (protocolsMap[tokenInfo.dappId]?.itemValue) {
itemValue = Number(protocolsMap[tokenInfo.dappId].itemValue) + currentTokenBalance;
} else {
itemValue = currentTokenBalance;
}

protocolsMap[tokenInfo.dappId] = {
color: "",
itemLabel: dapps[tokenInfo.dappId].name,
itemValueSymbol: "$",
itemValue: itemValue.toFixed(2)
}
}
}
}

const getProtocolsFromDapps = async (protocolsMap: ChartItemMap, userDapps: ArgentUserDapp[], tokens: ArgentTokenMap, dapps: ArgentDappMap) => {
for await (const userDapp of userDapps) {
if (protocolsMap[userDapp.dappId]) { continue; } // Ignore entry if already present in the map

let protocolBalance = 0;
if (!userDapp.products[0]) { return; }
for await (const position of userDapp.products[0].positions) {
for await (const tokenAddress of Object.keys(position.totalBalances)) {
protocolBalance += await calculateTokenPrice(
tokenAddress,
tokenToDecimal(position.totalBalances[tokenAddress], tokens[tokenAddress].decimals),
"USD"
);
}
}

protocolsMap[userDapp.dappId] = {
color: "",
itemLabel: dapps[userDapp.dappId].name,
itemValueSymbol: "$",
itemValue: protocolBalance.toFixed(2)
}
}
}

const sortProtocols = (protocolsMap: ChartItemMap) => {
return Object.values(protocolsMap).sort((a, b) => parseFloat(b.itemValue) - parseFloat(a.itemValue));
}

const handleExtraProtocols = (sortedProtocols: ChartItem[]) => {
let otherProtocols = sortedProtocols.length > 5 ? sortedProtocols.splice(4) : [];
if (otherProtocols.length === 0) { return;}
sortedProtocols.push({
itemLabel: "Others",
itemValue: otherProtocols.reduce((valueSum, protocol) => valueSum + Number(protocol.itemValue), 0).toFixed(2),
itemValueSymbol: "$",
color: ""
});
}

const assignProtocolColors = (sortedProtocols: ChartItem[]) => {
const portfolioProtocolColors = [
"#278015",
"#23F51F",
"#DEFE5C",
"#9EFABB",
"#F4FAFF"
];
sortedProtocols.forEach((protocol, index) => {
protocol.color = portfolioProtocolColors[index];
});
}

const fetchPortfolioProtocols = useCallback(async (addr: string) => {
let dapps: ArgentDappMap = {};
let tokens: ArgentTokenMap = {};
let userTokens: ArgentUserToken[] = [];
let userDapps: ArgentUserDapp[] = [];

setLoadingProtocols(true);
try {
[dapps, tokens, userTokens, userDapps] = await Promise.all([
fetchDapps(),
fetchTokens(),
fetchUserTokens(addr),
fetchUserDapps(addr)
]);
} catch (error) {
showNotification("Error while fetching address portfolio", "error");
console.log("Error while fetching address portfolio", error);
}

if (!dapps || !tokens || (!userTokens && !userDapps)) return;
let protocolsMap: ChartItemMap = {};

try {
await getProtocolsFromTokens(protocolsMap, userTokens, tokens, dapps);
await handleDebt(protocolsMap, userDapps, tokens); // Tokens show debt as balance 0, so need to handle it manually
await getProtocolsFromDapps(protocolsMap, userDapps, tokens, dapps);

let sortedProtocols = sortProtocols(protocolsMap);
handleExtraProtocols(sortedProtocols);
assignProtocolColors(sortedProtocols);

setPortfolioProtocols(sortedProtocols);
} catch (error) {
showNotification("Error while calculating address portfolio stats", "error");
console.log("Error while calculating address portfolio stats", error);
}

setLoadingProtocols(false);
}, []);

useEffect(() => {
if (!identity) return;
fetchQuestData(identity.owner);
fetchPageData(identity.owner);
fetchPortfolioAssets(identity.owner);
fetchPortfolioProtocols(identity.owner);
}, [identity]);

useEffect(() => setNotFound(false), [dynamicRoute]);
Expand Down Expand Up @@ -325,6 +513,30 @@ export default function Page({ params }: AddressOrDomainProps) {
)}
</div>

{/* Portfolio charts */}
<div className={styles.dashboard_portfolio_summary_container}>
{loadingProtocols ? ( // Change for corresponding state
<PortfolioSummarySkeleton />
) : (
<PortfolioSummary
title="Portfolio by assets type"
data={portfolioAssets}
totalBalance={portfolioAssets.reduce((sum, item) => sum + Number(item.itemValue), 0)}
isProtocol={false}
/>
)}
{loadingProtocols ? (
<PortfolioSummarySkeleton />
) : (
<PortfolioSummary
title="Portfolio by protocol usage"
data={portfolioProtocols}
totalBalance={portfolioProtocols.reduce((sum, item) => sum + Number(item.itemValue), 0)}
isProtocol={true}
/>
)}
</div>

{/* Completed Quests */}
<div className={styles.dashboard_completed_tasks_container}>
<div>
Expand Down
5 changes: 5 additions & 0 deletions app/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ if (typeof window !== "undefined") {
export const availableConnectors = [
new InjectedConnector({ options: { id: "braavos", name: "Braavos" } }),
new InjectedConnector({ options: { id: "argentX", name: "Argent X" } }),
new InjectedConnector({ options: { id: "bitkeep", name: "Bitget Wallet" } }),
new InjectedConnector({ options: { id: "okxwallet", name: "Okx Wallet" } }), // Added okxwallet

new WebWalletConnector({
url:
getCurrentNetwork() === "TESTNET"
Expand All @@ -49,6 +52,8 @@ export const availableConnectors = [
chainId: constants.NetworkName.SN_MAIN,
icons: ["https://starknet.quest/visuals/starknetquestLogo.svg"],
}),

new InjectedConnector({ options: { id: "keplr", name: "Keplr" } }),
];

export function Providers({ children }: { children: React.ReactNode }) {
Expand Down
25 changes: 20 additions & 5 deletions components/UI/changeWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import useGetDiscoveryWallets from "@hooks/useGetDiscoveryWallets";
import Typography from "./typography/typography";
import { TEXT_TYPE } from "@constants/typography";

// Define an array of wallet IDs with explicit types
const INSTALLABLE_WALLETS = ["braavos", "argentX", "bitkeep"] as const;
type WalletId = typeof INSTALLABLE_WALLETS[number];

type ChangeWalletProps = {
closeWallet: () => void;
hasWallet: boolean;
Expand Down Expand Up @@ -53,16 +57,21 @@ const ChangeWallet: FunctionComponent<ChangeWalletProps> = ({
></path>
</svg>
</button>
<Typography type={TEXT_TYPE.BODY_NORMAL} color="secondary" className={styles.menu_title}>Change wallet</Typography>
<Typography
type={TEXT_TYPE.BODY_NORMAL}
color="secondary"
className={styles.menu_title}
>
Change wallet
</Typography>
{connectors.map((connector) => {
if (connector.available()) {
return (
<div className="mt-5 flex justify-center" key={connector.id}>
<Button onClick={() => connectWallet(connector)}>
<div className="flex justify-center items-center">
<WalletIcons id={connector.id} />
{connector.id === "braavos" ||
connector.id === "argentX" ||
{INSTALLABLE_WALLETS.includes(connector.id as WalletId) ||
connector.id === "okxwallet"
? `Connect ${connector.name}`
: "Login with Email"}
Expand All @@ -71,7 +80,7 @@ const ChangeWallet: FunctionComponent<ChangeWalletProps> = ({
</div>
);
} else {
if (connector.id === "braavos" || connector.id === "argentX") {
if (INSTALLABLE_WALLETS.includes(connector.id as WalletId)) {
return (
<div className="mt-5 flex justify-center" key={connector.id}>
<Button
Expand All @@ -95,11 +104,17 @@ const ChangeWallet: FunctionComponent<ChangeWalletProps> = ({
}
}
})}
<Typography color="secondary" type={TEXT_TYPE.BODY_MIDDLE} onClick={() => closeWallet()} className={styles.closeMobile}>
<Typography
color="secondary"
type={TEXT_TYPE.BODY_MIDDLE}
onClick={() => closeWallet()}
className={styles.closeMobile}
>
Close
</Typography>
</div>
</Modal>
);
};

export default ChangeWallet;
Loading