diff --git a/app/providers.tsx b/app/providers.tsx index 9dcfb02..6f35994 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,10 +1,13 @@ "use client" import { apolloClient } from "@/lib/graphql"; +import reactQueryClient from "@/lib/react-query/client"; import { GlobalModalProvider } from "@/modules/modals"; import theme, { ThemeStorageManager } from "@/theme"; import { ApolloProvider } from "@apollo/client"; import { CacheProvider } from "@chakra-ui/next-js"; import { ChakraProvider } from "@chakra-ui/react"; +import { QueryClient } from "@cosmjs/stargate"; +import { QueryClientProvider } from "@tanstack/react-query"; import React, { FC, ReactNode } from "react" interface Props { @@ -15,15 +18,17 @@ const Providers: FC = (props) => { const { children } = props; return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ) } diff --git a/package-lock.json b/package-lock.json index fbe296b..21ceaf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ "@chakra-ui/icons": "^2.1.1", "@chakra-ui/next-js": "^2.1.5", "@chakra-ui/react": "^2.8.1", + "@cosmjs/amino": "^0.32.4", "@cosmjs/cosmwasm-stargate": "^0.32.3", "@cosmjs/proto-signing": "^0.32.3", "@cosmjs/stargate": "^0.32.3", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.60.6", "axios": "^1.5.1", "dayjs": "^1.11.6", "framer-motion": "^10.16.4", @@ -1497,14 +1499,14 @@ } }, "node_modules/@cosmjs/amino": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.32.3.tgz", - "integrity": "sha512-G4zXl+dJbqrz1sSJ56H/25l5NJEk/pAPIr8piAHgbXYw88OdAOlpA26PQvk2IbSN/rRgVbvlLTNgX2tzz1dyUA==", + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.32.4.tgz", + "integrity": "sha512-zKYOt6hPy8obIFtLie/xtygCkH9ZROiQ12UHfKsOkWaZfPQUvVbtgmu6R4Kn1tFLI/SRkw7eqhaogmW/3NYu/Q==", "dependencies": { - "@cosmjs/crypto": "^0.32.3", - "@cosmjs/encoding": "^0.32.3", - "@cosmjs/math": "^0.32.3", - "@cosmjs/utils": "^0.32.3" + "@cosmjs/crypto": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4" } }, "node_modules/@cosmjs/cosmwasm-stargate": { @@ -1525,13 +1527,13 @@ } }, "node_modules/@cosmjs/crypto": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.32.3.tgz", - "integrity": "sha512-niQOWJHUtlJm2GG4F00yGT7sGPKxfUwz+2qQ30uO/E3p58gOusTcH2qjiJNVxb8vScYJhFYFqpm/OA/mVqoUGQ==", + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.32.4.tgz", + "integrity": "sha512-zicjGU051LF1V9v7bp8p7ovq+VyC91xlaHdsFOTo2oVry3KQikp8L/81RkXmUIT8FxMwdx1T7DmFwVQikcSDIw==", "dependencies": { - "@cosmjs/encoding": "^0.32.3", - "@cosmjs/math": "^0.32.3", - "@cosmjs/utils": "^0.32.3", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4", "@noble/hashes": "^1", "bn.js": "^5.2.0", "elliptic": "^6.5.4", @@ -1539,9 +1541,9 @@ } }, "node_modules/@cosmjs/encoding": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.32.3.tgz", - "integrity": "sha512-p4KF7hhv8jBQX3MkB3Defuhz/W0l3PwWVYU2vkVuBJ13bJcXyhU9nJjiMkaIv+XP+W2QgRceqNNgFUC5chNR7w==", + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.32.4.tgz", + "integrity": "sha512-tjvaEy6ZGxJchiizzTn7HVRiyTg1i4CObRRaTRPknm5EalE13SV+TCHq38gIDfyUeden4fCuaBVEdBR5+ti7Hw==", "dependencies": { "base64-js": "^1.3.0", "bech32": "^1.1.4", @@ -1563,9 +1565,9 @@ } }, "node_modules/@cosmjs/math": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.32.3.tgz", - "integrity": "sha512-amumUtZs8hCCnV+lSBaJIiZkGabQm22QGg/IotYrhcmoOEOjt82n7hMNlNXRs7V6WLMidGrGYcswB5zcmp0Meg==", + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.32.4.tgz", + "integrity": "sha512-++dqq2TJkoB8zsPVYCvrt88oJWsy1vMOuSOKcdlnXuOA/ASheTJuYy4+oZlTQ3Fr8eALDLGGPhJI02W2HyAQaw==", "dependencies": { "bn.js": "^5.2.0" } @@ -1647,9 +1649,9 @@ } }, "node_modules/@cosmjs/utils": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.32.3.tgz", - "integrity": "sha512-WCZK4yksj2hBDz4w7xFZQTRZQ/RJhBX26uFHmmQFIcNUUVAihrLO+RerqJgk0dZqC42wstM9pEUQGtPmLcIYvg==" + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.32.4.tgz", + "integrity": "sha512-D1Yc+Zy8oL/hkUkFUL/bwxvuDBzRGpc4cF7/SkdhxX4iHpSLgdOuTt1mhCh9+kl6NQREy9t7SYZ6xeW5gFe60w==" }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", @@ -3331,6 +3333,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.60.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz", + "integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.60.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.60.6.tgz", + "integrity": "sha512-FUzSDaiPkuZCmuGqrixfRRXJV9u+nrUh9lAlA5Q3ZFrOw1Js1VeBfxi1NIcJO3ZWJdKceBqKeBJdNcWStCYZnw==", + "dependencies": { + "@tanstack/query-core": "5.60.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@terra-money/terra.js": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.1.10.tgz", diff --git a/package.json b/package.json index cd30a11..6852d17 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,13 @@ "@chakra-ui/icons": "^2.1.1", "@chakra-ui/next-js": "^2.1.5", "@chakra-ui/react": "^2.8.1", + "@cosmjs/amino": "^0.32.4", "@cosmjs/cosmwasm-stargate": "^0.32.3", "@cosmjs/proto-signing": "^0.32.3", "@cosmjs/stargate": "^0.32.3", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.60.6", "axios": "^1.5.1", "dayjs": "^1.11.6", "framer-motion": "^10.16.4", @@ -44,4 +46,4 @@ "prettier": "^3.0.3", "typescript": "4.8.3" } -} \ No newline at end of file +} diff --git a/src/lib/andrjs/hooks/ado/useGetRate.ts b/src/lib/andrjs/hooks/ado/useGetRate.ts new file mode 100644 index 0000000..50f0b2d --- /dev/null +++ b/src/lib/andrjs/hooks/ado/useGetRate.ts @@ -0,0 +1,43 @@ +import useAndromedaClient from "@/lib/andrjs/hooks/useAndromedaClient"; +import { useQuery } from "@tanstack/react-query"; + +export interface Rate { + "local": { + "rate_type": "additive" | "deductive", + "value": + { + "flat": { + "denom": string, + "amount": string + } + } + | + { + "percent": { + "percent": string + } + }, + "description": string + } +} + +export function useGetRate(address: string, action: string) { + + const client = useAndromedaClient(); + + return useQuery({ + queryKey: ["ado", "rate", address, action, client?.isConnected], + enabled: !!client?.isConnected, + queryFn: async () => { + + const rate = await client!.chainClient!.queryClient!.queryContractSmart(address, { + "rates": { + "action": action + } + }) + return rate as Rate; + } + }) + + +} \ No newline at end of file diff --git a/src/lib/react-query/client.ts b/src/lib/react-query/client.ts new file mode 100644 index 0000000..f7f345f --- /dev/null +++ b/src/lib/react-query/client.ts @@ -0,0 +1,33 @@ +import { shortenString } from "@/utils/string"; +import { createStandaloneToast } from "@chakra-ui/react"; +import { QueryClient } from "@tanstack/react-query"; + + +const { toast } = createStandaloneToast() +const reactQueryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1 * 60 * 1000 + }, + mutations: { + onError: (err) => { + if ("error" in err) { + err = err.error as Error + } + let message: string = err?.message ?? "No Description" + toast({ + description: shortenString(message, 100), + status: "error", + position: "top-right", + duration: 3000, + isClosable: true + } + + ) + + } + + } + } +}) +export default reactQueryClient \ No newline at end of file diff --git a/src/modules/common/ui/PromiseButton.tsx b/src/modules/common/ui/PromiseButton.tsx new file mode 100644 index 0000000..41287ae --- /dev/null +++ b/src/modules/common/ui/PromiseButton.tsx @@ -0,0 +1,31 @@ +import { Button, ButtonProps } from "@chakra-ui/react"; +import React, { FC, ReactNode, useCallback, useState } from "react" + +interface Props extends ButtonProps { +} + +const PromiseButton = React.forwardRef(function PromiseButton(props, ref) { + const { children, onClick, isLoading, ...buttonProps } = props; + const [loading, setLoading] = useState(false); + + const promiseOnClick: typeof onClick = useCallback(async (e) => { + if (loading) return; + try { + setLoading(true); + await onClick?.(e) + } finally { + setLoading(false); + } + }, [onClick]) + return ( + + ) +}) + +export default PromiseButton \ No newline at end of file diff --git a/src/modules/exchange/ExchangeIntro.tsx b/src/modules/exchange/ExchangeIntro.tsx index c066446..ca17d99 100644 --- a/src/modules/exchange/ExchangeIntro.tsx +++ b/src/modules/exchange/ExchangeIntro.tsx @@ -3,17 +3,21 @@ import { useGetCw20MarketingInfo } from "@/lib/graphql/hooks/cw20"; import { useAndromedaStore } from "@/zustand/andromeda"; import { Flex, Text, Button, Link } from "@chakra-ui/react"; import React, { FC } from "react"; +import PromiseButton from "../common/ui/PromiseButton"; interface ExchangeIntroProps { cw20: string; } + + const ExchangeIntro: FC = (props) => { const { cw20 } = props; const { accounts, chainId } = useAndromedaStore(); const { data: chainConfig } = useQueryChain(chainId); const { data: tokenInfo } = useGetCw20MarketingInfo(cw20); + return ( @@ -24,9 +28,9 @@ const ExchangeIntro: FC = (props) => { {tokenInfo?.marketingInfo?.description} - + diff --git a/src/modules/exchange/ExchangePage.tsx b/src/modules/exchange/ExchangePage.tsx index 2cd58b1..418e86d 100644 --- a/src/modules/exchange/ExchangePage.tsx +++ b/src/modules/exchange/ExchangePage.tsx @@ -1,16 +1,19 @@ import { IExchangeCollection } from "@/lib/app/types"; -import { Flex } from "@chakra-ui/react"; +import { Box, Button, Flex, Text } from "@chakra-ui/react"; import React, { ChangeEvent, FC, useState } from "react"; import ExchangeIntro from "./ExchangeIntro"; import ExchangeCard from "./ExchangeCard"; +import CONFIG from "@/config"; +import PromiseButton from "../common/ui/PromiseButton"; interface Props { collection: IExchangeCollection; } const ExchangePage: FC = (props) => { + const { collection } = props; - + const [nativeAmount, setNativeAmount] = useState(0); const handleAndrInput = (e: ChangeEvent) => { let value = e.currentTarget.value ? parseInt(e.currentTarget.value) : 0; @@ -18,17 +21,49 @@ const ExchangePage: FC = (props) => { setNativeAmount(value); } } - + const handleAddToKeplr = async () => { + if (window.keplr) { + try { + await window.keplr.enable(CONFIG.chainId); + await window.keplr.suggestToken(CONFIG.chainId, collection.cw20); + } catch (error) { + console.error("Failed to add token to Keplr", error); + } + } + } + + return ( - - - + + + + + + + + + + + Add your cw20 token to Keplr for easy balance tracking + + + • You only need to do this once per token. +
+ • Make sure your Keplr wallet is connected before proceeding. +
+ • After adding, you can find the token in your Keplr asset list. +
+ + Add token to keplr + +
+
) } diff --git a/src/modules/modals/components/BuyNowModal.tsx b/src/modules/modals/components/BuyNowModal.tsx index d89fa32..90ee979 100644 --- a/src/modules/modals/components/BuyNowModal.tsx +++ b/src/modules/modals/components/BuyNowModal.tsx @@ -3,17 +3,23 @@ import useApp from "@/lib/app/hooks/useApp"; import { Box, Button, + Divider, FormControl, + Grid, Heading, + Table, Text, + VStack, } from "@chakra-ui/react"; -import { coins } from "@cosmjs/proto-signing"; -import { FC, useState } from "react"; +import { Coin, coin } from "@cosmjs/proto-signing"; +import { FC } from "react"; import { useExecuteModal } from "../hooks"; import { BuyNowModalProps } from "../types"; -import { Msg } from "@andromedaprotocol/andromeda.js"; import { useGetTokenMarketplaceInfo } from "@/lib/graphql/hooks/marketplace"; import { useGetCw721Token } from "@/lib/graphql/hooks/cw721"; +import { useGetRate } from "@/lib/andrjs/hooks/ado/useGetRate"; +import { addCoins } from "@cosmjs/amino"; + const BuyNowModal: FC = (props) => { const { contractAddress, tokenId, marketplaceAddress } = props; @@ -24,22 +30,55 @@ const BuyNowModal: FC = (props) => { tokenId ); + const { data: rate } = useGetRate(marketplaceAddress, "Buy") + const rateValue = rate?.local.value; + const rateType = rate?.local.rate_type + const flatRate = (rateValue && "flat" in rateValue) ? rateValue.flat : undefined; + const flatRateDenom = flatRate?.denom; + const percentRate = (rateValue && "percent" in rateValue) ? rateValue.percent : undefined + const marketplaceAmount = marketplaceState?.latestSaleState.price + const floatMarketplaceAmount = parseFloat(marketplaceAmount ?? "0") + const commaSeparatedAmount = Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(floatMarketplaceAmount) + + const calcAmount = (): Coin | undefined => { + if (flatRate && flatRateDenom) { + return { + amount: flatRate.amount, + denom: flatRateDenom + } + } else if (percentRate) { + const percentAmount = parseFloat(percentRate.percent) * floatMarketplaceAmount + return { + amount: percentAmount.toFixed(0), + denom: marketplaceState!.latestSaleState.coin_denom + } + } + } const { config } = useApp(); + const DENOM = marketplaceState?.latestSaleState.coin_denom ?? config?.coinDenom ?? "ujunox"; + const rateCoin = calcAmount(); + const rateCoinAmount = parseFloat(rateCoin?.amount ?? "0") + const commaSeparatedRateCoinAmount = Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(rateCoinAmount) + const marketplaceCoin = coin(marketplaceAmount ?? "0", DENOM); + + const totalAmount = (rateCoin && rateType === "additive") ? sumCoins([rateCoin, marketplaceCoin]) : [marketplaceCoin]; + console.log(totalAmount) + + const construct = useBuyNowConstruct(); - const DENOM = marketplaceState?.latestSaleState.coin_denom ?? config?.coinDenom ?? "ujunox"; // Execute place bid directly on auction const openExecute = useExecuteModal(marketplaceAddress); const onSubmit = () => { const msg = construct({ tokenAddress: contractAddress, tokenId: tokenId }); - console.log("price:", marketplaceState?.latestSaleState.price); + console.log(JSON.stringify(msg)); console.log("DENOM:", DENOM); - const funds = coins(marketplaceState?.latestSaleState.price ?? 0, DENOM); - openExecute(msg, true, funds); + + openExecute(msg, true, totalAmount); }; return ( @@ -49,10 +88,75 @@ const BuyNowModal: FC = (props) => { You are about to buy {token?.metadata?.name} which has tokenId {tokenId}. - + + {rateType === "additive" ? ( + <> + + + Price : + + + + + {commaSeparatedAmount.toString()} {DENOM} + + + + ) + : ( + <> + + + Price : + + + + + {commaSeparatedAmount.toString()} {DENOM} + + + + ) + } + + {(rateCoin && rateType === "additive") && ( + <> + + + Added Expense : + + + + + {rateType === "additive" ? "+" : ""} {commaSeparatedRateCoinAmount} {rateCoin?.denom} + + + + + )} + + {rateType === "additive" && ( + + + + Total Price : + + + + {totalAmount?.map(item => { + return ( + + { + Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(parseFloat(item.amount))} {item.denom} + + ) + })} + + + )}