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/src/app/(homepage)/components/AdvancedSection.tsx b/src/app/(homepage)/components/AdvancedSection.tsx index 26760ee..da7a2bb 100644 --- a/src/app/(homepage)/components/AdvancedSection.tsx +++ b/src/app/(homepage)/components/AdvancedSection.tsx @@ -31,7 +31,7 @@ const AdvancedSection: React.FC = () => { rel="noreferrer noopener" className="text-klerosUIComponentsPrimaryBlue items-center text-sm" > - Check it out + Check it out

diff --git a/src/app/(homepage)/components/ParticipateSection/Mint/AmountInput.tsx b/src/app/(homepage)/components/ParticipateSection/Mint/AmountInput.tsx index 41cbfe1..83fb537 100644 --- a/src/app/(homepage)/components/ParticipateSection/Mint/AmountInput.tsx +++ b/src/app/(homepage)/components/ParticipateSection/Mint/AmountInput.tsx @@ -1,34 +1,47 @@ -import { BigNumberField, DropdownSelect } from "@kleros/ui-components-library"; +import { useMemo } from "react"; + +import { BigNumberField } from "@kleros/ui-components-library"; import clsx from "clsx"; import { parseUnits, formatUnits } from "viem"; -import DAIIcon from "@/assets/svg/dai.svg"; +import LightButton from "@/components/LightButton"; + +import { formatValue, isUndefined } from "@/utils"; -export enum TokenType { - sDAI, - xDAI, -} interface IAmountInput { setAmount: (amount: bigint) => void; - setSelectedToken: (token: TokenType) => void; - notEnoughBalance: boolean; defaultValue?: bigint; value?: bigint; + balance?: bigint; + isMerge?: boolean; } const AmountInput: React.FC = ({ setAmount, - setSelectedToken, - notEnoughBalance, defaultValue, value, + balance, + isMerge = false, }) => { + const notEnoughBalance = useMemo(() => { + if (isMerge) return false; + if (!isUndefined(value) && !isUndefined(balance) && value > balance) + return true; + return false; + }, [value, balance, isMerge]); + + const handleMaxClick = () => { + if (!isUndefined(balance)) { + setAmount(balance); + } + }; + return ( -
+
{ @@ -44,37 +57,30 @@ const AmountInput: React.FC = ({ value={ typeof value !== "undefined" ? formatUnits(value, 18) : undefined } - isDisabled={typeof value !== "undefined"} - /> - button]:bg-klerosUIComponentsMediumBlue [&>button]:h-11.25 [&>button]:w-fit", - "[&>button]:border-none [&>button]:focus:shadow-none", - "[&>button]:rounded-l-none", - )} - callback={(item) => { - setSelectedToken(item.itemValue); - }} - defaultSelectedKey={TokenType.sDAI} - items={[ - { - text: "sDAI", - itemValue: TokenType.sDAI, - id: TokenType.sDAI, - icon: , - }, - { - text: "xDAI", - itemValue: TokenType.xDAI, - id: TokenType.xDAI, - icon: , - }, - ]} + isDisabled={isMerge} />
{notEnoughBalance ? "Not enough balance." : undefined} + {!notEnoughBalance && ( + + {!isUndefined(balance) + ? `Available: ${formatValue(balance)}` + : "Loading..."} + + )} + {isMerge ? null : ( + + )}
); }; diff --git a/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx b/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx index 9bd89be..cdb807d 100644 --- a/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx +++ b/src/app/(homepage)/components/ParticipateSection/Mint/MergeButton.tsx @@ -1,151 +1,31 @@ -import React, { useMemo } from "react"; +import React 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 { Address } from "viem"; -import { - gnosisRouterAddress, - gnosisRouterAbi, - sDaiAddress, - useWriteErc20Approve, - useWriteGnosisRouterMergePositions, -} from "@/generated"; - -import { useTokenAllowances } from "@/hooks/useTokenAllowances"; - -import { parentMarket, invalidMarket, markets } from "@/consts/markets"; +import { useTradeExecutorMerge } from "@/hooks/tradeWallet/useTradeExecutorMerge"; interface IMergeButton { amount: bigint; - isMinting: boolean; - toggleIsMinting: (value: boolean) => void; - refetchSDai: () => void; - refetchBalances: () => void; + tradeExecutor: Address; } -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 MergeButton: React.FC = ({ amount, tradeExecutor }) => { + const tradeExecutorMerge = useTradeExecutorMerge(); - 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], - }), + const handleSubmit = () => { + tradeExecutorMerge.mutate({ + tradeExecutor, + amount, }); - return calls; - }, [amount, needApproval]); - - const { writeContractAsync: approve } = useWriteErc20Approve(); - const { writeContractAsync: mergePositions } = - useWriteGnosisRouterMergePositions(); - + }; return (
); }; @@ -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..b7d1dd8 100644 --- a/src/app/(homepage)/components/ProjectFunding/PredictButton.tsx +++ b/src/app/(homepage)/components/ProjectFunding/PredictButton.tsx @@ -1,128 +1,115 @@ -import React from "react"; -import { useMemo } from "react"; +"use client"; +import React, { useEffect, useState } from "react"; -import { Button, Modal } from "@kleros/ui-components-library"; -import { useToggle } from "react-use"; -import { formatUnits } from "viem"; +import { Button } from "@kleros/ui-components-library"; +import { useQueryClient } from "@tanstack/react-query"; +import { Address } from "viem"; import { useCardInteraction } from "@/context/CardInteractionContext"; import { useMarketContext } from "@/context/MarketContext"; -import { useBalance } from "@/hooks/useBalance"; -import { useMarketQuote } from "@/hooks/useMarketQuote"; +import { useTradeExecutorPredict } from "@/hooks/tradeWallet/useTradeExecutorPredict"; +import { useGetQuotes } from "@/hooks/useGetQuotes"; +import { useProcessMarkets } from "@/hooks/useProcessMarkets"; +import { useTokenBalance } from "@/hooks/useTokenBalance"; -import LightButton from "@/components/LightButton"; +import { formatError } from "@/utils/formatError"; -import CloseIcon from "@/assets/svg/close-icon.svg"; +interface IPredictButton { + tradeExecutor: Address; + setErrorMessage: (message: string | undefined) => void; +} -import { isUndefined } from "@/utils"; +const PredictButton: React.FC = ({ + tradeExecutor, + setErrorMessage, +}) => { + const queryClient = useQueryClient(); + const { activeCardId } = useCardInteraction(); + const { market } = useMarketContext(); -import DefaultPredictButton from "./PredictPopup/ActionButtons/DefaultPredictButton"; -import TradeButton from "./PredictPopup/ActionButtons/TradeButton"; + const [pendingPrediction, setPendingPrediction] = useState(false); -import PredictPopup from "./PredictPopup"; + const { underlyingToken, marketId } = market; + const shouldFetch = activeCardId === marketId; -const PredictButton: React.FC = () => { - const [isOpen, toggleIsOpen] = useToggle(false); - const [isPopUpOpen, toggleIsPopUpOpen] = useToggle(false); - - const { setActiveCardId } = useCardInteraction(); + const processedMarkets = useProcessMarkets({ + tradeExecutor, + enabled: shouldFetch, + }); 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], + data: getQuotesResult, + isLoading: isLoadingQuotes, + error: getQuotesError, + } = useGetQuotes( + { + account: tradeExecutor, + processedMarkets: processedMarkets!, + }, + shouldFetch, ); - const sellToken = isUpPredict ? downToken : upToken; - const sellTokenBalance = isUpPredict ? downBalance : upBalance; - const { data: sellQuote } = useMarketQuote( - underlyingToken, - sellToken, - sellTokenBalance ? formatUnits(sellTokenBalance, 18) : "1", - ); + const { data: underlyingTokenBalanceData, isLoading: isLoadingBalance } = + useTokenBalance({ + address: tradeExecutor, + token: underlyingToken, + }); + + const tradeExecutorPredict = useTradeExecutorPredict(() => { + setErrorMessage(undefined); + queryClient.refetchQueries({ + queryKey: ["useTicksData", underlyingToken], + }); + }); + + useEffect(() => { + if (getQuotesError) { + setErrorMessage(formatError(getQuotesError)); + } else if (tradeExecutorPredict.error) { + setErrorMessage(formatError(tradeExecutorPredict.error)); + } else { + setErrorMessage(undefined); + } + }, [getQuotesError, tradeExecutorPredict.error, setErrorMessage]); + + // send the transaction once the loading is done and user had clicked on predict + useEffect(() => { + if (pendingPrediction && getQuotesResult && underlyingTokenBalanceData) { + tradeExecutorPredict.mutate({ + market, + amount: underlyingTokenBalanceData.value ?? 0n, + tradeExecutor, + getQuotesResult, + }); + setPendingPrediction(false); + } + }, [pendingPrediction, getQuotesResult, underlyingTokenBalanceData]); + + const handlePredict = () => { + if (!tradeExecutor || !underlyingTokenBalanceData) return; + + if (getQuotesResult) { + // Quotes are already available, go straight to txn + tradeExecutorPredict.mutate({ + market, + amount: underlyingTokenBalanceData.value ?? 0n, + tradeExecutor, + getQuotesResult, + }); + } else { + // Quotes still loading, wait until available + setPendingPrediction(true); + } + }; - // if no previous position, carry with the default behaviour - if (!needsSelling) - return ( - <> - {differenceBetweenRoutes > 0 ? ( -