diff --git a/package.json b/package.json index 642cf87..6253ff3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@cowprotocol/cow-sdk": "^5.10.3", - "@kleros/ui-components-library": "^3.6.0", + "@kleros/ui-components-library": "^3.7.0", "@reown/appkit": "^1.7.11", "@reown/appkit-adapter-wagmi": "^1.7.11", "@swapr/sdk": "https://github.com/seer-pm/swapr-sdk#6dea7e63f7e05c84a4374717ee1ad5baca86f7de", @@ -21,6 +21,7 @@ "@yornaath/batshit": "^0.11.1", "clsx": "^2.1.1", "ethers": "5.8.0", + "framer-motion": "^12.23.26", "graphql-request": "^7.3.1", "graphql-tag": "^2.12.6", "lightweight-charts": "^5.0.8", diff --git a/src/abi/CreditsManager.ts b/src/abi/CreditsManager.ts new file mode 100644 index 0000000..150bac4 --- /dev/null +++ b/src/abi/CreditsManager.ts @@ -0,0 +1,94 @@ +import { Abi } from "viem"; + +export const CreditsManagerAbi: Abi = [ + { + inputs: [ + { internalType: "contract ERC20", name: "_token", type: "address" }, + { + internalType: "contract SeerCredits", + name: "_seerCredits", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { internalType: "address", name: "_user", type: "address" }, + { internalType: "uint256", name: "_amount", type: "uint256" }, + ], + name: "canSpendCredits", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_governor", type: "address" }], + name: "changeGovernor", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "contract ERC20", name: "outputToken", type: "address" }, + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "governor", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "seerCredits", + outputs: [ + { internalType: "contract SeerCredits", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_contract", type: "address" }, + { internalType: "bool", name: "_whitelisted", type: "bool" }, + ], + name: "setWhitelistedContract", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "contract ERC20", name: "_token", type: "address" }, + ], + name: "sweepTokens", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "token", + outputs: [{ internalType: "contract ERC20", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "whitelistedContracts", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/src/abi/wrappedXDAI.ts b/src/abi/wrappedXDAI.ts new file mode 100644 index 0000000..5d4829e --- /dev/null +++ b/src/abi/wrappedXDAI.ts @@ -0,0 +1,155 @@ +import { Abi } from "viem"; + +export const wrappedXDAIAbi: Abi = [ + { + constant: true, + inputs: [], + name: "name", + outputs: [{ name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "guy", type: "address" }, + { name: "wad", type: "uint256" }, + ], + name: "approve", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "totalSupply", + outputs: [{ name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "src", type: "address" }, + { name: "dst", type: "address" }, + { name: "wad", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [{ name: "wad", type: "uint256" }], + name: "withdraw", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "decimals", + outputs: [{ name: "", type: "uint8" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [{ name: "", type: "address" }], + name: "balanceOf", + outputs: [{ name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [], + name: "symbol", + outputs: [{ name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "dst", type: "address" }, + { name: "wad", type: "uint256" }, + ], + name: "transfer", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [], + name: "deposit", + outputs: [], + payable: true, + stateMutability: "payable", + type: "function", + }, + { + constant: true, + inputs: [ + { name: "", type: "address" }, + { name: "", type: "address" }, + ], + name: "allowance", + outputs: [{ name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { payable: true, stateMutability: "payable", type: "fallback" }, + { + anonymous: false, + inputs: [ + { indexed: true, name: "src", type: "address" }, + { indexed: true, name: "guy", type: "address" }, + { indexed: false, name: "wad", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: "src", type: "address" }, + { indexed: true, name: "dst", type: "address" }, + { indexed: false, name: "wad", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: "dst", type: "address" }, + { indexed: false, name: "wad", type: "uint256" }, + ], + name: "Deposit", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: "src", type: "address" }, + { indexed: false, name: "wad", type: "uint256" }, + ], + name: "Withdrawal", + type: "event", + }, +] as const; diff --git a/src/app/(homepage)/components/Header/index.tsx b/src/app/(homepage)/components/Header/index.tsx index 13ff5ea..92f6b42 100644 --- a/src/app/(homepage)/components/Header/index.tsx +++ b/src/app/(homepage)/components/Header/index.tsx @@ -6,13 +6,15 @@ import SeerLogo from "@/components/SeerLogo"; import SeerHeaderBackground from "@/assets/png/seer-header-bg.png"; import ChartBar from "@/assets/svg/chart-bar.svg"; +import { metadata } from "@/consts/markets"; + import Countdown from "./Countdown"; const Header: React.FC = () => { return (

