diff --git a/.env.example b/.env.example index ece6795..7f594bd 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ NEXT_PUBLIC_REOWN_PROJECTID= NEXT_PUBLIC_GNOSIS_RPC= +# https://thegraph.com/explorer/subgraphs/AAA1vYjxwFHzbt6qKwLHNcDSASyr1J1xVViDH8gTMFMR?view=Query&chain=arbitrum-one +NEXT_PUBLIC_ALGEBRA_SUBGRAPH= diff --git a/.eslintrc.json b/.eslintrc.json index 84e1f88..d9dd64a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,7 @@ "plugin:prettier/recommended", "prettier" ], - "ignorePatterns": ["**/**/generated.ts"], + "ignorePatterns": ["**/generated.ts", "src/hooks/liquidity/gql/*"], "rules": { "max-len": [ "warn", diff --git a/.gitignore b/.gitignore index 8cd6d95..09e6c75 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ next-env.d.ts # wagmi generated /src/generated.ts + +# gql +/src/hooks/liquidity/gql diff --git a/codegen.ts b/codegen.ts new file mode 100644 index 0000000..538db2d --- /dev/null +++ b/codegen.ts @@ -0,0 +1,29 @@ +import { CodegenConfig } from "@graphql-codegen/cli"; + +const config: CodegenConfig = { + overwrite: true, + schema: [process.env.NEXT_PUBLIC_ALGEBRA_SUBGRAPH!], + documents: ["src/hooks/liquidity/swapr.graphql"], + generates: { + "./src/hooks/liquidity/gql/gql.ts": { + // preset: "client", + plugins: [ + "typescript", + "typescript-operations", + "typescript-graphql-request", + ], + config: { + strictScalars: true, + scalars: { + BigDecimal: "string", + BigInt: "string", + Int8: "string", + Bytes: "`0x${string}`", + Timestamp: "string", + }, + }, + }, + }, +}; + +export default config; diff --git a/package.json b/package.json index 73f1964..f6413cf 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "private": true, "scripts": { "dev": "yarn generate && next dev", - "build": "next build", + "build": "yarn generate && next build", "start": "next start", "lint": "next lint --fix", - "generate": "wagmi generate" + "generate": "wagmi generate && yarn generate:gql", + "generate:gql": "dotenv -e .env.local -- graphql-codegen" }, "dependencies": { "@cowprotocol/cow-sdk": "^5.10.3", @@ -17,10 +18,13 @@ "@swapr/sdk": "https://github.com/seer-pm/swapr-sdk#6dea7e63f7e05c84a4374717ee1ad5baca86f7de", "@tanstack/react-query": "^5.74.4", "@wagmi/core": "^2.17.3", + "@yornaath/batshit": "^0.11.1", "clsx": "^2.1.1", "ethers": "5.8.0", + "graphql-request": "^7.3.1", "graphql-tag": "^2.12.6", "lightweight-charts": "^5.0.8", + "micro-memoize": "^4.2.0", "next": "14.2.28", "next-themes": "^0.4.6", "pino-pretty": "^13.0.0", @@ -33,12 +37,17 @@ "wagmi": "^2.15.6" }, "devDependencies": { + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/typescript": "^5.0.2", + "@graphql-codegen/typescript-graphql-request": "^6.3.0", + "@graphql-codegen/typescript-operations": "^5.0.2", "@svgr/webpack": "^8.1.0", "@tailwindcss/postcss": "^4.1.4", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@wagmi/cli": "^2.3.1", + "dotenv-cli": "^10.0.0", "eslint": "^8", "eslint-config-next": "14.2.28", "eslint-config-prettier": "^10.1.2", diff --git a/public/futarchy-kleros.png b/public/futarchy-kleros.png new file mode 100644 index 0000000..e32361b Binary files /dev/null and b/public/futarchy-kleros.png differ diff --git a/public/futarchy_kleros.png b/public/futarchy_kleros.png deleted file mode 100644 index 29862da..0000000 Binary files a/public/futarchy_kleros.png and /dev/null differ diff --git a/public/web-app-manifest-192x192.png b/public/web-app-manifest-192x192.png new file mode 100644 index 0000000..02d3313 Binary files /dev/null and b/public/web-app-manifest-192x192.png differ diff --git a/public/web-app-manifest-512x512.png b/public/web-app-manifest-512x512.png new file mode 100644 index 0000000..1791019 Binary files /dev/null and b/public/web-app-manifest-512x512.png differ diff --git a/src/app/(homepage)/components/AdvancedSection.tsx b/src/app/(homepage)/components/AdvancedSection.tsx index 26760ee..449c1c5 100644 --- a/src/app/(homepage)/components/AdvancedSection.tsx +++ b/src/app/(homepage)/components/AdvancedSection.tsx @@ -25,13 +25,13 @@ const AdvancedSection: React.FC = () => { tokens in Seer.  - Check it out + Check it out

