From a7423c20c2042e186106b6dc882915a6d60a0161 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 4 Jul 2024 13:32:54 -0700 Subject: [PATCH 01/26] feat: add redeem nyc tab, skeleton logic --- src/components/layout/page-content.tsx | 5 +++ src/components/tabs/redeem-nyc.tsx | 56 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/components/tabs/redeem-nyc.tsx diff --git a/src/components/layout/page-content.tsx b/src/components/layout/page-content.tsx index a45694e..eacedfd 100644 --- a/src/components/layout/page-content.tsx +++ b/src/components/layout/page-content.tsx @@ -2,6 +2,7 @@ import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; import { useAtom } from "jotai"; import { activeTabAtom } from "../../store/common"; import Dashboard from "../tabs/dashboard"; +import RedeemNYC from "../tabs/redeem-nyc"; import MiningClaims from "../tabs/mining-claims"; import StackingClaims from "../tabs/stacking-claims"; import Voting from "../tabs/voting"; @@ -29,6 +30,7 @@ function Content() { }} > Dashboard + Redeem NYC Mining Claims Stacking Claims Voting @@ -37,6 +39,9 @@ function Content() { + + + diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx new file mode 100644 index 0000000..8f65295 --- /dev/null +++ b/src/components/tabs/redeem-nyc.tsx @@ -0,0 +1,56 @@ +import { Heading, Stack } from "@chakra-ui/react"; +import { atom, useAtomValue } from "jotai"; + +const v1BalanceNYCAtom = atom(0); +const v2BalanceNYCAtom = atom(0); + +const totalBalanceNYCAtom = atom( + (get) => get(v1BalanceNYCAtom) + get(v2BalanceNYCAtom) +); + +const amountForBalanceAtom = atom(0); + +function RedeemNYC() { + const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); + const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); + const totalBalanceNYC = useAtomValue(totalBalanceNYCAtom); + const amountForBalance = useAtomValue(amountForBalanceAtom); + + return ( + + CityCoins NYC Redemption + + ); +} + +export default RedeemNYC; + +/* + +Need a button to refresh the information +- get V1 NYC balance +- get V2 NYC balance +- derive total balance w/ micro adaptation +- get amount for balance from contract + +Need a button to redeem NYC +- post condition burn balances +- post condition receive STX + +Need a button to redeem NYC for stSTX / liSTX. +- disclaimer/pop-up for the user to understand the risks +- link to their official websites, documentation, chat rooms +- post condition depends on which asset + - stSTX has a function we can call to get amount + - liSTX mints an NFT so no need for post condition + +stSTX calculation: + +> To get the ststx/stx ratio you can call `get-stx-per-ststx` on `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.data-core-v1`. The param `reserve-contract` should be `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.reserve-v1`. +> +> It will currently return `u1015555`. Meaning for 1.015555 STX you will get 1 stSTX. + +stackingDAO wrapper: +https://explorer.hiro.so/txid/SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.cc-redemption-v1?chain=mainnet + +*/ From 33a6ef4c3ec290f44c39adfe30e7bb1769923490 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 4 Jul 2024 13:38:59 -0700 Subject: [PATCH 02/26] fix: sonnet with some mods for the icons --- src/components/tabs/redeem-nyc.tsx | 149 +++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 30 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 8f65295..43e045b 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -1,5 +1,25 @@ -import { Heading, Stack } from "@chakra-ui/react"; +import { + Heading, + Button, + Stat, + StatLabel, + StatNumber, + StatGroup, + VStack, + HStack, + Text, + useDisclosure, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Link, +} from "@chakra-ui/react"; import { atom, useAtomValue } from "jotai"; +import { LuExternalLink, LuRepeat } from "react-icons/lu"; const v1BalanceNYCAtom = atom(0); const v2BalanceNYCAtom = atom(0); @@ -16,41 +36,110 @@ function RedeemNYC() { const totalBalanceNYC = useAtomValue(totalBalanceNYCAtom); const amountForBalance = useAtomValue(amountForBalanceAtom); - return ( - - CityCoins NYC Redemption - - ); -} + const { isOpen, onOpen, onClose } = useDisclosure(); -export default RedeemNYC; + const refreshBalances = () => { + // Implement the logic to refresh balances here + console.log("Refreshing balances..."); + }; + + const redeemNYC = () => { + // Implement the logic to redeem NYC here + console.log("Redeeming NYC..."); + }; -/* + const redeemForStSTX = () => { + // Implement the logic to redeem for stSTX here + console.log("Redeeming for stSTX..."); + onClose(); + }; -Need a button to refresh the information -- get V1 NYC balance -- get V2 NYC balance -- derive total balance w/ micro adaptation -- get amount for balance from contract + const redeemForLiSTX = () => { + // Implement the logic to redeem for liSTX here + console.log("Redeeming for liSTX..."); + onClose(); + }; -Need a button to redeem NYC -- post condition burn balances -- post condition receive STX + return ( + + CityCoins NYC Redemption + + -Need a button to redeem NYC for stSTX / liSTX. -- disclaimer/pop-up for the user to understand the risks -- link to their official websites, documentation, chat rooms -- post condition depends on which asset - - stSTX has a function we can call to get amount - - liSTX mints an NFT so no need for post condition + + + V1 NYC Balance + {v1BalanceNYC} + + + V2 NYC Balance + {v2BalanceNYC} + + -stSTX calculation: + + + Total NYC Balance + {totalBalanceNYC} + + + Amount for Balance + {amountForBalance} + + -> To get the ststx/stx ratio you can call `get-stx-per-ststx` on `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.data-core-v1`. The param `reserve-contract` should be `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.reserve-v1`. -> -> It will currently return `u1015555`. Meaning for 1.015555 STX you will get 1 stSTX. + + + + -stackingDAO wrapper: -https://explorer.hiro.so/txid/SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.cc-redemption-v1?chain=mainnet + + + + Redeem NYC for stSTX / liSTX + + + + Please be aware of the risks associated with redeeming for stSTX + or liSTX. Make sure to understand the implications before + proceeding. + + + Official Resources: + + StackingDAO Website + + + StackingDAO Discord + + + Lido Finance Website + + + Lido Finance Discord + + + + + + + + + + + + + ); +} -*/ +export default RedeemNYC; From b1e1c1c06f1ea63d808c9d808c79b0264555998f Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 4 Jul 2024 13:44:09 -0700 Subject: [PATCH 03/26] chore: update react-icons to latest --- package-lock.json | 10 ++++------ package.json | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2acff9d..3e0b564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "openai": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^3.11.0", + "react-icons": "^5.2.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -16899,11 +16899,9 @@ } }, "node_modules/react-icons": { - "version": "3.11.0", - "license": "MIT", - "dependencies": { - "camelcase": "^5.0.0" - }, + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", "peerDependencies": { "react": "*" } diff --git a/package.json b/package.json index 0967f93..7e9a5e9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "openai": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^3.11.0", + "react-icons": "^5.2.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" From 53ac0e1e3e8fa6d324fe34650fb2df7a504bd146 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 4 Jul 2024 13:57:44 -0700 Subject: [PATCH 04/26] fix: cleaning up the generated code, update styling --- src/components/tabs/redeem-nyc.tsx | 49 +++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 43e045b..68957fb 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -7,6 +7,7 @@ import { StatGroup, VStack, HStack, + Stack, Text, useDisclosure, Modal, @@ -17,6 +18,7 @@ import { ModalBody, ModalCloseButton, Link, + Divider, } from "@chakra-ui/react"; import { atom, useAtomValue } from "jotai"; import { LuExternalLink, LuRepeat } from "react-icons/lu"; @@ -90,14 +92,14 @@ function RedeemNYC() { - - - - + @@ -105,24 +107,43 @@ function RedeemNYC() { Redeem NYC for stSTX / liSTX + + If you would like to claim and stack in the same transaction,{" "} + + StackingDAO + {" "} + and{" "} + + LISA + {" "} + have partnered to offer stSTX and liSTX. + + + Please review the resources below before proceeding to fully + understand the process through their platform. + Please be aware of the risks associated with redeeming for stSTX - or liSTX. Make sure to understand the implications before - proceeding. + or liSTX. StackingDAO and LISA are not affiliated with CityCoins. - + + Official Resources: - - StackingDAO Website + + StackingDAO Website - StackingDAO Discord + StackingDAO Discord - Lido Finance Website + Lido Finance Website - Lido Finance Discord + Lido Finance Discord From 72b821a67bb4edeb3f1f36212e90583c0a2d78cf Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 13:31:41 -0700 Subject: [PATCH 05/26] fix: update active tab default to Voting tab --- src/store/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/common.ts b/src/store/common.ts index 0ad918c..5ce485b 100644 --- a/src/store/common.ts +++ b/src/store/common.ts @@ -27,7 +27,7 @@ export type LoadableDataset = { export const activeTabAtom = atomWithStorage( "citycoins-ui-activeTab", - 3 // temporarily set to voting tab, default: 0 + 4 // default: Voting ); // HELPER FUNCTIONS From 38523d02d52bc640b443babed0e7c65bc0dd0b97 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 13:45:45 -0700 Subject: [PATCH 06/26] fix: cleanup and standardize sign-in This shows a button to connect wallet when the user is not logged in for areas that require one, in a consistent way. --- src/components/tabs/dashboard.tsx | 258 ++++++++++++------------ src/components/tabs/mining-claims.tsx | 17 +- src/components/tabs/redeem-nyc.tsx | 13 ++ src/components/tabs/stacking-claims.tsx | 17 +- src/components/votes/ccip-022.tsx | 10 +- 5 files changed, 180 insertions(+), 135 deletions(-) diff --git a/src/components/tabs/dashboard.tsx b/src/components/tabs/dashboard.tsx index 9096269..63fbb4d 100644 --- a/src/components/tabs/dashboard.tsx +++ b/src/components/tabs/dashboard.tsx @@ -46,140 +46,136 @@ function Dashboard() { } }; + if (!stxAddress) { + return ( + + CityCoins Dashboard + Wallet connection required to access dashboard. + + + ); + } + return ( CityCoins Dashboard - {stxAddress ? ( - <> - - {stxAddress} - {`${transactions.length} transactions detected`} - - - - {/* Transaction Stats and Filters */} - - - - Mining TXs - {miningTransactions.length} - - } - aria-label="Filter transactions" - title="Filter Transactions" - size="xs" - onClick={() => selectTransactions("mining")} - /> - - - - Mining Claim TXs - {miningClaimTransactions.length} - - } - aria-label="Filter transactions" - title="Filter Transactions" - size="xs" - onClick={() => selectTransactions("mining-claims")} - /> - - - - Stacking TXs - {stackingTransactions.length} - - } - aria-label="Filter transactions" - title="Filter Transactions" - size="xs" - onClick={() => selectTransactions("stacking")} - /> - - - - Stacking Claim TXs - {stackingClaimTransactions.length} - - } - aria-label="Filter transactions" - title="Filter Transactions" - size="xs" - onClick={() => selectTransactions("stacking-claims")} - /> - - - - Voting TXs - {votingTransactions.length} - - } - aria-label="Filter transactions" - title="Filter Transactions" - size="xs" - onClick={() => selectTransactions("voting")} - /> - - - - - - ) : ( - <> - Wallet connection required to access dashboard. - - - )} + + + {stxAddress} + {`${transactions.length} transactions detected`} + + + + {/* Transaction Stats and Filters */} + + + + Mining TXs + {miningTransactions.length} + + } + aria-label="Filter transactions" + title="Filter Transactions" + size="xs" + onClick={() => selectTransactions("mining")} + /> + + + + Mining Claim TXs + {miningClaimTransactions.length} + + } + aria-label="Filter transactions" + title="Filter Transactions" + size="xs" + onClick={() => selectTransactions("mining-claims")} + /> + + + + Stacking TXs + {stackingTransactions.length} + + } + aria-label="Filter transactions" + title="Filter Transactions" + size="xs" + onClick={() => selectTransactions("stacking")} + /> + + + + Stacking Claim TXs + {stackingClaimTransactions.length} + + } + aria-label="Filter transactions" + title="Filter Transactions" + size="xs" + onClick={() => selectTransactions("stacking-claims")} + /> + + + + Voting TXs + {votingTransactions.length} + + } + aria-label="Filter transactions" + title="Filter Transactions" + size="xs" + onClick={() => selectTransactions("voting")} + /> + + + + ); } diff --git a/src/components/tabs/mining-claims.tsx b/src/components/tabs/mining-claims.tsx index 715ae6a..b5265e9 100644 --- a/src/components/tabs/mining-claims.tsx +++ b/src/components/tabs/mining-claims.tsx @@ -1,7 +1,22 @@ -import { Heading, Stack } from "@chakra-ui/react"; +import { Heading, Stack, Text } from "@chakra-ui/react"; +import { useAtomValue } from "jotai"; +import { stxAddressAtom } from "../../store/stacks"; import ComingSoon from "../coming-soon"; +import SignIn from "../auth/sign-in"; function MiningClaims() { + const stxAddress = useAtomValue(stxAddressAtom); + + if (!stxAddress) { + return ( + + CityCoins Mining Claims + Wallet connection required to access mining claims. + + + ); + } + return ( CityCoins Mining Claims diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 68957fb..a953e6d 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -22,6 +22,8 @@ import { } from "@chakra-ui/react"; import { atom, useAtomValue } from "jotai"; import { LuExternalLink, LuRepeat } from "react-icons/lu"; +import { stxAddressAtom } from "../../store/stacks"; +import SignIn from "../auth/sign-in"; const v1BalanceNYCAtom = atom(0); const v2BalanceNYCAtom = atom(0); @@ -33,6 +35,7 @@ const totalBalanceNYCAtom = atom( const amountForBalanceAtom = atom(0); function RedeemNYC() { + const stxAddress = useAtomValue(stxAddressAtom); const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); const totalBalanceNYC = useAtomValue(totalBalanceNYCAtom); @@ -62,6 +65,16 @@ function RedeemNYC() { onClose(); }; + if (!stxAddress) { + return ( + + CityCoins NYC Redemption + Wallet connection required to access redemption. + + + ); + } + return ( CityCoins NYC Redemption diff --git a/src/components/tabs/stacking-claims.tsx b/src/components/tabs/stacking-claims.tsx index ec3c9ad..3c1c35a 100644 --- a/src/components/tabs/stacking-claims.tsx +++ b/src/components/tabs/stacking-claims.tsx @@ -1,7 +1,22 @@ -import { Heading, Stack } from "@chakra-ui/react"; +import { Heading, Stack, Text } from "@chakra-ui/react"; +import { useAtomValue } from "jotai"; +import { stxAddressAtom } from "../../store/stacks"; import ComingSoon from "../coming-soon"; +import SignIn from "../auth/sign-in"; function StackingClaims() { + const stxAddress = useAtomValue(stxAddressAtom); + + if (!stxAddress) { + return ( + + CityCoins Mining Claims + Wallet connection required to access stacking claims. + + + ); + } + return ( CityCoins Stacking Claims diff --git a/src/components/votes/ccip-022.tsx b/src/components/votes/ccip-022.tsx index 960abb9..7fe2fba 100644 --- a/src/components/votes/ccip-022.tsx +++ b/src/components/votes/ccip-022.tsx @@ -17,13 +17,19 @@ import { useAtomValue } from "jotai"; import { useCcip022VoteData } from "../../hooks/use-ccip-022-vote-data"; import { useCcip022VoteActions } from "../../hooks/use-ccip-022-vote-actions"; import { formatMicroAmount } from "../../store/common"; -import { hasVotedAtom } from "../../store/ccip-022"; -import { Ccip022VoteTotals } from "../../store/ccip-022"; +import { Ccip022VoteTotals, hasVotedAtom } from "../../store/ccip-022"; +import { stxAddressAtom } from "../../store/stacks"; +import SignIn from "../auth/sign-in"; import VoteProgressBarCCIP022 from "./vote-progress-bar-ccip022"; function VoteButtons() { const { voteYes, voteNo, isRequestPending } = useCcip022VoteActions(); const hasVoted = useAtomValue(hasVotedAtom); + const stxAddress = useAtomValue(stxAddressAtom); + + if (!stxAddress) { + return ; + } return ( <> From 11c1593f72e72d04eef38c0f3332a485cef7492e Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 15:33:21 -0700 Subject: [PATCH 07/26] fix: separate atoms and create local/derived/query variants with helpers --- src/store/ccd-012.ts | 335 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/store/ccd-012.ts diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts new file mode 100644 index 0000000..3d4d214 --- /dev/null +++ b/src/store/ccd-012.ts @@ -0,0 +1,335 @@ +import { atom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; +import { fetchReadOnlyFunction } from "micro-stacks/api"; +import { principalCV, uintCV } from "micro-stacks/clarity"; +import { stxAddressAtom } from "./stacks"; + +///////////////////////// +// TYPES +///////////////////////// + +type NycRedemptionInfo = { + redemptionsEnabled: boolean; + blockHeight: number; + totalSupply: number; + contractBalance: number; + redemptionRatio: number; +}; + +type AddressNycBalances = { + address: string; + balanceV1: number; + balanceV2: number; + totalBalance: number; +}; + +type AddressNycRedemptionInfo = { + address: string; + nycBalances: AddressNycBalances; + redemptionAmount: number; + redemptionClaims: number; +}; + +///////////////////////// +// CONSTANTS +///////////////////////// + +export const CONTRACT_ADDRESS = "SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH"; +export const CONTRACT_NAME = "ccd012-redemption-nyc"; +export const CONTRACT_FQ_NAME = `${CONTRACT_ADDRESS}.${CONTRACT_NAME}`; + +export const MICRO = (decimals: number) => Math.pow(10, decimals); + +///////////////////////// +// LOCALSTORAGE ATOMS +///////////////////////// + +const ccd012V1BalanceNYCLocalAtom = atomWithStorage( + "citycoins-ccd012-NYCV1Balance", + null +); + +const ccd012V2BalanceNYCLocalAtom = atomWithStorage( + "citycoins-ccd012-NYCV2Balance", + null +); + +export const ccd012IsRedemptionEnabledAtom = atomWithStorage( + "citycoins-ccd012-isRedemptionEnabled", + false +); + +export const ccd012RedemptionInfoAtom = + atomWithStorage( + "citycoins-ccd012-redemptionInfo", + null + ); + +export const ccd012NycBalancesAtom = atomWithStorage( + "citycoins-ccd012-nycBalances", + null +); + +export const ccd012RedemptionForBalanceAtom = atomWithStorage( + "citycoins-ccd012-redemptionForBalance", + null +); + +export const ccd012RedemptionAmountClaimedAtom = atomWithStorage( + "citycoins-ccd012-redemptionAmountClaimed", + null +); + +export const ccd012UserRedemptionInfoAtom = + atomWithStorage( + "citycoins-ccd012-userRedemptionInfo", + null + ); + +///////////////////////// +// DERIVED ATOMS +///////////////////////// + +export const v1BalanceNYCAtom = atom( + // getter + (get) => get(ccd012V1BalanceNYCLocalAtom), + // setter + async (get, set) => { + const balance = await get(v1BalanceNYCQueryAtom); + if (balance === undefined) return; + set(ccd012V1BalanceNYCLocalAtom, balance); + } +); + +export const v2BalanceNYCAtom = atom( + // getter + (get) => get(ccd012V2BalanceNYCLocalAtom), + // setter + async (get, set) => { + const balance = await get(v2BalanceNYCQueryAtom); + if (balance === undefined) return; + set(ccd012V2BalanceNYCLocalAtom, balance); + } +); + +export const totalBalanceNYCAtom = atom((get) => { + const v1Balance = (get(v1BalanceNYCAtom) ?? 0) * MICRO(6); + const v2Balance = get(v2BalanceNYCAtom) ?? 0; + return v1Balance + v2Balance; +}); + +///////////////////////// +// LOADABLE ASYNC ATOMS +///////////////////////// + +const v1BalanceNYCQueryAtom = atom(async (get) => { + const stxAddress = get(stxAddressAtom); + if (stxAddress === null) return undefined; + try { + const v1Balance = await getV1Balance(stxAddress); + return v1Balance; + } catch (error) { + throw new Error( + `Failed to fetch NYC V1 balance for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const v2BalanceNYCQueryAtom = atom(async (get) => { + const stxAddress = get(stxAddressAtom); + if (stxAddress === null) return undefined; + try { + const v2Balance = await getV2Balance(stxAddress); + return v2Balance; + } catch (error) { + throw new Error( + `Failed to fetch NYC V2 balance for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const isRedemptionEnabledQueryAtom = atom(async () => { + try { + const redemptionEnabled = await isRedemptionEnabled(); + return redemptionEnabled; + } catch (error) { + throw new Error( + `Failed to fetch is-redemption-enabled for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const redemptionInfoQueryAtom = atom(async () => { + try { + const redemptionInfo = await getRedemptionInfo(); + return redemptionInfo; + } catch (error) { + throw new Error( + `Failed to fetch redemption-info for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const nycBalancesQueryAtom = atom(async (get) => { + const stxAddress = get(stxAddressAtom); + if (stxAddress === null) return undefined; + try { + const nycBalances = await getNycBalances(stxAddress); + return nycBalances; + } catch (error) { + throw new Error( + `Failed to fetch nyc-balances for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const redemptionForBalanceQueryAtom = atom(async (get) => { + const totalBalance = get(totalBalanceNYCAtom); + try { + const redemptionForBalance = await getRedemptionForBalance(totalBalance); + return redemptionForBalance; + } catch (error) { + throw new Error( + `Failed to fetch redemption-for-balance for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const redemptionAmountClaimedQueryAtom = atom(async (get) => { + const stxAddress = get(stxAddressAtom); + if (stxAddress === null) return undefined; + try { + const redemptionAmountClaimed = await getRedemptionAmountClaimed( + stxAddress + ); + return redemptionAmountClaimed; + } catch (error) { + throw new Error( + `Failed to fetch redemption-amount-claimed for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +const userRedemptionInfoQueryAtom = atom(async (get) => { + const stxAddress = get(stxAddressAtom); + if (stxAddress === null) return undefined; + try { + const userRedemptionInfo = await getUserRedemptionInfo(stxAddress); + return userRedemptionInfo; + } catch (error) { + throw new Error( + `Failed to fetch user-redemption-info for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +///////////////////////// +// HELPER FUNCTIONS +///////////////////////// + +async function getV1Balance(address: string): Promise { + const v1TokenContractAddress = "SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5"; + const v1TokenContractName = "newyorkcitycoin-token"; + const v1Balance = await fetchReadOnlyFunction({ + contractAddress: v1TokenContractAddress, + contractName: v1TokenContractName, + functionName: "get-balance", + functionArgs: [principalCV(address)], + }); + return v1Balance; +} + +async function getV2Balance(address: string): Promise { + const v2TokenContractAddress = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11"; + const v2TokenContractName = "newyorkcitycoin-v2"; + const v2Balance = await fetchReadOnlyFunction({ + contractAddress: v2TokenContractAddress, + contractName: v2TokenContractName, + functionName: "get-balance", + functionArgs: [principalCV(address)], + }); + return v2Balance; +} + +async function isRedemptionEnabled(): Promise { + const isRedemptionEnabledQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "is-redemption-enabled", + functionArgs: [], + }, + true + ); + return isRedemptionEnabledQuery; +} + +async function getRedemptionInfo(): Promise { + const redemptionInfoQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-redemption-info", + functionArgs: [], + }, + true + ); + return redemptionInfoQuery; +} + +async function getNycBalances(address: string): Promise { + const nycBalancesQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-nyc-balances", + functionArgs: [address], + }, + true + ); + return nycBalancesQuery; +} + +async function getRedemptionForBalance( + balance: number +): Promise { + const redemptionForBalanceQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-redemption-for-balance", + functionArgs: [uintCV(balance)], + }, + true + ); + return redemptionForBalanceQuery; +} + +async function getRedemptionAmountClaimed(address: string): Promise { + const redemptionAmountClaimedQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-redemption-amount-claimed", + functionArgs: [principalCV(address)], + }, + true + ); + return redemptionAmountClaimedQuery; +} + +async function getUserRedemptionInfo( + address: string +): Promise { + const userRedemptionInfoQuery = + await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-user-redemption-info", + functionArgs: [principalCV(address)], + }, + true + ); + return userRedemptionInfoQuery; +} From 1a330a4aed1e4b49b6ae495221fe80256c4660c2 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 15:53:40 -0700 Subject: [PATCH 08/26] fix: add balance for nyc v1/v2 with refresh --- src/components/tabs/redeem-nyc.tsx | 43 +++++++++++++++++++----------- src/store/ccd-012.ts | 33 ++++++++++++++++++++--- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index a953e6d..175508e 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -20,32 +20,31 @@ import { Link, Divider, } from "@chakra-ui/react"; -import { atom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { LuExternalLink, LuRepeat } from "react-icons/lu"; import { stxAddressAtom } from "../../store/stacks"; import SignIn from "../auth/sign-in"; - -const v1BalanceNYCAtom = atom(0); -const v2BalanceNYCAtom = atom(0); - -const totalBalanceNYCAtom = atom( - (get) => get(v1BalanceNYCAtom) + get(v2BalanceNYCAtom) -); - -const amountForBalanceAtom = atom(0); +import { + totalBalanceNYCAtom, + v1BalanceNYCAtom, + v2BalanceNYCAtom, +} from "../../store/ccd-012"; +import { formatMicroAmount } from "../../store/common"; function RedeemNYC() { const stxAddress = useAtomValue(stxAddressAtom); - const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); - const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); + const [v1BalanceNYC, setV1BalanceNyc] = useAtom(v1BalanceNYCAtom); + const [v2BalanceNYC, setV2BalanceNyc] = useAtom(v2BalanceNYCAtom); const totalBalanceNYC = useAtomValue(totalBalanceNYCAtom); - const amountForBalance = useAtomValue(amountForBalanceAtom); + const amountForBalance = 0; // useAtomValue(amountForBalanceAtom); const { isOpen, onOpen, onClose } = useDisclosure(); const refreshBalances = () => { // Implement the logic to refresh balances here console.log("Refreshing balances..."); + setV1BalanceNyc(); + setV2BalanceNyc(); }; const redeemNYC = () => { @@ -86,18 +85,30 @@ function RedeemNYC() { V1 NYC Balance - {v1BalanceNYC} + {v1BalanceNYC ? ( + {formatMicroAmount(v1BalanceNYC)} + ) : ( + + (none detected) + + )} V2 NYC Balance - {v2BalanceNYC} + {v2BalanceNYC ? ( + {formatMicroAmount(v2BalanceNYC)} + ) : ( + + (none detected) + + )} Total NYC Balance - {totalBalanceNYC} + {formatMicroAmount(totalBalanceNYC)} Amount for Balance diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index 3d4d214..61f298b 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -97,7 +97,15 @@ export const v1BalanceNYCAtom = atom( async (get, set) => { const balance = await get(v1BalanceNYCQueryAtom); if (balance === undefined) return; - set(ccd012V1BalanceNYCLocalAtom, balance); + if (typeof balance === "bigint") { + try { + set(ccd012V1BalanceNYCLocalAtom, getBalanceFromBigint(balance)); + } catch (error) { + console.error(`Failed to set v1BalanceAtom with bigint: ${error}`); + } + } else if (typeof balance === "number") { + set(ccd012V1BalanceNYCLocalAtom, balance); + } } ); @@ -108,7 +116,15 @@ export const v2BalanceNYCAtom = atom( async (get, set) => { const balance = await get(v2BalanceNYCQueryAtom); if (balance === undefined) return; - set(ccd012V2BalanceNYCLocalAtom, balance); + if (typeof balance === "bigint") { + try { + set(ccd012V2BalanceNYCLocalAtom, getBalanceFromBigint(balance)); + } catch (error) { + console.error(`Failed to set v2BalanceAtom with bigint: ${error}`); + } + } else if (typeof balance === "number") { + set(ccd012V2BalanceNYCLocalAtom, balance); + } } ); @@ -227,6 +243,17 @@ const userRedemptionInfoQueryAtom = atom(async (get) => { // HELPER FUNCTIONS ///////////////////////// +function getBalanceFromBigint(balance: bigint): number { + const numberBalance = Number(balance); + if (Number.isSafeInteger(numberBalance)) { + return numberBalance; + } else { + throw new Error( + "BigInt value is too large to be safely converted to number" + ); + } +} + async function getV1Balance(address: string): Promise { const v1TokenContractAddress = "SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5"; const v1TokenContractName = "newyorkcitycoin-token"; @@ -241,7 +268,7 @@ async function getV1Balance(address: string): Promise { async function getV2Balance(address: string): Promise { const v2TokenContractAddress = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11"; - const v2TokenContractName = "newyorkcitycoin-v2"; + const v2TokenContractName = "newyorkcitycoin-token-v2"; const v2Balance = await fetchReadOnlyFunction({ contractAddress: v2TokenContractAddress, contractName: v2TokenContractName, From e8ecdc93c70995583ad8ad804486a32181afd0bb Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 16:04:53 -0700 Subject: [PATCH 09/26] fix: mmm toast --- src/components/tabs/redeem-nyc.tsx | 34 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 175508e..716570b 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -19,6 +19,7 @@ import { ModalCloseButton, Link, Divider, + useToast, } from "@chakra-ui/react"; import { useAtom, useAtomValue } from "jotai"; import { LuExternalLink, LuRepeat } from "react-icons/lu"; @@ -32,6 +33,7 @@ import { import { formatMicroAmount } from "../../store/common"; function RedeemNYC() { + const toast = useToast(); const stxAddress = useAtomValue(stxAddressAtom); const [v1BalanceNYC, setV1BalanceNyc] = useAtom(v1BalanceNYCAtom); const [v2BalanceNYC, setV2BalanceNyc] = useAtom(v2BalanceNYCAtom); @@ -41,26 +43,46 @@ function RedeemNYC() { const { isOpen, onOpen, onClose } = useDisclosure(); const refreshBalances = () => { - // Implement the logic to refresh balances here + toast({ + title: "Refreshing balances...", + status: "info", + duration: 2000, + isClosable: true, + }); console.log("Refreshing balances..."); setV1BalanceNyc(); setV2BalanceNyc(); }; const redeemNYC = () => { - // Implement the logic to redeem NYC here + toast({ + title: "Redeeming NYC...", + status: "info", + duration: 2000, + isClosable: true, + }); console.log("Redeeming NYC..."); }; const redeemForStSTX = () => { - // Implement the logic to redeem for stSTX here - console.log("Redeeming for stSTX..."); + toast({ + title: "Redeeming NYC for stSTX...", + status: "info", + duration: 2000, + isClosable: true, + }); + console.log("Redeeming NYC for stSTX..."); onClose(); }; const redeemForLiSTX = () => { - // Implement the logic to redeem for liSTX here - console.log("Redeeming for liSTX..."); + toast({ + title: "Redeeming NYC for liSTX...", + status: "info", + duration: 2000, + isClosable: true, + }); + console.log("Redeeming NYC for liSTX..."); onClose(); }; From 763d6351ccdc8f818c3342f9d2f6ec2d526794aa Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 16:19:13 -0700 Subject: [PATCH 10/26] fix: add update for redemption amount from contract --- src/components/tabs/redeem-nyc.tsx | 14 ++++++++++++-- src/store/ccd-012.ts | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 716570b..9928bee 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -26,6 +26,7 @@ import { LuExternalLink, LuRepeat } from "react-icons/lu"; import { stxAddressAtom } from "../../store/stacks"; import SignIn from "../auth/sign-in"; import { + redemptionForBalanceAtom, totalBalanceNYCAtom, v1BalanceNYCAtom, v2BalanceNYCAtom, @@ -38,7 +39,9 @@ function RedeemNYC() { const [v1BalanceNYC, setV1BalanceNyc] = useAtom(v1BalanceNYCAtom); const [v2BalanceNYC, setV2BalanceNyc] = useAtom(v2BalanceNYCAtom); const totalBalanceNYC = useAtomValue(totalBalanceNYCAtom); - const amountForBalance = 0; // useAtomValue(amountForBalanceAtom); + const [redemptionForBalance, setRedemptionForBalance] = useAtom( + redemptionForBalanceAtom + ); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -52,6 +55,7 @@ function RedeemNYC() { console.log("Refreshing balances..."); setV1BalanceNyc(); setV2BalanceNyc(); + setRedemptionForBalance(); }; const redeemNYC = () => { @@ -134,7 +138,13 @@ function RedeemNYC() { Amount for Balance - {amountForBalance} + {redemptionForBalance ? ( + {formatMicroAmount(redemptionForBalance)} + ) : ( + + (none detected) + + )} diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index 61f298b..7ba73c2 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -134,6 +134,26 @@ export const totalBalanceNYCAtom = atom((get) => { return v1Balance + v2Balance; }); +export const isRedemptionEnabledAtom = atom( + // getter + (get) => get(ccd012IsRedemptionEnabledAtom), + // setter + async (get, set) => { + const isRedemptionEnabled = await get(isRedemptionEnabledQueryAtom); + set(ccd012IsRedemptionEnabledAtom, isRedemptionEnabled); + } +); + +export const redemptionForBalanceAtom = atom( + // getter + (get) => get(ccd012RedemptionForBalanceAtom), + // setter + async (get, set) => { + const redemptionForBalance = await get(redemptionForBalanceQueryAtom); + set(ccd012RedemptionForBalanceAtom, redemptionForBalance); + } +); + ///////////////////////// // LOADABLE ASYNC ATOMS ///////////////////////// From f15167c9bd2144fb2235b792f6f0cdaf13b4420a Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 16:24:07 -0700 Subject: [PATCH 11/26] fix: add missing derived atoms with new structure Separates functions, atoms, storage, and exported items in a clean way to use moving forward. --- src/store/ccd-012.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index 7ba73c2..e170c6b 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -144,6 +144,27 @@ export const isRedemptionEnabledAtom = atom( } ); +export const redemptionInfoAtom = atom( + // getter + (get) => get(ccd012RedemptionInfoAtom), + // setter + async (get, set) => { + const redemptionInfo = await get(redemptionInfoQueryAtom); + set(ccd012RedemptionInfoAtom, redemptionInfo); + } +); + +export const nycBalancesAtom = atom( + // getter + (get) => get(ccd012NycBalancesAtom), + // setter + async (get, set) => { + const nycBalances = await get(nycBalancesQueryAtom); + if (!nycBalances) return; + set(ccd012NycBalancesAtom, nycBalances); + } +); + export const redemptionForBalanceAtom = atom( // getter (get) => get(ccd012RedemptionForBalanceAtom), @@ -154,6 +175,28 @@ export const redemptionForBalanceAtom = atom( } ); +export const redemptionAmountClaimed = atom( + // getter + (get) => get(ccd012RedemptionAmountClaimedAtom), + // setter + async (get, set) => { + const redemptionAmountClaimed = await get(redemptionAmountClaimedQueryAtom); + if (!redemptionAmountClaimed) return; + set(ccd012RedemptionAmountClaimedAtom, redemptionAmountClaimed); + } +); + +export const userRedemptionInfoAtom = atom( + // getter + (get) => get(ccd012UserRedemptionInfoAtom), + // setter + async (get, set) => { + const userRedemptionInfo = await get(userRedemptionInfoQueryAtom); + if (!userRedemptionInfo) return; + set(ccd012UserRedemptionInfoAtom, userRedemptionInfo); + } +); + ///////////////////////// // LOADABLE ASYNC ATOMS ///////////////////////// From 264685ec5b4383d1e9ceaf4e7feefd78a7903b82 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 5 Jul 2024 16:38:14 -0700 Subject: [PATCH 12/26] fix: add hook for redeem nyc contract call --- src/components/tabs/redeem-nyc.tsx | 8 ++++++-- src/hooks/use-ccd-012.tsx | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/hooks/use-ccd-012.tsx diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 9928bee..4d5644b 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -32,9 +32,12 @@ import { v2BalanceNYCAtom, } from "../../store/ccd-012"; import { formatMicroAmount } from "../../store/common"; +import { useCcd012RedeemNyc } from "../../hooks/use-ccd-012"; function RedeemNYC() { const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const stxAddress = useAtomValue(stxAddressAtom); const [v1BalanceNYC, setV1BalanceNyc] = useAtom(v1BalanceNYCAtom); const [v2BalanceNYC, setV2BalanceNyc] = useAtom(v2BalanceNYCAtom); @@ -43,7 +46,7 @@ function RedeemNYC() { redemptionForBalanceAtom ); - const { isOpen, onOpen, onClose } = useDisclosure(); + const { redeemNycCall, isRequestPending } = useCcd012RedeemNyc(); const refreshBalances = () => { toast({ @@ -66,6 +69,7 @@ function RedeemNYC() { isClosable: true, }); console.log("Redeeming NYC..."); + redeemNycCall(); }; const redeemForStSTX = () => { @@ -149,7 +153,7 @@ function RedeemNYC() { - - + - Redeem NYC for stSTX / liSTX + Redeem NYC and Stack STX @@ -176,46 +180,76 @@ function RedeemNYC() { LISA {" "} - have partnered to offer stSTX and liSTX. + have partnered to offer redemption for stSTX and liSTX. Please review the resources below before proceeding to fully understand the process through their platform. - Please be aware of the risks associated with redeeming for stSTX - or liSTX. StackingDAO and LISA are not affiliated with CityCoins. + Please be aware of how each protocol operates and the associated + risks for stSTX or liSTX before continuing. - - Official Resources: - - StackingDAO Website + + Official StackingDAO Resources + + + StackingDAO Website + + - StackingDAO Discord + + StackingDAO Community + + - - Lido Finance Website + Official LISA Resources + + + LISA Website + + - - Lido Finance Discord + + + LISA Community + + + - - - - + - + From c486ab1709d1c13cd7954c6b34887061f3662f8b Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sat, 6 Jul 2024 23:15:36 -0700 Subject: [PATCH 16/26] fix: checkbox value should use consent atom value --- src/components/tabs/redeem-nyc.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 4aa6b15..6ae2e8f 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -223,6 +223,7 @@ function RedeemNYC() { setConsentChecked(e.target.checked)} mb={4} > From 5a15629daa65fa0a45321596ab173e5d07dd76e8 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sun, 7 Jul 2024 15:15:01 -0700 Subject: [PATCH 17/26] fix: add majority of stackingdao code --- src/hooks/use-ccd-012.tsx | 56 +++++++++++++++++++++++++++++++++++++++ src/store/ccd-012.ts | 6 +++++ 2 files changed, 62 insertions(+) diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 2fcae2a..6956a57 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -1,4 +1,6 @@ import { useToast } from "@chakra-ui/react"; +import { FinishedTxData } from "micro-stacks/connect"; +import { ClarityValue } from "micro-stacks/clarity"; import { useOpenContractCall } from "@micro-stacks/react"; import { createAssetInfo, @@ -15,12 +17,27 @@ import { NYC_V2_CONTRACT_ADDRESS, NYC_V2_CONTRACT_NAME, redemptionForBalanceAtom, + STACKING_DAO_CONTRACT_ADDRESS, + STACKING_DAO_CONTRACT_NAME, + STACKING_DAO_FUNCTION_NAME, v1BalanceNYCAtom, v2BalanceNYCAtom, } from "../store/ccd-012"; import { useAtomValue } from "jotai"; import { stxAddressAtom } from "../store/stacks"; +const onFinishToast = (tx: FinishedTxData, toast: any) => { + toast({ + title: "Redemption TX Sent", + status: "success", + description: `View on explorer:\nhttps://explorer.hiro.so/txid/${tx.txId}?chain=mainnet`, + }); +}; + +const onCancelToast = (toast: any) => { + toast({ title: "Redemption Cancelled", status: "warning" }); +}; + export const useCcd012RedeemNyc = () => { const toast = useToast(); const stxAddress = useAtomValue(stxAddressAtom); @@ -93,3 +110,42 @@ export const useCcd012RedeemNyc = () => { return { redeemNycCall, isRequestPending }; }; + +export const useCcd012StackingDao = () => { + const toast = useToast(); + const stxAddress = useAtomValue(stxAddressAtom); + const { openContractCall, isRequestPending } = useOpenContractCall(); + + const functionArgs: string[] = []; + // function args: + // - reserve: + // - commission: + // - staking: + // - direct-helpers: + // - referrer: (optional principal) + // - pool: (optional principal) + + const postConditions: string[] = []; + // post conditions: + // - burn nyc v1 (balance from user) + // - burn nyc v2 (balance from user) + // - xfer redemption-stx from contract (to user) + // - xfer redemption-stx from user (to StackingDAO) + // - xfer stSTX from contract (query amount) + + const contractCallParams = { + contractAddress: STACKING_DAO_CONTRACT_ADDRESS, + contractName: STACKING_DAO_CONTRACT_NAME, + functionName: STACKING_DAO_FUNCTION_NAME, + functionArgs: functionArgs, + postConditions: postConditions, + onFinish: (finishedTx: FinishedTxData) => onFinishToast(finishedTx, toast), + onCancel: () => onCancelToast(toast), + }; + + const stackingDaoCall = async () => { + await openContractCall(contractCallParams); + }; + + return { stackingDaoCall, isRequestPending }; +}; diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index 70798af..f27745c 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -48,6 +48,12 @@ export const NYC_V2_CONTRACT_ADDRESS = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11"; export const NYC_V2_CONTRACT_NAME = "newyorkcitycoin-token-v2"; +export const STACKING_DAO_CONTRACT_ADDRESS = + "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG"; +export const STACKING_DAO_CONTRACT_NAME = "cc-redemption-v1"; +export const STACKING_DAO_FQ_NAME = `${STACKING_DAO_CONTRACT_ADDRESS}.${STACKING_DAO_CONTRACT_NAME}`; +export const STACKING_DAO_FUNCTION_NAME = "deposit"; + export const MICRO = (decimals: number) => Math.pow(10, decimals); ///////////////////////// From 5ad0314cea84644370228f898c898c6cd784656a Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sun, 7 Jul 2024 15:22:59 -0700 Subject: [PATCH 18/26] fix: generalize base post conditions before wrappers --- src/hooks/use-ccd-012.tsx | 50 ++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 6956a57..a7eff4b 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -1,12 +1,12 @@ import { useToast } from "@chakra-ui/react"; import { FinishedTxData } from "micro-stacks/connect"; -import { ClarityValue } from "micro-stacks/clarity"; import { useOpenContractCall } from "@micro-stacks/react"; import { createAssetInfo, createFungiblePostCondition, FungibleConditionCode, makeContractSTXPostCondition, + PostCondition, } from "micro-stacks/transactions"; import { CONTRACT_ADDRESS, @@ -38,19 +38,14 @@ const onCancelToast = (toast: any) => { toast({ title: "Redemption Cancelled", status: "warning" }); }; -export const useCcd012RedeemNyc = () => { - const toast = useToast(); - const stxAddress = useAtomValue(stxAddressAtom); - const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); - const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); - const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); - const { openContractCall, isRequestPending } = useOpenContractCall(); - - // can set a state atom here for UI feedback - - const postConditions = []; - +function buildRedemptionPostConditions( + stxAddress: null | string, + v1BalanceNYC: null | number, + v2BalanceNYC: null | number, + redemptionForBalance: null | number +) { if (stxAddress) { + const postConditions: PostCondition[] = []; // add v1 post condition if needed if (v1BalanceNYC !== null && v1BalanceNYC > 0) { postConditions.push( @@ -92,7 +87,26 @@ export const useCcd012RedeemNyc = () => { ) ); } + return postConditions; } +} + +export const useCcd012RedeemNyc = () => { + const toast = useToast(); + const stxAddress = useAtomValue(stxAddressAtom); + const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); + const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); + const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); + const { openContractCall, isRequestPending } = useOpenContractCall(); + + // can set a state atom here for UI feedback + + const postConditions = buildRedemptionPostConditions( + stxAddress, + v1BalanceNYC, + v2BalanceNYC, + redemptionForBalance + ); const contractCallParams = { contractAddress: CONTRACT_ADDRESS, @@ -114,6 +128,9 @@ export const useCcd012RedeemNyc = () => { export const useCcd012StackingDao = () => { const toast = useToast(); const stxAddress = useAtomValue(stxAddressAtom); + const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); + const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); + const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); const { openContractCall, isRequestPending } = useOpenContractCall(); const functionArgs: string[] = []; @@ -125,7 +142,12 @@ export const useCcd012StackingDao = () => { // - referrer: (optional principal) // - pool: (optional principal) - const postConditions: string[] = []; + const postConditions = buildRedemptionPostConditions( + stxAddress, + v1BalanceNYC, + v2BalanceNYC, + redemptionForBalance + ); // post conditions: // - burn nyc v1 (balance from user) // - burn nyc v2 (balance from user) From bf719f82b7dcb7fc5aa79e351024cb0d9ae33a9e Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sun, 7 Jul 2024 15:33:15 -0700 Subject: [PATCH 19/26] fix: add lisa integration code --- src/hooks/use-ccd-012.tsx | 55 ++++++++++++++++++++++++++++++++++++--- src/store/ccd-012.ts | 5 ++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index a7eff4b..85efb59 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -4,6 +4,7 @@ import { useOpenContractCall } from "@micro-stacks/react"; import { createAssetInfo, createFungiblePostCondition, + createSTXPostCondition, FungibleConditionCode, makeContractSTXPostCondition, PostCondition, @@ -11,6 +12,9 @@ import { import { CONTRACT_ADDRESS, CONTRACT_NAME, + LISA_CONTRACT_ADDRESS, + LISA_CONTRACT_NAME, + LISA_FUNCTION_NAME, NYC_ASSET_NAME, NYC_V1_CONTRACT_ADDRESS, NYC_V1_CONTRACT_NAME, @@ -148,10 +152,7 @@ export const useCcd012StackingDao = () => { v2BalanceNYC, redemptionForBalance ); - // post conditions: - // - burn nyc v1 (balance from user) - // - burn nyc v2 (balance from user) - // - xfer redemption-stx from contract (to user) + // add to post conditions: // - xfer redemption-stx from user (to StackingDAO) // - xfer stSTX from contract (query amount) @@ -171,3 +172,49 @@ export const useCcd012StackingDao = () => { return { stackingDaoCall, isRequestPending }; }; + +export const useCcd012Lisa = () => { + const toast = useToast(); + const stxAddress = useAtomValue(stxAddressAtom); + const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); + const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); + const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); + const { openContractCall, isRequestPending } = useOpenContractCall(); + + const functionArgs: string[] = []; + // function args: (none) + + const postConditions = buildRedemptionPostConditions( + stxAddress, + v1BalanceNYC, + v2BalanceNYC, + redemptionForBalance + ); + // add to post conditions: + // - xfer redemption-stx from user (to LISA) + if (stxAddress && postConditions) { + postConditions.push( + createSTXPostCondition( + stxAddress, + FungibleConditionCode.Equal, + redemptionForBalance! + ) + ); + } + + const contractCallParams = { + contractAddress: LISA_CONTRACT_ADDRESS, + contractName: LISA_CONTRACT_NAME, + functionName: LISA_FUNCTION_NAME, + functionArgs: functionArgs, + postConditions: postConditions, + onFinish: (finishedTx: FinishedTxData) => onFinishToast(finishedTx, toast), + onCancel: () => onCancelToast(toast), + }; + + const lisaCall = async () => { + await openContractCall(contractCallParams); + }; + + return { lisaCall, isRequestPending }; +}; diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index f27745c..58344fd 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -54,6 +54,11 @@ export const STACKING_DAO_CONTRACT_NAME = "cc-redemption-v1"; export const STACKING_DAO_FQ_NAME = `${STACKING_DAO_CONTRACT_ADDRESS}.${STACKING_DAO_CONTRACT_NAME}`; export const STACKING_DAO_FUNCTION_NAME = "deposit"; +export const LISA_CONTRACT_ADDRESS = "SPGAB1P3YV109E22KXFJYM63GK0G21BYX50CQ80B"; +export const LISA_CONTRACT_NAME = "redeem-nyc-for-listx"; +export const LISA_FQ_NAME = `${LISA_CONTRACT_ADDRESS}.${LISA_CONTRACT_NAME}`; +export const LISA_FUNCTION_NAME = "redeem-nyc-and-stack-with-lisa"; + export const MICRO = (decimals: number) => Math.pow(10, decimals); ///////////////////////// From a461d7734cf096e1b283a297617249edaee75df7 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sun, 7 Jul 2024 15:43:58 -0700 Subject: [PATCH 20/26] fix: implement new hooks for stackingdao/lisa redemptions --- src/components/tabs/redeem-nyc.tsx | 13 ++++++++++++- src/hooks/use-ccd-012.tsx | 6 +++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 6ae2e8f..85f01bd 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -33,7 +33,11 @@ import { v2BalanceNYCAtom, } from "../../store/ccd-012"; import { formatAmount, formatMicroAmount } from "../../store/common"; -import { useCcd012RedeemNyc } from "../../hooks/use-ccd-012"; +import { + useCcd012RedeemNyc, + useCcd012StackingDao, + useCcd012Lisa, +} from "../../hooks/use-ccd-012"; const consentCheckedAtom = atom(false); @@ -51,6 +55,9 @@ function RedeemNYC() { ); const { redeemNycCall, isRequestPending } = useCcd012RedeemNyc(); + const { stackingDaoCall, isRequestPending: isRequestPendingStackingDAO } = + useCcd012StackingDao(); + const { lisaCall, isRequestPending: isRequestPendingLisa } = useCcd012Lisa(); const refreshBalances = () => { toast({ @@ -85,6 +92,7 @@ function RedeemNYC() { }); console.log("Redeeming NYC for stSTX..."); onClose(); + stackingDaoCall(); }; const redeemForLiSTX = () => { @@ -96,6 +104,7 @@ function RedeemNYC() { }); console.log("Redeeming NYC for liSTX..."); onClose(); + lisaCall(); }; if (!stxAddress) { @@ -239,6 +248,7 @@ function RedeemNYC() { onClick={redeemForStSTX} width="full" isDisabled={!consentChecked} + isLoading={isRequestPendingStackingDAO} > Redeem stSTX @@ -247,6 +257,7 @@ function RedeemNYC() { onClick={redeemForLiSTX} width="full" isDisabled={!consentChecked} + isLoading={isRequestPendingLisa} > Redeem liSTX diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 85efb59..144e2e9 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -118,8 +118,8 @@ export const useCcd012RedeemNyc = () => { functionName: "redeem-nyc", functionArgs: [], postConditions, - onFinish: () => toast({ title: "Redemption TX Sent", status: "success" }), - onCancel: () => toast({ title: "Redemption Cancelled", status: "warning" }), + onFinish: (finishedTx: FinishedTxData) => onFinishToast(finishedTx, toast), + onCancel: () => onCancelToast(toast), }; const redeemNycCall = async () => { @@ -197,7 +197,7 @@ export const useCcd012Lisa = () => { createSTXPostCondition( stxAddress, FungibleConditionCode.Equal, - redemptionForBalance! + redemptionForBalance ?? 0 ) ); } From f0a2e7a11dbdffc61bd1b60cc25ccc56ff33b313 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sun, 7 Jul 2024 15:47:57 -0700 Subject: [PATCH 21/26] fix: consistent toast is the best toast --- src/components/tabs/redeem-nyc.tsx | 11 ++++++++--- src/hooks/use-ccd-012.tsx | 13 +++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 85f01bd..94978a5 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -65,6 +65,8 @@ function RedeemNYC() { status: "info", duration: 2000, isClosable: true, + position: "top", + variant: "solid", }); console.log("Refreshing balances..."); setV1BalanceNyc(); @@ -76,8 +78,9 @@ function RedeemNYC() { toast({ title: "Redeeming NYC...", status: "info", - duration: 2000, isClosable: true, + position: "top", + variant: "solid", }); console.log("Redeeming NYC..."); redeemNycCall(); @@ -87,8 +90,9 @@ function RedeemNYC() { toast({ title: "Redeeming NYC for stSTX...", status: "info", - duration: 2000, isClosable: true, + position: "top", + variant: "solid", }); console.log("Redeeming NYC for stSTX..."); onClose(); @@ -99,8 +103,9 @@ function RedeemNYC() { toast({ title: "Redeeming NYC for liSTX...", status: "info", - duration: 2000, isClosable: true, + position: "top", + variant: "solid", }); console.log("Redeeming NYC for liSTX..."); onClose(); diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 144e2e9..e72d4a0 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -33,13 +33,22 @@ import { stxAddressAtom } from "../store/stacks"; const onFinishToast = (tx: FinishedTxData, toast: any) => { toast({ title: "Redemption TX Sent", - status: "success", description: `View on explorer:\nhttps://explorer.hiro.so/txid/${tx.txId}?chain=mainnet`, + status: "success", + position: "top", + variant: "solid", + isClosable: true, }); }; const onCancelToast = (toast: any) => { - toast({ title: "Redemption Cancelled", status: "warning" }); + toast({ + title: "Redemption Cancelled", + status: "warning", + position: "top", + variant: "solid", + isClosable: true, + }); }; function buildRedemptionPostConditions( From 2a2e1660fd4a9112d3fb959653706be121889095 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sun, 7 Jul 2024 22:17:30 -0700 Subject: [PATCH 22/26] fix: update post conditions and fn args for stackingdao Almost there, needs a useEffect() for stSTXRatio since it's an async call with fetchReadOnly --- src/hooks/use-ccd-012.tsx | 57 ++++++++++++++++++++++++++++++++------- src/store/ccip-022.ts | 20 +++++++++++++- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index e72d4a0..9191850 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -1,4 +1,5 @@ import { useToast } from "@chakra-ui/react"; +import { ClarityValue, noneCV, principalCV } from "micro-stacks/clarity"; import { FinishedTxData } from "micro-stacks/connect"; import { useOpenContractCall } from "@micro-stacks/react"; import { @@ -29,6 +30,7 @@ import { } from "../store/ccd-012"; import { useAtomValue } from "jotai"; import { stxAddressAtom } from "../store/stacks"; +import { getStackingDaoRatio } from "../store/ccip-022"; const onFinishToast = (tx: FinishedTxData, toast: any) => { toast({ @@ -146,24 +148,59 @@ export const useCcd012StackingDao = () => { const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); const { openContractCall, isRequestPending } = useOpenContractCall(); - const functionArgs: string[] = []; - // function args: - // - reserve: - // - commission: - // - staking: - // - direct-helpers: - // - referrer: (optional principal) - // - pool: (optional principal) - const postConditions = buildRedemptionPostConditions( stxAddress, v1BalanceNYC, v2BalanceNYC, redemptionForBalance ); + + if (!stxAddress || !postConditions) + // return stub function and false for isRequestPending + return { stackingDaoCall: () => {}, isRequestPending: false }; + // add to post conditions: // - xfer redemption-stx from user (to StackingDAO) - // - xfer stSTX from contract (query amount) + postConditions.push( + createSTXPostCondition( + stxAddress, + FungibleConditionCode.Equal, + redemptionForBalance ?? 0 + ) + ); + + // - xfer stSTX from contract (query amount from contract) + // stSTX token: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token + const stSTXRatio = getStackingDaoRatio(); + if (stSTXRatio) { + postConditions.push( + createFungiblePostCondition( + STACKING_DAO_CONTRACT_ADDRESS, + FungibleConditionCode.Equal, + redemptionForBalance ?? 0, + createAssetInfo(STACKING_DAO_CONTRACT_ADDRESS, "ststx-token", "stSTX") + ) + ); + } + + // function args based on a test tx from the UI: + const functionArgs: ClarityValue[] = []; + // - reserve: reserve-v1 + functionArgs.push(principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.reserve-v1`)); + // - commission: commission-v2 + functionArgs.push( + principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.commission-v2`) + ); + // - staking: staking-v0 + functionArgs.push(principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.staking-v0`)); + // - direct-helpers: direct-helpers-v1 + functionArgs.push( + principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.direct-helpers-v1`) + ); + // - referrer: none + functionArgs.push(noneCV()); + // - pool: none + functionArgs.push(noneCV()); const contractCallParams = { contractAddress: STACKING_DAO_CONTRACT_ADDRESS, diff --git a/src/store/ccip-022.ts b/src/store/ccip-022.ts index 6490978..19b23c0 100644 --- a/src/store/ccip-022.ts +++ b/src/store/ccip-022.ts @@ -2,8 +2,9 @@ import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { fetchReadOnlyFunction } from "micro-stacks/api"; import { validateStacksAddress } from "micro-stacks/crypto"; -import { standardPrincipalCV, uintCV } from "micro-stacks/clarity"; +import { principalCV, standardPrincipalCV, uintCV } from "micro-stacks/clarity"; import { stxAddressAtom } from "./stacks"; +import { STACKING_DAO_CONTRACT_ADDRESS } from "./ccd-012"; ///////////////////////// // TYPES @@ -199,3 +200,20 @@ async function getVoterInfo(voterAddress: string): Promise { // console.log("Voter Info", voterInfoQuery); return voterInfoQuery; } + +// helper to get the stSTX to STX ratio from the StackingDAO contract +// calls `get-stx-per-ststx` on `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.data-core-v1` +// The param `reserve-contract` should be `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.reserve-v1`. +// example: return `u1015555`, meaning for 1.015555 STX you will get 1 stSTX. +export async function getStackingDaoRatio(): Promise { + const stackingDaoRatioQuery = await fetchReadOnlyFunction( + { + contractAddress: STACKING_DAO_CONTRACT_ADDRESS, + contractName: "data-core-v1", + functionName: "get-stx-per-ststx", + functionArgs: [principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.reserve-1`)], + }, + true + ); + return stackingDaoRatioQuery; +} From 538cc088850bd04f7e3f432b161e78cd4a1067d2 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 8 Jul 2024 09:14:06 -0700 Subject: [PATCH 23/26] fix: refactor stSTX ratio lookup into useEffect --- src/hooks/use-ccd-012.tsx | 22 +++++++++++++++++++--- src/store/ccd-012.ts | 2 ++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 9191850..67bacdb 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -25,12 +25,14 @@ import { STACKING_DAO_CONTRACT_ADDRESS, STACKING_DAO_CONTRACT_NAME, STACKING_DAO_FUNCTION_NAME, + stSTXRatioAtom, v1BalanceNYCAtom, v2BalanceNYCAtom, } from "../store/ccd-012"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { stxAddressAtom } from "../store/stacks"; import { getStackingDaoRatio } from "../store/ccip-022"; +import { useEffect } from "react"; const onFinishToast = (tx: FinishedTxData, toast: any) => { toast({ @@ -146,8 +148,23 @@ export const useCcd012StackingDao = () => { const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); + const [stSTXRatio, setStSTXRatio] = useAtom(stSTXRatioAtom); const { openContractCall, isRequestPending } = useOpenContractCall(); + useEffect(() => { + const fetchSTXRatio = async () => { + try { + const ratio = await getStackingDaoRatio(); + setStSTXRatio(ratio); + } catch (error) { + console.error("Failed to fetch stSTX ratio:", error); + setStSTXRatio(null); + } + }; + + fetchSTXRatio(); + }, [setStSTXRatio]); + const postConditions = buildRedemptionPostConditions( stxAddress, v1BalanceNYC, @@ -170,8 +187,7 @@ export const useCcd012StackingDao = () => { ); // - xfer stSTX from contract (query amount from contract) - // stSTX token: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token - const stSTXRatio = getStackingDaoRatio(); + // - stSTX token: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token if (stSTXRatio) { postConditions.push( createFungiblePostCondition( diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index 58344fd..df7b067 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -111,6 +111,8 @@ export const ccd012UserRedemptionInfoAtom = // DERIVED ATOMS ///////////////////////// +export const stSTXRatioAtom = atom(null); + export const v1BalanceNYCAtom = atom( // getter (get) => get(ccd012V1BalanceNYCLocalAtom), From dcb1093ee06dac7fd17594f21a91011ace8a7389 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 8 Jul 2024 09:20:15 -0700 Subject: [PATCH 24/26] fix: typo in reserve contract name --- src/store/ccip-022.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/store/ccip-022.ts b/src/store/ccip-022.ts index 19b23c0..4fef92a 100644 --- a/src/store/ccip-022.ts +++ b/src/store/ccip-022.ts @@ -211,7 +211,9 @@ export async function getStackingDaoRatio(): Promise { contractAddress: STACKING_DAO_CONTRACT_ADDRESS, contractName: "data-core-v1", functionName: "get-stx-per-ststx", - functionArgs: [principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.reserve-1`)], + functionArgs: [ + principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.reserve-v1`), + ], }, true ); From 0a0aeb1049f5befe83f00b89bb6e7c33f687e98e Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 8 Jul 2024 10:12:02 -0700 Subject: [PATCH 25/26] fix: store txid once sent and display explorer link --- src/components/tabs/redeem-nyc.tsx | 75 ++++++++++++++++++++++++++---- src/hooks/use-ccd-012.tsx | 37 ++++++++------- src/store/ccd-012.ts | 47 ++++++++++++++++++- src/store/ccip-022.ts | 19 -------- 4 files changed, 133 insertions(+), 45 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 94978a5..7ce200c 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -27,6 +27,7 @@ import { LuExternalLink, LuRepeat } from "react-icons/lu"; import { stxAddressAtom } from "../../store/stacks"; import SignIn from "../auth/sign-in"; import { + ccd012TxIdAtom, redemptionForBalanceAtom, totalBalanceNYCAtom, v1BalanceNYCAtom, @@ -53,6 +54,7 @@ function RedeemNYC() { const [redemptionForBalance, setRedemptionForBalance] = useAtom( redemptionForBalanceAtom ); + const ccd012TxId = useAtomValue(ccd012TxIdAtom); const { redeemNycCall, isRequestPending } = useCcd012RedeemNyc(); const { stackingDaoCall, isRequestPending: isRequestPendingStackingDAO } = @@ -74,6 +76,37 @@ function RedeemNYC() { setRedemptionForBalance(); }; + const readyToRedeem = () => { + let toastMsg = ""; + if (!consentChecked) { + toastMsg = "Please read and acknowledge the disclaimer."; + } + if (!stxAddress) { + toastMsg = + "No STX address detected, please log out and reconnect your wallet."; + } + if (!v1BalanceNYC || !v2BalanceNYC) { + toastMsg = + "No NYC balance detected, please refresh balances and try again."; + } + if (!redemptionForBalance) { + toastMsg = + "Unable to compute redemption amount, please refresh balances and try again."; + } + // a little hacky, but works if msg above was never set = no error + if (toastMsg === "") return true; + // else display msg and exit false + toast({ + title: "Redemption Preparation Error", + description: toastMsg, + status: "warning", + isClosable: true, + position: "top", + variant: "solid", + }); + return false; + }; + const redeemNYC = () => { toast({ title: "Redeeming NYC...", @@ -83,7 +116,7 @@ function RedeemNYC() { variant: "solid", }); console.log("Redeeming NYC..."); - redeemNycCall(); + readyToRedeem() && redeemNycCall(); }; const redeemForStSTX = () => { @@ -95,8 +128,8 @@ function RedeemNYC() { variant: "solid", }); console.log("Redeeming NYC for stSTX..."); + readyToRedeem() && stackingDaoCall(); onClose(); - stackingDaoCall(); }; const redeemForLiSTX = () => { @@ -108,8 +141,8 @@ function RedeemNYC() { variant: "solid", }); console.log("Redeeming NYC for liSTX..."); + readyToRedeem() && lisaCall(); onClose(); - lisaCall(); }; if (!stxAddress) { @@ -171,12 +204,36 @@ function RedeemNYC() { - - + {ccd012TxId === null ? ( + <> + + + + ) : ( + + Redemption submitted! + + View on explorer + + + + )} diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 67bacdb..0f14ebc 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { useToast } from "@chakra-ui/react"; import { ClarityValue, noneCV, principalCV } from "micro-stacks/clarity"; import { FinishedTxData } from "micro-stacks/connect"; @@ -25,14 +26,13 @@ import { STACKING_DAO_CONTRACT_ADDRESS, STACKING_DAO_CONTRACT_NAME, STACKING_DAO_FUNCTION_NAME, + ccd012TxIdAtom, stSTXRatioAtom, v1BalanceNYCAtom, v2BalanceNYCAtom, } from "../store/ccd-012"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { stxAddressAtom } from "../store/stacks"; -import { getStackingDaoRatio } from "../store/ccip-022"; -import { useEffect } from "react"; const onFinishToast = (tx: FinishedTxData, toast: any) => { toast({ @@ -42,7 +42,9 @@ const onFinishToast = (tx: FinishedTxData, toast: any) => { position: "top", variant: "solid", isClosable: true, + duration: 9000, }); + return tx.txId; }; const onCancelToast = (toast: any) => { @@ -114,6 +116,7 @@ export const useCcd012RedeemNyc = () => { const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); + const setTxId = useSetAtom(ccd012TxIdAtom); const { openContractCall, isRequestPending } = useOpenContractCall(); // can set a state atom here for UI feedback @@ -131,7 +134,10 @@ export const useCcd012RedeemNyc = () => { functionName: "redeem-nyc", functionArgs: [], postConditions, - onFinish: (finishedTx: FinishedTxData) => onFinishToast(finishedTx, toast), + onFinish: (finishedTx: FinishedTxData) => { + const txId = onFinishToast(finishedTx, toast); + setTxId(txId); + }, onCancel: () => onCancelToast(toast), }; @@ -149,19 +155,11 @@ export const useCcd012StackingDao = () => { const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); const [stSTXRatio, setStSTXRatio] = useAtom(stSTXRatioAtom); + const setTxId = useSetAtom(ccd012TxIdAtom); const { openContractCall, isRequestPending } = useOpenContractCall(); useEffect(() => { - const fetchSTXRatio = async () => { - try { - const ratio = await getStackingDaoRatio(); - setStSTXRatio(ratio); - } catch (error) { - console.error("Failed to fetch stSTX ratio:", error); - setStSTXRatio(null); - } - }; - + const fetchSTXRatio = async () => setStSTXRatio(); fetchSTXRatio(); }, [setStSTXRatio]); @@ -224,7 +222,10 @@ export const useCcd012StackingDao = () => { functionName: STACKING_DAO_FUNCTION_NAME, functionArgs: functionArgs, postConditions: postConditions, - onFinish: (finishedTx: FinishedTxData) => onFinishToast(finishedTx, toast), + onFinish: (finishedTx: FinishedTxData) => { + const txId = onFinishToast(finishedTx, toast); + setTxId(txId); + }, onCancel: () => onCancelToast(toast), }; @@ -241,6 +242,7 @@ export const useCcd012Lisa = () => { const v1BalanceNYC = useAtomValue(v1BalanceNYCAtom); const v2BalanceNYC = useAtomValue(v2BalanceNYCAtom); const redemptionForBalance = useAtomValue(redemptionForBalanceAtom); + const setTxId = useSetAtom(ccd012TxIdAtom); const { openContractCall, isRequestPending } = useOpenContractCall(); const functionArgs: string[] = []; @@ -270,7 +272,10 @@ export const useCcd012Lisa = () => { functionName: LISA_FUNCTION_NAME, functionArgs: functionArgs, postConditions: postConditions, - onFinish: (finishedTx: FinishedTxData) => onFinishToast(finishedTx, toast), + onFinish: (finishedTx: FinishedTxData) => { + const txId = onFinishToast(finishedTx, toast); + setTxId(txId); + }, onCancel: () => onCancelToast(toast), }; diff --git a/src/store/ccd-012.ts b/src/store/ccd-012.ts index df7b067..55ad6ac 100644 --- a/src/store/ccd-012.ts +++ b/src/store/ccd-012.ts @@ -107,11 +107,26 @@ export const ccd012UserRedemptionInfoAtom = null ); +const ccd012stSTXRatioAtom = atomWithStorage( + "citycoins-ccd012-stSTXRatio", + null +); + +export const ccd012TxIdAtom = atom(null); + ///////////////////////// // DERIVED ATOMS ///////////////////////// -export const stSTXRatioAtom = atom(null); +export const stSTXRatioAtom = atom( + // getter + (get) => get(ccd012stSTXRatioAtom), + // setter + async (get, set) => { + const ratio = await get(getStackingDaoRatioQueryAtom); + set(ccd012stSTXRatioAtom, ratio); + } +); export const v1BalanceNYCAtom = atom( // getter @@ -325,6 +340,17 @@ const userRedemptionInfoQueryAtom = atom(async (get) => { } }); +const getStackingDaoRatioQueryAtom = atom(async () => { + try { + const ratio = await getStackingDaoRatio(); + return ratio; + } catch (error) { + throw new Error( + `Failed to fetch stSTX ratio for ${STACKING_DAO_FQ_NAME}: ${error}` + ); + } +}); + ///////////////////////// // HELPER FUNCTIONS ///////////////////////// @@ -442,3 +468,22 @@ async function getUserRedemptionInfo( ); return userRedemptionInfoQuery; } + +// helper to get the stSTX to STX ratio from the StackingDAO contract +// calls `get-stx-per-ststx` on `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.data-core-v1` +// The param `reserve-contract` should be `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.reserve-v1`. +// example: return `u1015555`, meaning for 1.015555 STX you will get 1 stSTX. +export async function getStackingDaoRatio(): Promise { + const stackingDaoRatioQuery = await fetchReadOnlyFunction( + { + contractAddress: STACKING_DAO_CONTRACT_ADDRESS, + contractName: "data-core-v1", + functionName: "get-stx-per-ststx", + functionArgs: [ + principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.reserve-v1`), + ], + }, + true + ); + return stackingDaoRatioQuery; +} diff --git a/src/store/ccip-022.ts b/src/store/ccip-022.ts index 4fef92a..1f8ca0d 100644 --- a/src/store/ccip-022.ts +++ b/src/store/ccip-022.ts @@ -200,22 +200,3 @@ async function getVoterInfo(voterAddress: string): Promise { // console.log("Voter Info", voterInfoQuery); return voterInfoQuery; } - -// helper to get the stSTX to STX ratio from the StackingDAO contract -// calls `get-stx-per-ststx` on `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.data-core-v1` -// The param `reserve-contract` should be `SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.reserve-v1`. -// example: return `u1015555`, meaning for 1.015555 STX you will get 1 stSTX. -export async function getStackingDaoRatio(): Promise { - const stackingDaoRatioQuery = await fetchReadOnlyFunction( - { - contractAddress: STACKING_DAO_CONTRACT_ADDRESS, - contractName: "data-core-v1", - functionName: "get-stx-per-ststx", - functionArgs: [ - principalCV(`${STACKING_DAO_CONTRACT_ADDRESS}.reserve-v1`), - ], - }, - true - ); - return stackingDaoRatioQuery; -} From c0e0cf506c4f04cc7c82b56aed29f101fe7a2b8e Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 8 Jul 2024 10:25:18 -0700 Subject: [PATCH 26/26] chore: code cleanup --- src/components/tabs/redeem-nyc.tsx | 2 +- src/hooks/use-ccd-012.tsx | 2 +- src/store/ccip-022.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/tabs/redeem-nyc.tsx b/src/components/tabs/redeem-nyc.tsx index 7ce200c..eeac8d8 100644 --- a/src/components/tabs/redeem-nyc.tsx +++ b/src/components/tabs/redeem-nyc.tsx @@ -24,8 +24,8 @@ import { } from "@chakra-ui/react"; import { atom, useAtom, useAtomValue } from "jotai"; import { LuExternalLink, LuRepeat } from "react-icons/lu"; -import { stxAddressAtom } from "../../store/stacks"; import SignIn from "../auth/sign-in"; +import { stxAddressAtom } from "../../store/stacks"; import { ccd012TxIdAtom, redemptionForBalanceAtom, diff --git a/src/hooks/use-ccd-012.tsx b/src/hooks/use-ccd-012.tsx index 0f14ebc..6615df4 100644 --- a/src/hooks/use-ccd-012.tsx +++ b/src/hooks/use-ccd-012.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useToast } from "@chakra-ui/react"; import { ClarityValue, noneCV, principalCV } from "micro-stacks/clarity"; import { FinishedTxData } from "micro-stacks/connect"; @@ -31,7 +32,6 @@ import { v1BalanceNYCAtom, v2BalanceNYCAtom, } from "../store/ccd-012"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { stxAddressAtom } from "../store/stacks"; const onFinishToast = (tx: FinishedTxData, toast: any) => { diff --git a/src/store/ccip-022.ts b/src/store/ccip-022.ts index 1f8ca0d..6490978 100644 --- a/src/store/ccip-022.ts +++ b/src/store/ccip-022.ts @@ -2,9 +2,8 @@ import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { fetchReadOnlyFunction } from "micro-stacks/api"; import { validateStacksAddress } from "micro-stacks/crypto"; -import { principalCV, standardPrincipalCV, uintCV } from "micro-stacks/clarity"; +import { standardPrincipalCV, uintCV } from "micro-stacks/clarity"; import { stxAddressAtom } from "./stacks"; -import { STACKING_DAO_CONTRACT_ADDRESS } from "./ccd-012"; ///////////////////////// // TYPES