- Session 1 - Movies Experiment + {metadata.name}

@@ -31,6 +33,7 @@ const Header: React.FC = () => { className={clsx( "relative mt-8 box-border w-full overflow-hidden rounded-xl", "border-gradient-purple-blue", + "flex flex-col gap-2", )} > { alt="Seer header background" className="absolute -z-2 size-full object-cover max-md:opacity-35" /> -
+

- If watched, what score will Clément give to the movie? + {metadata.question}

+

+ {metadata.questionDescription} +

); diff --git a/src/app/(homepage)/components/ParticipateSection/CsvUpload/CsvDownload.tsx b/src/app/(homepage)/components/ParticipateSection/CsvUpload/CsvDownload.tsx new file mode 100644 index 0000000..720396a --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/CsvUpload/CsvDownload.tsx @@ -0,0 +1,26 @@ +import { useCallback } from "react"; + +import { useMarketsStore } from "@/store/markets"; + +import LightButton from "@/components/LightButton"; + +import { downloadCsvFile, generateMarketCsv } from "@/utils/csv"; + +const CsvDownload: React.FC = () => { + const markets = useMarketsStore((state) => state.markets); + const handleDownload = useCallback(() => { + const csv = generateMarketCsv(markets); + downloadCsvFile("market-predictions.csv", csv); + }, [markets]); + + return ( + + ); +}; + +export default CsvDownload; diff --git a/src/app/(homepage)/components/ParticipateSection/CsvUpload/index.tsx b/src/app/(homepage)/components/ParticipateSection/CsvUpload/index.tsx index 04a9acb..77af54b 100644 --- a/src/app/(homepage)/components/ParticipateSection/CsvUpload/index.tsx +++ b/src/app/(homepage)/components/ParticipateSection/CsvUpload/index.tsx @@ -7,7 +7,9 @@ import { useToggle } from "react-use"; import { useMarketsStore } from "@/store/markets"; import { isUndefined } from "@/utils"; -import { parseMarketCSV } from "@/utils/parseCsvFile"; +import { parseMarketCSV } from "@/utils/csv"; + +import CsvDownload from "./CsvDownload"; interface ICsvUploadPopup { isOpen: boolean; @@ -65,13 +67,13 @@ const CsvUploadPopup: React.FC = ({ )} > - marketId,score + marketName,score - 0x105d957043ee12f7705efa072af11e718f8c5b83,49.45 + Judge Dredd (1995),49.45 - 0x68af0afe82dda5c9c26e6a458a143caad35708d6,53.52 + Bacurau (2019),53.52 ... @@ -82,6 +84,7 @@ const CsvUploadPopup: React.FC = ({ Gnosis ecosystem.
+ { diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/DepositInterface.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/DepositInterface.tsx index 71db3fd..a854bb8 100644 --- a/src/app/(homepage)/components/ParticipateSection/TradeWallet/DepositInterface.tsx +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/DepositInterface.tsx @@ -7,7 +7,7 @@ import { useAccount, useBalance } from "wagmi"; import { useDepositToTradeExecutor } from "@/hooks/tradeWallet/useDepositToTradeExecutor"; import { useTokenBalance } from "@/hooks/useTokenBalance"; -import AmountInput, { TokenType } from "@/components/AmountInput"; +import AmountInput from "@/components/AmountInput"; import LightButton from "@/components/LightButton"; import CloseIcon from "@/assets/svg/close-icon.svg"; @@ -15,6 +15,7 @@ import CloseIcon from "@/assets/svg/close-icon.svg"; import { isUndefined } from "@/utils"; import { collateral } from "@/consts"; +import { TokenType } from "@/consts/tokens"; interface DepositInterfaceProps { isOpen: boolean; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/index.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/index.tsx index 4298cdf..69c8e5f 100644 --- a/src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/index.tsx +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/index.tsx @@ -21,7 +21,7 @@ const ProjectBalances: React.FC = () => { ); return ( = ({ isOpen, toggleIsOpen, }) => { - const [amount, setAmount] = useState(); + const [amount, setAmount] = useState(); + const [selectedToken, setSelectedToken] = useState(TokenType.sDAI); const { address: account } = useAccount(); - const { data: balanceData, isLoading: isBalanceLoading } = useTokenBalance({ + const { data: balanceData } = useTokenBalance({ + address: tradeExecutor, + token: Tokens[selectedToken].address, + }); + const { data: balanceXDai } = useBalance({ address: tradeExecutor, - token: collateral.address, }); - const balance = - balanceData && formatUnits(balanceData.value, balanceData.decimals); + + const balance = useMemo(() => { + if (selectedToken === TokenType.xDAI) { + return balanceXDai?.value ?? 0n; + } + return balanceData?.value ?? 0n; + }, [balanceXDai, balanceData, selectedToken]); const withdrawFromTradeExecutor = useWithdrawFromTradeExecutor(() => { setAmount(undefined); @@ -50,25 +52,17 @@ export const WithdrawInterface: React.FC = ({ const onSubmit = (e: FormEvent) => { e.preventDefault(); - const data = Object.fromEntries(new FormData(e.currentTarget)); - const depositAmount = data["amount"]; - - if (!account) return; + if (!account || !amount) return; withdrawFromTradeExecutor.mutate({ account, - tokens: [collateral.address], - amounts: [parseUnits(depositAmount as string, collateral.decimals)], + tokens: [Tokens[selectedToken].address], + amounts: [amount], + isXDai: selectedToken === TokenType.xDAI, tradeExecutor, }); }; - const handleMaxClick = () => { - if (balance) { - setAmount(balance); - } - }; - return ( = ({

- Withdraw sDAI + Withdraw

Withdraw from trade wallet to your account @@ -95,37 +89,12 @@ export const WithdrawInterface: React.FC = ({

- { - if (!curr) return null; - return parseUnits(curr.toString() ?? "0", 18) > - (balanceData?.value ?? 0n) - ? "Not enough balance" - : undefined; - }} - message={ - isBalanceLoading - ? "Loading..." - : `Available: ${formatValue(balanceData?.value ?? 0n)} sDAI` - } - isReadOnly={withdrawFromTradeExecutor.isPending} - className="md:min-w-xl" - /> -
@@ -134,8 +103,10 @@ export const WithdrawInterface: React.FC = ({ text="Withdraw" isDisabled={ withdrawFromTradeExecutor.isPending || - isBalanceLoading || - balanceData?.value === 0n + (selectedToken === TokenType.xDAI + ? !balanceXDai + : !balanceData) || + balance === 0n } isLoading={withdrawFromTradeExecutor.isPending} /> diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx index a787a34..f4a9537 100644 --- a/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { Button, Card } from "@kleros/ui-components-library"; +import { Button, Card, DropdownSelect } from "@kleros/ui-components-library"; import clsx from "clsx"; import Link from "next/link"; import { useToggle } from "react-use"; @@ -96,18 +96,18 @@ export const TradeWallet = () => {
-
+
@@ -158,46 +166,56 @@ export const TradeWallet = () => { )} - - - - - + {isDepositOpen ? ( + + ) : null} + {isWithdrawOpen ? ( + + ) : null} + {isRedeemOpen ? ( + + ) : null} + {isMintOpen ? ( + + ) : null} + {isMergeOpen ? ( + + ) : null} ); }; diff --git a/src/app/(homepage)/components/PredictAll/PredictAllPopup/Header.tsx b/src/app/(homepage)/components/PredictAll/PredictAllPopup/Header.tsx index 0f6a99d..ddb6cba 100644 --- a/src/app/(homepage)/components/PredictAll/PredictAllPopup/Header.tsx +++ b/src/app/(homepage)/components/PredictAll/PredictAllPopup/Header.tsx @@ -1,9 +1,15 @@ import React from "react"; import clsx from "clsx"; +import { AnimatePresence, motion } from "framer-motion"; + +import { useMarketsStore } from "@/store/markets"; import { usePredictionMarkets } from "@/hooks/usePredictionMarkets"; +import LightButton from "@/components/LightButton"; + +import CloseIcon from "@/assets/svg/close-icon.svg"; import ArrowDown from "@/assets/svg/long-arrow-down.svg"; import ArrowUp from "@/assets/svg/long-arrow-up.svg"; @@ -11,6 +17,7 @@ import { isUndefined } from "@/utils"; const Header: React.FC = () => { const markets = usePredictionMarkets(); + const removeMarket = useMarketsStore((state) => state.removeMarket); return (
@@ -23,49 +30,66 @@ const Header: React.FC = () => { "scroll-shadows max-h-58 overflow-hidden overflow-y-scroll", )} > - {markets.map((market) => ( -
-
- - - {market.name} - - - Score - - - {market.prediction} - -
- {!isUndefined(market?.prediction) && - !isUndefined(market?.marketEstimate) ? ( - - ) : null} -
- ))} + + {markets.map((market) => ( + +
+ + + {market.name} + + + Score + + + {market.prediction} + +
+ {!isUndefined(market?.prediction) && + !isUndefined(market?.marketEstimate) ? ( + + ) : null} + {markets.length > 1 ? ( + + } + onPress={() => removeMarket(market.marketId)} + /> + ) : null} +
+ ))} +
); diff --git a/src/app/(homepage)/components/PredictAll/PredictAllPopup/index.tsx b/src/app/(homepage)/components/PredictAll/PredictAllPopup/index.tsx index f17d6f4..5f1543a 100644 --- a/src/app/(homepage)/components/PredictAll/PredictAllPopup/index.tsx +++ b/src/app/(homepage)/components/PredictAll/PredictAllPopup/index.tsx @@ -1,9 +1,11 @@ import React, { useMemo, useState } from "react"; import { Button, Modal } from "@kleros/ui-components-library"; +import { useToggle } from "react-use"; import { useAccount, useBalance } from "wagmi"; import { + seerCreditsAddress, useReadSDaiPreviewDeposit, useReadSDaiPreviewRedeem, } from "@/generated"; @@ -14,7 +16,6 @@ import { usePredictionMarkets } from "@/hooks/usePredictionMarkets"; import { useTokenBalance } from "@/hooks/useTokenBalance"; import { useTokensBalances } from "@/hooks/useTokenBalances"; -import { TokenType } from "@/components/AmountInput"; import { PredictAmountSection } from "@/components/Predict/PredictAmountSection"; import PredictSteps from "@/components/Predict/PredictSteps"; @@ -23,6 +24,7 @@ import { isUndefined } from "@/utils"; import { collateral } from "@/consts"; import Header from "./Header"; +import { TokenType } from "@/consts/tokens"; interface IPredictAllPopup { isOpen: boolean; toggleIsOpen: () => void; @@ -36,6 +38,7 @@ export const PredictAllPopup: React.FC = ({ const [amount, setAmount] = useState(); const [selectedToken, setSelectedToken] = useState(TokenType.sDAI); + const [isUsingSeerCredits, toggleIsUsingCredits] = useToggle(false); const isXDai = selectedToken === TokenType.xDAI; @@ -55,6 +58,10 @@ export const PredictAllPopup: React.FC = ({ address: account, token: collateral.address, }); + const { data: userSeerCreditsBalanceData } = useTokenBalance({ + address: account, + token: seerCreditsAddress, + }); const { data: userXDaiBalanceData } = useBalance({ address: account, }); @@ -64,6 +71,19 @@ export const PredictAllPopup: React.FC = ({ token: collateral.address, }); + const { data: seerCreditsEquivalentXDAI } = useReadSDaiPreviewRedeem({ + args: [userSeerCreditsBalanceData?.value ?? 0n], + query: { + enabled: + !isUndefined(userSeerCreditsBalanceData) && + userSeerCreditsBalanceData.value > 0 && + isXDai, + retry: false, + }, + }); + + const seerCreditsBalance = userSeerCreditsBalanceData?.value ?? 0n; + const { data: tokensBalances } = useTokensBalances( tradeExecutor, markets.flatMap((market) => [market.upToken, market.downToken]), @@ -95,21 +115,45 @@ export const PredictAllPopup: React.FC = ({ }, }); + // the total amount of collateral being supplied in sDAI + // accounts for all sources of collateral including seer credits const sDAIDepositAmount = useMemo(() => { if (!isXDai) return amount; return resultingDeposit; }, [resultingDeposit, amount, isXDai]); - //sDAI required + // additional sDAI required to be deposited, accounts for Seer credits if being used const toBeAdded = useMemo(() => { if (isUndefined(sDAIDepositAmount)) return 0n; - return sDAIDepositAmount > (walletSDaiBalanceData?.value ?? 0n) - ? sDAIDepositAmount - (walletSDaiBalanceData?.value ?? 0n) - : 0n; - }, [sDAIDepositAmount, walletSDaiBalanceData]); + // account for wallet balance + const sDAIDepositWalletBalanceOffset = + sDAIDepositAmount > (walletSDaiBalanceData?.value ?? 0n) + ? sDAIDepositAmount - (walletSDaiBalanceData?.value ?? 0n) + : 0n; + //account for Seer Credits + if (isUsingSeerCredits) { + return sDAIDepositWalletBalanceOffset - seerCreditsBalance > 0 + ? sDAIDepositWalletBalanceOffset - seerCreditsBalance + : 0n; + } + return sDAIDepositWalletBalanceOffset; + }, [ + sDAIDepositAmount, + walletSDaiBalanceData, + seerCreditsBalance, + isUsingSeerCredits, + ]); + + const toBeAddedSeerCredits = useMemo(() => { + if (!isUsingSeerCredits) return 0n; + // sDAIDepositAmount is alrd adjusted in case xDAI is selected + return (sDAIDepositAmount ?? 0n) > seerCreditsBalance + ? seerCreditsBalance + : sDAIDepositAmount; + }, [seerCreditsBalance, sDAIDepositAmount, isUsingSeerCredits]); // when using xDAI input, we need to convert the additional sDAI amount required, - // back to xDAI to take what's necessary + // back to xDAI to take what's necessary const { data: toBeAddedXDai } = useReadSDaiPreviewRedeem({ args: [toBeAdded], query: { @@ -120,16 +164,25 @@ export const PredictAllPopup: React.FC = ({ // can be either xDAI or sDAI const availableBalance = useMemo(() => { - return selectedToken === TokenType.sDAI + const seerCreditBalanceEquivalent = isXDai + ? (seerCreditsEquivalentXDAI ?? 0n) + : seerCreditsBalance; + return !isXDai ? (userSDaiBalanceData?.value ?? 0n) + - (walletSDaiBalanceData?.value ?? 0n) - : (userXDaiBalanceData?.value ?? 0n) + (walletXDaiBalance ?? 0n); + (walletSDaiBalanceData?.value ?? 0n) + + (isUsingSeerCredits ? seerCreditBalanceEquivalent : 0n) + : (userXDaiBalanceData?.value ?? 0n) + + (walletXDaiBalance ?? 0n) + + (isUsingSeerCredits ? seerCreditBalanceEquivalent : 0n); }, [ - selectedToken, + isXDai, userSDaiBalanceData, walletSDaiBalanceData, userXDaiBalanceData, walletXDaiBalance, + seerCreditsBalance, + isUsingSeerCredits, + seerCreditsEquivalentXDAI, ]); const { @@ -138,11 +191,15 @@ export const PredictAllPopup: React.FC = ({ isCreatingWallet, isAddingCollateral, isCollateralAdded, + isAddingSeerCredits, + isSeerCreditsAdded, isProcessingMarkets, isLoadingQuotes, isPredictionSuccessful, isSending, error, + frozenToBeAdded, + frozenToBeAddedSeerCredits, tradeExecutorPredictAll, } = usePredictAllFlow({ account, @@ -152,6 +209,7 @@ export const PredictAllPopup: React.FC = ({ sDAIDepositAmount, toBeAdded, toBeAddedXDai, + toBeAddedSeerCredits, walletUnderlyingBalances: underlyingTokensBalances, walletTokensBalances: tokensBalances, onDone: () => { @@ -186,16 +244,23 @@ export const PredictAllPopup: React.FC = ({ toBeAdded, toBeAddedXDai, isXDai, + toggleIsUsingCredits, + isUsingSeerCredits, + seerCreditsBalance, }} isWalletCreated={checkTradeExecutorResult?.isCreated ?? false} /> { @@ -22,12 +24,14 @@ const PredictAll: React.FC = () => {

Predict all the estimates above

-