diff --git a/src/app/(homepage)/components/Chart/index.tsx b/src/app/(homepage)/components/Chart/index.tsx index 5a0b6c5..a358600 100644 --- a/src/app/(homepage)/components/Chart/index.tsx +++ b/src/app/(homepage)/components/Chart/index.tsx @@ -81,7 +81,7 @@ const Chart: React.FC<{ data: IChartData[] }> = ({ data }) => { const latestTimestamp = Date.now() / 1000; // Generate common timestamps for all markets - const timestamps = getTimestamps(1754407213, latestTimestamp); + const timestamps = getTimestamps(1751328000, latestTimestamp); const seriesData: Record< string, diff --git a/src/app/(homepage)/components/Header/index.tsx b/src/app/(homepage)/components/Header/index.tsx index 9cd8098..13ff5ea 100644 --- a/src/app/(homepage)/components/Header/index.tsx +++ b/src/app/(homepage)/components/Header/index.tsx @@ -12,7 +12,7 @@ const Header: React.FC = () => { return (

- Session 1 - Retro PGF Experiment + Session 1 - Movies Experiment

@@ -41,7 +41,7 @@ const Header: React.FC = () => {

- If rated, what score would the gourmet committee give to the meal? + If watched, what score will Clément give to the movie?

diff --git a/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx b/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx deleted file mode 100644 index 9bd89be..0000000 --- a/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React, { useMemo } from "react"; - -import { Button } from "@kleros/ui-components-library"; -import { waitForTransactionReceipt } from "@wagmi/core"; -import { encodeFunctionData, erc20Abi, Address } from "viem"; -import { useConfig, useSendCalls, useCapabilities } from "wagmi"; - -import { - gnosisRouterAddress, - gnosisRouterAbi, - sDaiAddress, - useWriteErc20Approve, - useWriteGnosisRouterMergePositions, -} from "@/generated"; - -import { useTokenAllowances } from "@/hooks/useTokenAllowances"; - -import { parentMarket, invalidMarket, markets } from "@/consts/markets"; - -interface IMergeButton { - amount: bigint; - isMinting: boolean; - toggleIsMinting: (value: boolean) => void; - refetchSDai: () => void; - refetchBalances: () => void; -} - -const MergeButton: React.FC = ({ - amount, - isMinting, - toggleIsMinting, - refetchSDai, - refetchBalances, -}) => { - const wagmiConfig = useConfig(); - const { sendCalls } = useSendCalls(); - - const atomicSupport = false; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { data: capabilities } = useCapabilities(); - // const atomicSupport = useMemo( - // () => - // ["ready", "supported"].includes(capabilities?.[100].atomic?.status ?? ""), - // [capabilities], - // ); - - const allowances = useTokenAllowances( - markets - .map(({ underlyingToken }) => underlyingToken) - .concat([invalidMarket]), - gnosisRouterAddress, - ); - - const needApproval = useMemo(() => { - const queryKey = allowances.queryKey as readonly [ - unknown, - { contracts: { address: Address }[] }, - ]; - if (typeof allowances?.data !== "undefined") { - return allowances.data - .map(({ result }, i) => ({ - address: queryKey[1].contracts[i].address, - result, - })) - .filter(({ result }) => typeof result === "bigint" && result < amount) - .map(({ address }) => address); - } - return []; - }, [allowances, amount]); - - const calls = useMemo(() => { - const calls = needApproval.map((address) => ({ - to: address, - value: 0n, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: "approve", - args: [gnosisRouterAddress, amount], - }), - })); - calls.push({ - to: gnosisRouterAddress, - value: 0n, - data: encodeFunctionData({ - abi: gnosisRouterAbi, - functionName: "mergePositions", - args: [sDaiAddress, parentMarket, amount], - }), - }); - return calls; - }, [amount, needApproval]); - - const { writeContractAsync: approve } = useWriteErc20Approve(); - const { writeContractAsync: mergePositions } = - useWriteGnosisRouterMergePositions(); - - return ( -
+ + ); +}; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/MergeInterface.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/MergeInterface.tsx new file mode 100644 index 0000000..e5b5708 --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/MergeInterface.tsx @@ -0,0 +1,158 @@ +import React, { FormEvent, useMemo, useState } from "react"; + +import { + BigNumberField, + Button, + Form, + Modal, +} from "@kleros/ui-components-library"; +import clsx from "clsx"; +import { Address, formatUnits, parseUnits } from "viem"; +import { useAccount } from "wagmi"; + +import { useTradeExecutorMerge } from "@/hooks/tradeWallet/useTradeExecutorMerge"; +import { useTokensBalances } from "@/hooks/useTokenBalances"; + +import LightButton from "@/components/LightButton"; + +import CloseIcon from "@/assets/svg/close-icon.svg"; + +import { formatValue, isUndefined } from "@/utils"; + +import { collateral, DECIMALS } from "@/consts"; +import { markets } from "@/consts/markets"; + +interface MergeInterfaceProps { + isOpen: boolean; + toggleIsOpen: () => void; + tradeExecutor: Address; +} + +const MergeInterface: React.FC = ({ + tradeExecutor, + isOpen, + toggleIsOpen, +}) => { + const [amount, setAmount] = useState(); + + const { address: account } = useAccount(); + + const { data: marketBalances, isLoading: isBalanceLoading } = + useTokensBalances( + tradeExecutor, + markets.map(({ underlyingToken }) => underlyingToken), + ); + + const minMarketBalance = useMemo(() => { + if (!isUndefined(marketBalances)) { + if (marketBalances.some((result) => typeof result !== "bigint")) + return 0n; + const minResult = marketBalances.reduce((acc, curr) => + curr! < acc! ? curr : acc, + ); + return minResult as bigint; + } + return 0n; + }, [marketBalances]); + + const tradeExecutorMerge = useTradeExecutorMerge(() => { + setAmount(undefined); + toggleIsOpen(); + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + const mergeAmount = data["amount"]; + + if (!account) return; + + tradeExecutorMerge.mutate({ + amount: parseUnits(mergeAmount as string, collateral.decimals), + tradeExecutor, + }); + }; + + const handleMaxClick = () => { + if (minMarketBalance) { + setAmount(formatUnits(minMarketBalance, DECIMALS)); + } + }; + + return ( + + + } + onPress={toggleIsOpen} + /> + +
+
+

+ Merge Project tokens +

+

+ Merge Project tokens to your account +

+
+
+
+ { + if (!curr) return null; + return parseUnits(curr.toString() ?? "0", 18) > + (minMarketBalance ?? 0n) + ? "Not enough balance" + : undefined; + }} + message={ + isBalanceLoading + ? "Loading..." + : `Available: ${formatValue(minMarketBalance ?? 0n)} sDAI` + } + isReadOnly={tradeExecutorMerge.isPending} + className="md:min-w-xl" + /> + +
+ +
+
+ ); +}; + +export default MergeInterface; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/MintInterface.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/MintInterface.tsx new file mode 100644 index 0000000..09a2b92 --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/MintInterface.tsx @@ -0,0 +1,146 @@ +import React, { FormEvent, useState } from "react"; + +import { + BigNumberField, + Button, + Form, + Modal, +} from "@kleros/ui-components-library"; +import clsx from "clsx"; +import { Address, formatUnits, parseUnits } from "viem"; +import { useAccount } from "wagmi"; + +import { useTradeExecutorSplit } from "@/hooks/tradeWallet/useTradeExecutorSplit"; +import { useTokenBalance } from "@/hooks/useTokenBalance"; + +import LightButton from "@/components/LightButton"; + +import CloseIcon from "@/assets/svg/close-icon.svg"; + +import { formatValue } from "@/utils"; + +import { collateral } from "@/consts"; + +interface MintInterfaceProps { + isOpen: boolean; + toggleIsOpen: () => void; + tradeExecutor: Address; +} + +const MintInterface: React.FC = ({ + tradeExecutor, + isOpen, + toggleIsOpen, +}) => { + const [amount, setAmount] = useState(); + + const { address: account } = useAccount(); + + const { data: balanceData, isLoading: isBalanceLoading } = useTokenBalance({ + address: tradeExecutor, + token: collateral.address, + }); + const balance = + balanceData && formatUnits(balanceData.value, balanceData.decimals); + + const tradeExecutorSplit = useTradeExecutorSplit(() => { + setAmount(undefined); + toggleIsOpen(); + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + const mintAmount = data["amount"]; + + if (!account) return; + + tradeExecutorSplit.mutate({ + amount: parseUnits(mintAmount as string, collateral.decimals), + tradeExecutor, + }); + }; + + const handleMaxClick = () => { + if (balance) { + setAmount(balance); + } + }; + + return ( + + + } + onPress={toggleIsOpen} + /> + +
+
+

+ Mint Project tokens +

+

+ Mint Project tokens to your Trade wallet +

+
+
+
+ { + 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={tradeExecutorSplit.isPending} + className="md:min-w-xl" + /> + +
+ +
+
+ ); +}; + +export default MintInterface; diff --git a/src/app/(homepage)/components/ParticipateSection/Mint/ProjectAmount.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/ProjectAmount.tsx similarity index 72% rename from src/app/(homepage)/components/ParticipateSection/Mint/ProjectAmount.tsx rename to src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/ProjectAmount.tsx index 268f184..96c63dc 100644 --- a/src/app/(homepage)/components/ParticipateSection/Mint/ProjectAmount.tsx +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/ProjectBalances/ProjectAmount.tsx @@ -3,31 +3,22 @@ import React from "react"; import { BigNumberField, Tooltip } from "@kleros/ui-components-library"; import { formatUnits } from "viem"; -import { formatValue, shortenName } from "@/utils"; +import { shortenName } from "@/utils"; interface IProjectAmount { - amount: bigint; balance?: bigint; name: string; color: string; } -const ProjectAmount: React.FC = ({ - amount, - balance, - name, - color, -}) => { +const ProjectAmount: React.FC = ({ balance, name, color }) => { return (
- - {`Balance: ${formatValue(balance ?? 0n)}`} -
{ + const { tradeExecutor } = useTradeWallet(); + + const { data: marketBalances } = useTokensBalances( + tradeExecutor, + markets.map(({ underlyingToken }) => underlyingToken), + ); + return ( + + + +
+ ), + body: ( +
+ {markets.map(({ name, color }, i) => ( + + ))} +
+ ), + }, + ]} + /> + ); +}; + +export default ProjectBalances; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/RedeemInterface.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/RedeemInterface.tsx new file mode 100644 index 0000000..7a26765 --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/RedeemInterface.tsx @@ -0,0 +1,173 @@ +import React, { useMemo } from "react"; + +import { Button, Modal } from "@kleros/ui-components-library"; +import { Address, formatUnits } from "viem"; + +import { useReadGnosisRouterGetWinningOutcomes } from "@/generated"; + +import { useRedeemParentsToTradeExecutor } from "@/hooks/tradeWallet/useRedeemParentsToTradeExecutor"; +import { useTokensBalances } from "@/hooks/useTokenBalances"; + +import LightButton from "@/components/LightButton"; + +import CloseIcon from "@/assets/svg/close-icon.svg"; + +import { isUndefined } from "@/utils"; + +import { markets, parentConditionId } from "@/consts/markets"; + +interface RedeemParentsInterfaceProps { + isOpen: boolean; + toggleIsOpen: () => void; + tradeExecutor: Address; +} + +export const RedeemParentsInterface: React.FC = ({ + tradeExecutor, + isOpen, + toggleIsOpen, +}) => { + const { data: winningOutcomes, isLoading: winningOutcomesLoading } = + useReadGnosisRouterGetWinningOutcomes({ + args: [parentConditionId], + }); + + const numberOutcomes = useMemo( + () => + winningOutcomes?.reduce((acc, outcome) => (outcome ? acc + 1 : acc), 0), + [winningOutcomes], + ); + + const winningTokens = useMemo(() => { + if (!winningOutcomesLoading && !isUndefined(winningOutcomes)) { + return markets + .filter( + ({ parentMarketOutcome }) => winningOutcomes[parentMarketOutcome], + ) + .map(({ underlyingToken, parentMarketOutcome }) => ({ + underlyingToken, + index: parentMarketOutcome, + })); + } + }, [winningOutcomesLoading, winningOutcomes]); + + const { data: balances, isLoading: balancesLoading } = useTokensBalances( + tradeExecutor, + winningTokens?.map(({ underlyingToken }) => underlyingToken) ?? [], + ); + + const winningTokensWithBalance = useMemo( + () => + winningTokens + ?.map(({ underlyingToken, index }, i) => ({ + address: underlyingToken, + index, + balance: balances?.[i] as bigint, + })) + .filter(({ balance }) => balance > 0n), + [balances, winningTokens], + ); + + const isCheckingStatus = + winningOutcomesLoading || + balancesLoading || + isUndefined(numberOutcomes) || + isUndefined(winningTokensWithBalance); + + // if there is value to be redeemed, its redeemable + const isRedeemable = useMemo(() => { + if (isCheckingStatus) return; + if (isUndefined(numberOutcomes) && numberOutcomes === 0) return false; + const totalBalance = winningTokensWithBalance.reduce( + (acc, { balance }) => acc + balance, + 0n, + ); + return totalBalance > 0n; + }, [numberOutcomes, winningTokensWithBalance, isCheckingStatus]); + + // estimatted value of tokens based on number of winning outcomes + const totalValue = useMemo(() => { + // can be zero if market not resolved yet + if (numberOutcomes === 0) return; + const totalBalance = winningTokensWithBalance?.reduce( + (acc, { balance }) => acc + balance, + 0n, + ); + + if ( + !isUndefined(totalBalance) && + !isUndefined(winningTokensWithBalance) && + !isUndefined(numberOutcomes) + ) { + const formattedAmount = parseFloat( + formatUnits(totalBalance / BigInt(numberOutcomes), 18), + ).toFixed(2); + if (formattedAmount !== "0.00") { + return formattedAmount; + } else if (totalBalance > 0n) { + return "< 0.01"; + } + } + }, [winningTokensWithBalance, numberOutcomes]); + + const redeemParentsFromTradeExecutor = useRedeemParentsToTradeExecutor(() => { + toggleIsOpen(); + }); + + const handleRedeem = () => { + if (isUndefined(balances)) return; + + redeemParentsFromTradeExecutor.mutate({ + tokens: markets.map(({ marketId }) => marketId), + outcomeIndexes: markets.map(({ parentMarketOutcome }) => + BigInt(parentMarketOutcome), + ), + amounts: balances, + tradeExecutor, + }); + }; + + return ( + + + } + onPress={toggleIsOpen} + /> + +
+
+

+ Redeem Tokens +

+

+ {isCheckingStatus + ? "Checking redemptions..." + : isRedeemable + ? `You have tokens from the selected projects that were not spent. + You can redeem up to ${totalValue} sDAI.` + : "Nothing to redeem."} +

+
+ +
+
+ ); +}; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/TradeWalletSkeleton.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/TradeWalletSkeleton.tsx new file mode 100644 index 0000000..60de7f8 --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/TradeWalletSkeleton.tsx @@ -0,0 +1,21 @@ +import { Card } from "@kleros/ui-components-library"; +import clsx from "clsx"; + +import { Skeleton } from "@/components/Skeleton"; + +const TradeWalletSkeleton: React.FC = () => { + return ( + + + + + + ); +}; + +export default TradeWalletSkeleton; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/WithdrawInterface.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/WithdrawInterface.tsx new file mode 100644 index 0000000..7d399d8 --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/WithdrawInterface.tsx @@ -0,0 +1,146 @@ +import React, { FormEvent, useState } from "react"; + +import { + BigNumberField, + Button, + Form, + Modal, +} from "@kleros/ui-components-library"; +import clsx from "clsx"; +import { Address, formatUnits, parseUnits } from "viem"; +import { useAccount } from "wagmi"; + +import { useWithdrawFromTradeExecutor } from "@/hooks/tradeWallet/useWithdrawFromTradeExecutor"; +import { useTokenBalance } from "@/hooks/useTokenBalance"; + +import LightButton from "@/components/LightButton"; + +import CloseIcon from "@/assets/svg/close-icon.svg"; + +import { formatValue } from "@/utils"; + +import { collateral } from "@/consts"; + +interface WithdrawInterfaceProps { + isOpen: boolean; + toggleIsOpen: () => void; + tradeExecutor: Address; +} + +export const WithdrawInterface: React.FC = ({ + tradeExecutor, + isOpen, + toggleIsOpen, +}) => { + const [amount, setAmount] = useState(); + + const { address: account } = useAccount(); + + const { data: balanceData, isLoading: isBalanceLoading } = useTokenBalance({ + address: tradeExecutor, + token: collateral.address, + }); + const balance = + balanceData && formatUnits(balanceData.value, balanceData.decimals); + + const withdrawFromTradeExecutor = useWithdrawFromTradeExecutor(() => { + setAmount(undefined); + toggleIsOpen(); + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + const depositAmount = data["amount"]; + + if (!account) return; + + withdrawFromTradeExecutor.mutate({ + account, + tokens: [collateral.address], + amounts: [parseUnits(depositAmount as string, collateral.decimals)], + tradeExecutor, + }); + }; + + const handleMaxClick = () => { + if (balance) { + setAmount(balance); + } + }; + + return ( + + + } + onPress={toggleIsOpen} + /> + +
+
+

+ Withdraw sDAI +

+

+ Withdraw from trade wallet to your account +

+
+
+
+ { + 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" + /> + +
+ +
+
+ ); +}; diff --git a/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx b/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx new file mode 100644 index 0000000..a787a34 --- /dev/null +++ b/src/app/(homepage)/components/ParticipateSection/TradeWallet/index.tsx @@ -0,0 +1,203 @@ +import { useMemo } from "react"; + +import { Button, Card } from "@kleros/ui-components-library"; +import clsx from "clsx"; +import Link from "next/link"; +import { useToggle } from "react-use"; +import { useAccount } from "wagmi"; + +import { useTradeWallet } from "@/context/TradeWalletContext"; +import { useGetWinningOutcomes } from "@/hooks/useGetWinningOutcomes"; +import { useTokenBalance } from "@/hooks/useTokenBalance"; + +import WithHelpTooltip from "@/components/WithHelpTooltip"; + +import ExternalArrow from "@/assets/svg/external-arrow.svg"; +import WalletIcon from "@/assets/svg/wallet.svg"; + +import { formatValue, isUndefined, shortenAddress } from "@/utils"; + +import { collateral } from "@/consts"; +import { parentConditionId } from "@/consts/markets"; + +import { DepositInterface } from "./DepositInterface"; +import MergeInterface from "./MergeInterface"; +import MintInterface from "./MintInterface"; +import ProjectBalances from "./ProjectBalances"; +import { RedeemParentsInterface } from "./RedeemInterface"; +import TradeWalletSkeleton from "./TradeWalletSkeleton"; +import { WithdrawInterface } from "./WithdrawInterface"; + +export const TradeWallet = () => { + const [isDepositOpen, toggleIsDepositOpen] = useToggle(false); + const [isWithdrawOpen, toggleIsWithdrawOpen] = useToggle(false); + const [isRedeemOpen, toggleIsRedeemOpen] = useToggle(false); + const [isMintOpen, toggleIsMintOpen] = useToggle(false); + const [isMergeOpen, toggleIsMergeOpen] = useToggle(false); + + const { tradeExecutor, isLoadingTradeWallet } = useTradeWallet(); + const { address: account, chain } = useAccount(); + + const { data: balanceData, isLoading: isBalanceLoading } = useTokenBalance({ + address: tradeExecutor, + token: collateral.address, + }); + + const blockExplorerUrl = chain?.blockExplorers?.default?.url; + + const { data: parentWinningOutcomes } = + useGetWinningOutcomes(parentConditionId); + const isParentResolved = useMemo( + () => + isUndefined(parentWinningOutcomes) + ? false + : parentWinningOutcomes.some((val) => val === true), + [parentWinningOutcomes], + ); + + return ( + <> + {isLoadingTradeWallet ? : null} + {account && tradeExecutor && ( + +
+ {/* Left side: title + buttons */} +
+
+
+ + +

+ Trade Wallet +

+
+
+ + + {shortenAddress(tradeExecutor)} + + +
+ +
+
+
+ + {/* Right side: balance */} +
+

+ sDai Balance +

+

+ {isBalanceLoading ? ( + Loading... + ) : ( + {formatValue(balanceData?.value ?? 0n)} + )} +

+
+
+ +
+ )} + + + + + + + ); +}; diff --git a/src/app/(homepage)/components/ParticipateSection/index.tsx b/src/app/(homepage)/components/ParticipateSection/index.tsx index c6d1ad0..4e2262b 100644 --- a/src/app/(homepage)/components/ParticipateSection/index.tsx +++ b/src/app/(homepage)/components/ParticipateSection/index.tsx @@ -1,7 +1,6 @@ import { Card } from "@kleros/ui-components-library"; -import Mint from "./Mint"; -import RedeemParentMarket from "./RedeemParentMarket"; +import { TradeWallet } from "./TradeWallet"; const ParticipateSection: React.FC = () => { return ( @@ -10,7 +9,7 @@ const ParticipateSection: React.FC = () => { Participate - + { you want to predict.

-
); }; diff --git a/src/app/(homepage)/components/ProjectFunding/PositionValue.tsx b/src/app/(homepage)/components/ProjectFunding/PositionValue.tsx index 66bf822..7d60b1f 100644 --- a/src/app/(homepage)/components/ProjectFunding/PositionValue.tsx +++ b/src/app/(homepage)/components/ProjectFunding/PositionValue.tsx @@ -2,12 +2,10 @@ import React, { useMemo } from "react"; import clsx from "clsx"; import { formatUnits, Address, formatEther } from "viem"; -import { useAccount } from "wagmi"; - -import { useReadErc20BalanceOf } from "@/generated"; import { useMarketContext } from "@/context/MarketContext"; import { useMarketPrice } from "@/hooks/useMarketPrice"; +import { useTokenBalance } from "@/hooks/useTokenBalance"; import { formatValue, isUndefined } from "@/utils"; @@ -19,15 +17,15 @@ interface IPositionValue { upToken: Address; downToken: Address; underlyingToken: Address; + tradeExecutor: Address; } const PositionValue: React.FC = ({ upToken, downToken, underlyingToken, + tradeExecutor, }) => { - const { address } = useAccount(); - const { isLoadingMarketPrice, isResolved, @@ -40,14 +38,14 @@ const PositionValue: React.FC = ({ value: upValue, balance: upBalance, price: upPrice, - } = useTokenPositionValue(upToken, underlyingToken, address ?? "0x", { + } = useTokenPositionValue(upToken, underlyingToken, tradeExecutor ?? "0x", { isUp: true, }); const { value: downValue, balance: downBalance, price: downPrice, - } = useTokenPositionValue(downToken, underlyingToken, address ?? "0x", { + } = useTokenPositionValue(downToken, underlyingToken, tradeExecutor ?? "0x", { isUp: false, }); const totalValue = upValue + downValue; @@ -125,7 +123,7 @@ const PositionValue: React.FC = ({ {totalValue.toFixed(2)} sDAI

- {isResolved ? : null} + {isResolved ? : null} ); }; @@ -138,14 +136,11 @@ const useTokenPositionValue = ( ) => { const { hasLiquidity, marketPrice } = useMarketContext(); - const { data: balance } = useReadErc20BalanceOf({ - address: token, - args: [address ?? "0x"], - query: { - staleTime: 5000, - enabled: typeof address !== "undefined", - }, + const { data: balanceData } = useTokenBalance({ + token: token, + address: address, }); + const balance = balanceData?.value; const { data } = useMarketPrice( token, diff --git a/src/app/(homepage)/components/ProjectFunding/PredictButton.tsx b/src/app/(homepage)/components/ProjectFunding/PredictButton.tsx index 1a92cca..09139d3 100644 --- a/src/app/(homepage)/components/ProjectFunding/PredictButton.tsx +++ b/src/app/(homepage)/components/ProjectFunding/PredictButton.tsx @@ -1,127 +1,25 @@ +"use client"; import React from "react"; -import { useMemo } from "react"; -import { Button, Modal } from "@kleros/ui-components-library"; +import { Button } from "@kleros/ui-components-library"; import { useToggle } from "react-use"; -import { formatUnits } from "viem"; -import { useCardInteraction } from "@/context/CardInteractionContext"; import { useMarketContext } from "@/context/MarketContext"; -import { useBalance } from "@/hooks/useBalance"; -import { useMarketQuote } from "@/hooks/useMarketQuote"; -import LightButton from "@/components/LightButton"; +import { PredictPopup } from "./PredictPopup"; -import CloseIcon from "@/assets/svg/close-icon.svg"; - -import { isUndefined } from "@/utils"; - -import DefaultPredictButton from "./PredictPopup/ActionButtons/DefaultPredictButton"; -import TradeButton from "./PredictPopup/ActionButtons/TradeButton"; - -import PredictPopup from "./PredictPopup"; - -const PredictButton: React.FC = () => { +const PredictButton: React.FC = ({}) => { const [isOpen, toggleIsOpen] = useToggle(false); - const [isPopUpOpen, toggleIsPopUpOpen] = useToggle(false); - - const { setActiveCardId } = useCardInteraction(); - - const { - market, - isUpPredict, - differenceBetweenRoutes, - isLoading: isLoadingComplexRoute, - hasLiquidity, - refetchQuotes, - } = useMarketContext(); - - const { upToken, downToken, underlyingToken, marketId } = market; - - const { data: underlyingBalance } = useBalance(underlyingToken); - const { data: upBalance } = useBalance(upToken); - const { data: downBalance } = useBalance(downToken); - - const needsSelling = useMemo( - () => - isUpPredict - ? !isUndefined(downBalance) && downBalance > 0 - : !isUndefined(upBalance) && upBalance > 0, - [isUpPredict, downBalance, upBalance], - ); - - const sellToken = isUpPredict ? downToken : upToken; - const sellTokenBalance = isUpPredict ? downBalance : upBalance; - const { data: sellQuote } = useMarketQuote( - underlyingToken, - sellToken, - sellTokenBalance ? formatUnits(sellTokenBalance, 18) : "1", - ); - - // if no previous position, carry with the default behaviour - if (!needsSelling) - return ( - <> - {differenceBetweenRoutes > 0 ? ( -