From fe7ef2b8e8730d7c75e3902e1b7422729e16f86a Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 11 Dec 2025 15:44:04 +0530 Subject: [PATCH 01/11] feat: allow-removing-prediction-in-popup --- package.json | 1 + .../PredictAll/PredictAllPopup/Header.tsx | 110 +++++++++++------- src/store/markets.ts | 16 +++ yarn.lock | 39 +++++++ 4 files changed, 123 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index e1163e3..85a0d70 100644 --- a/package.json +++ b/package.json @@ -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/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/store/markets.ts b/src/store/markets.ts index b0871cb..99d3e0e 100644 --- a/src/store/markets.ts +++ b/src/store/markets.ts @@ -16,6 +16,7 @@ interface MarketsStore { setPrediction: (marketId: string, prediction: number) => void; setMarketEstimate: (marketId: string, estimate: number) => void; resetPredictionMarkets: () => void; + removeMarket: (marketId: string) => void; } export const useMarketsStore = create((set) => ({ @@ -74,4 +75,19 @@ export const useMarketsStore = create((set) => ({ return { markets: updatedMarkets }; }), + + removeMarket: (marketId) => + set((state) => { + const market = state.markets[marketId]; + if (isUndefined(market)) return state; + + return { + markets: { + ...state.markets, + // Setting the prediction to marketEstimate, + // removes it from the predicable markets (@/hooks/usePredictionMarkets) + [marketId]: { ...market, prediction: market?.marketEstimate }, + }, + }; + }), })); diff --git a/yarn.lock b/yarn.lock index e980808..7b2f9d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10990,6 +10990,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.23.26": + version: 12.23.26 + resolution: "framer-motion@npm:12.23.26" + dependencies: + motion-dom: "npm:^12.23.23" + motion-utils: "npm:^12.23.6" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/7dbbc4a392b969804e04a056e3d89a55bf31572dc7f6cd79050b90616fbb84a1762e4ac4e2537c735d347bbf4ceebea126f5c090234149b12cffa3ea6c518a34 + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -11082,6 +11104,7 @@ __metadata: eslint-config-prettier: "npm:^10.1.2" eslint-plugin-prettier: "npm:^5.2.6" ethers: "npm:5.8.0" + framer-motion: "npm:^12.23.26" graphql-request: "npm:^7.3.1" graphql-tag: "npm:^2.12.6" lightweight-charts: "npm:^5.0.8" @@ -13403,6 +13426,22 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.23.23": + version: 12.23.23 + resolution: "motion-dom@npm:12.23.23" + dependencies: + motion-utils: "npm:^12.23.6" + checksum: 10c0/139705731085063519b88f23fcc5b1c13e15707a4ff3365da02ef9a4bf2a2d8ebed9a151c57e7f215ccd9e822365d93c16e28e619fbf25611f61dcff5ee81d75 + languageName: node + linkType: hard + +"motion-utils@npm:^12.23.6": + version: 12.23.6 + resolution: "motion-utils@npm:12.23.6" + checksum: 10c0/c058e8ba6423b3baa63e985bcc669877ee7d9579d938f5348b4e60c5ea1b4b33dd7f4877434436a4a5807f3cf00370d3fd4079a6fdd6309c5c87aa62b311a897 + languageName: node + linkType: hard + "ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" From 420bab1dcaf2f933a813255d6ac54660dbe3b7db Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 12 Dec 2025 12:15:28 +0530 Subject: [PATCH 02/11] feat: collapsible-funding-card --- package.json | 2 +- .../TradeWallet/ProjectBalances/index.tsx | 2 +- .../components/ProjectFunding/index.tsx | 159 +++++++----------- yarn.lock | 10 +- 4 files changed, 67 insertions(+), 106 deletions(-) diff --git a/package.json b/package.json index 85a0d70..205d289 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", 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 ( { - const { setActiveCardId } = useCardInteraction(); - const { - isUpPredict, - market, - prediction, - setPrediction, - showEstimateVariant, - hasLiquidity, - } = useMarketContext(); - const { - name, - color, - upToken, - downToken, - precision, - details, - marketId, - underlyingToken, - minValue, - maxValue, - } = market; + const { market } = useMarketContext(); + const { name, color, upToken, downToken, details, underlyingToken } = market; const { tradeExecutor } = useTradeWallet(); return ( - setActiveCardId(marketId)} - > -
-
-
- -

- {name} -

-
-
- -
-
- -
- -
- setPrediction(e * precision)} - /> - -
- - -
-
-
- {tradeExecutor ? ( -
- +
+
+
+ +

+ {name} +

+
+
+
+ + ), + body: ( +
+
+ +
+ {tradeExecutor ? ( +
+ + {/* */} +
+ ) : null} + }]} + /> +
+ ), + expandButton: ({ expanded, toggle }) => ( +
- ) : null} - }]} - /> -
-
+ ), + }, + ]} + /> ); }; diff --git a/yarn.lock b/yarn.lock index 7b2f9d9..b891c41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3444,9 +3444,9 @@ __metadata: languageName: node linkType: hard -"@kleros/ui-components-library@npm:^3.6.0": - version: 3.6.0 - resolution: "@kleros/ui-components-library@npm:3.6.0" +"@kleros/ui-components-library@npm:^3.7.0": + version: 3.7.0 + resolution: "@kleros/ui-components-library@npm:3.7.0" dependencies: "@internationalized/date": "npm:^3.7.0" bignumber.js: "npm:^9.1.2" @@ -3467,7 +3467,7 @@ __metadata: react-dom: ^18.0.0 react-is: ^18.0.0 tailwindcss: ^4.0.11 - checksum: 10c0/d76e481f3e72f76d40037a784ac65f97c411ce287fb7720aebb084f81032681b7f19cad0bd5269a719db148069dd18f17280083bd44938225e7e1dd3d51547d4 + checksum: 10c0/25b41deb28a338d98f3740c62ed81def420af8b90e74135f2712bc2be5aebe7eb810878e61fe9b44341e6d9430cf3a5fc85b9bd040431f8c894358f66107e9c4 languageName: node linkType: hard @@ -11084,7 +11084,7 @@ __metadata: "@graphql-codegen/typescript": "npm:^5.0.2" "@graphql-codegen/typescript-graphql-request": "npm:^6.3.0" "@graphql-codegen/typescript-operations": "npm:^5.0.2" - "@kleros/ui-components-library": "npm:^3.6.0" + "@kleros/ui-components-library": "npm:^3.7.0" "@reown/appkit": "npm:^1.7.11" "@reown/appkit-adapter-wagmi": "npm:^1.7.11" "@svgr/webpack": "npm:^8.1.0" From 50ecc190cd3d7b63b689196d54ebd153a113b748 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 12 Dec 2025 12:15:52 +0530 Subject: [PATCH 03/11] feat: question-description --- src/app/(homepage)/components/Header/index.tsx | 12 +++++++++--- src/consts/markets.ts | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) 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/consts/markets.ts b/src/consts/markets.ts index 88ecc21..f5ba5d7 100644 --- a/src/consts/markets.ts +++ b/src/consts/markets.ts @@ -36,6 +36,13 @@ export interface IMarket { conditionId: `0x${string}`; } +export const metadata = { + name: "Session 1 - Movies Experiment", + question: "If watched, what score will Clément give to the movie?", + questionDescription: + "Which movies will Clément watch as part of the “Distilled Clément’s Judgement experiment”? \nAnd for each movie, what rating will he give to that movie?", +}; + export const markets: Array = [ { name: "Judge Dredd (1995)", From 72a038dad4b37ffd20c34ca4941875b920f88f0a Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 12 Dec 2025 12:16:39 +0530 Subject: [PATCH 04/11] feat: show-funding-cards-in-unconnected-state --- .../components/PredictAll/index.tsx | 16 +++++---- src/app/(homepage)/page.tsx | 36 ++++++++----------- src/components/EnsureChain.tsx | 4 +-- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/app/(homepage)/components/PredictAll/index.tsx b/src/app/(homepage)/components/PredictAll/index.tsx index af90dcd..e6c6252 100644 --- a/src/app/(homepage)/components/PredictAll/index.tsx +++ b/src/app/(homepage)/components/PredictAll/index.tsx @@ -4,6 +4,8 @@ import { useToggle } from "react-use"; import { usePredictionMarkets } from "@/hooks/usePredictionMarkets"; +import EnsureChain from "@/components/EnsureChain"; + import { PredictAllPopup } from "./PredictAllPopup"; const PredictAll: React.FC = () => { @@ -22,12 +24,14 @@ const PredictAll: React.FC = () => {

Predict all the estimates above

-
- - - - -
- {markets.map((market, i) => ( - - - - ))} -
-
- -
-
+ + +
+ {markets.map((market, i) => ( + + + + ))} +
+ +
diff --git a/src/components/EnsureChain.tsx b/src/components/EnsureChain.tsx index 579512b..6b6d6a2 100644 --- a/src/components/EnsureChain.tsx +++ b/src/components/EnsureChain.tsx @@ -8,9 +8,7 @@ const EnsureChain: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { address } = useAccount(); return isUndefined(address) ? ( -
- -
+ ) : ( children ); From 0b3b46bdc06f3756abf37584d01e330efddffd66 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 12 Dec 2025 12:17:00 +0530 Subject: [PATCH 05/11] feat: csv-template-download --- .../CsvUpload/CsvDownload.tsx | 26 +++++ .../ParticipateSection/CsvUpload/index.tsx | 11 ++- src/utils/csv.ts | 99 +++++++++++++++++++ src/utils/marketIdFromName.ts | 11 +++ src/utils/parseCsvFile.ts | 66 ------------- 5 files changed, 143 insertions(+), 70 deletions(-) create mode 100644 src/app/(homepage)/components/ParticipateSection/CsvUpload/CsvDownload.tsx create mode 100644 src/utils/csv.ts create mode 100644 src/utils/marketIdFromName.ts delete mode 100644 src/utils/parseCsvFile.ts 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/utils/csv.ts b/src/utils/csv.ts new file mode 100644 index 0000000..ce58cb9 --- /dev/null +++ b/src/utils/csv.ts @@ -0,0 +1,99 @@ +import { PredictionMarket } from "@/store/markets"; + +import marketFromName from "./marketIdFromName"; + +import { isUndefined } from "."; + +export const parseMarketCSV = (csvText: string): Record => { + const lines = csvText.trim().split("\n"); + + if (lines.length < 2) { + throw new Error("CSV must have at least a header row and one data row"); + } + + const headers = lines[0].split(",").map((h) => h.trim()); + + // Check for required columns + if (headers.length !== 2) { + throw new Error("CSV must have exactly 2 columns: marketName, score"); + } + + if (!headers.includes("marketName") || !headers.includes("score")) { + throw new Error("CSV must have columns: marketName, score"); + } + + const result: Record = {}; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; // Skip empty lines + + const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g); + + if (isUndefined(values) || values.length !== 2) { + throw new Error( + `Row ${i + 1}: Expected 2 columns, found ${values?.length}`, + ); + } + + // remove quotes + const marketName = values[0].replace(/^"|"$/g, ""); + const scoreStr = values[1]; + // Check for empty values + if (!marketName || !scoreStr) { + throw new Error(`Row ${i + 1}: All columns must have values`); + } + const market = marketFromName(marketName); + + if (!market) { + throw new Error(`Row ${i + 1}: No market found named: ${marketName}`); + } + + const marketId = market.marketId; + + // Validate score + const score = parseFloat(scoreStr); + if (isNaN(score)) { + throw new Error( + `Row ${i + 1}: Score "${scoreStr}" is not a valid number`, + ); + } + + if (score < 0) { + throw new Error(`Row ${i + 1}: Score cannot be negative`); + } + + result[marketId] = score; + } + + if (Object.values(result).length === 0) { + throw new Error("CSV contains no valid data rows"); + } + + return result; +}; + +export function generateMarketCsv(markets: Record) { + const header = ["marketName", "score"]; + + const rows = Object.values(markets).map((market) => [ + `"${market.name}"`, + market.prediction ?? market?.marketEstimate, + ]); + + const csvContent = [header, ...rows] + .map((row) => row.map((v) => `${v}`).join(",")) + .join("\n"); + + return csvContent; +} + +export function downloadCsvFile(filename: string, content: string) { + const blob = new Blob([content], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); +} diff --git a/src/utils/marketIdFromName.ts b/src/utils/marketIdFromName.ts new file mode 100644 index 0000000..7abebb6 --- /dev/null +++ b/src/utils/marketIdFromName.ts @@ -0,0 +1,11 @@ +import { markets } from "@/consts/markets"; + +const marketFromName = (name: string) => { + const processedName = name.trim().toLowerCase(); + + return markets.find( + (market) => market.name.trim().toLowerCase() === processedName, + ); +}; + +export default marketFromName; diff --git a/src/utils/parseCsvFile.ts b/src/utils/parseCsvFile.ts deleted file mode 100644 index d90d801..0000000 --- a/src/utils/parseCsvFile.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { isAddress } from "viem"; - -export const parseMarketCSV = (csvText: string): Record => { - const lines = csvText.trim().split("\n"); - - if (lines.length < 2) { - throw new Error("CSV must have at least a header row and one data row"); - } - - const headers = lines[0].split(",").map((h) => h.trim()); - - // Check for required columns - if (headers.length !== 2) { - throw new Error("CSV must have exactly 2 columns: marketId, score"); - } - - if (!headers.includes("marketId") || !headers.includes("score")) { - throw new Error("CSV must have columns: marketId, score"); - } - - const result: Record = {}; - - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; // Skip empty lines - - const values = line.split(","); - - if (values.length !== 2) { - throw new Error( - `Row ${i + 1}: Expected 2 columns, found ${values.length}`, - ); - } - - const marketId = values[0]; - const scoreStr = values[1]; - - // Check for empty values - if (!marketId || !scoreStr) { - throw new Error(`Row ${i + 1}: All columns must have values`); - } - - // Validate score - const score = parseFloat(scoreStr); - if (isNaN(score)) { - throw new Error( - `Row ${i + 1}: Score "${scoreStr}" is not a valid number`, - ); - } - - if (score < 0) { - throw new Error(`Row ${i + 1}: Score cannot be negative`); - } - - if (!isAddress(marketId)) { - throw new Error(`Row ${i + 1}: marketId needs to be the market address`); - } - result[marketId.trim()] = score; - } - - if (Object.values(result).length === 0) { - throw new Error("CSV contains no valid data rows"); - } - - return result; -}; From b162fddc1c5b1b37bf0df3ec59bc83dde3009653 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 16 Dec 2025 16:17:30 +0530 Subject: [PATCH 06/11] feat: seer-credits-support --- src/abi/CreditsManager.ts | 94 +++++++++++ src/abi/wrappedXDAI.ts | 155 ++++++++++++++++++ .../PredictAll/PredictAllPopup/index.tsx | 87 ++++++++-- .../components/ProjectFunding/index.tsx | 27 +-- .../Predict/PredictAmountSection.tsx | 21 ++- src/components/Predict/PredictSteps.tsx | 22 +++ src/consts/index.ts | 20 ++- src/hooks/predict/usePredictAllFlow.ts | 61 ++++++- src/hooks/predict/usePredictState.ts | 8 + .../tradeWallet/useTradeExecutorPredictAll.ts | 106 +++++++++++- src/utils/getQuotes.ts | 43 ++++- src/utils/index.ts | 2 +- src/utils/swapr.ts | 6 + wagmi.config.ts | 6 + 14 files changed, 620 insertions(+), 38 deletions(-) create mode 100644 src/abi/CreditsManager.ts create mode 100644 src/abi/wrappedXDAI.ts 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/PredictAll/PredictAllPopup/index.tsx b/src/app/(homepage)/components/PredictAll/PredictAllPopup/index.tsx index f17d6f4..b9efa27 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"; @@ -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} /> { ), expandButton: ({ expanded, toggle }) => ( -