From 2a222afcbd5649177270ef7ddaf3c656603912f8 Mon Sep 17 00:00:00 2001 From: Yarre Date: Tue, 14 Jan 2025 15:26:39 +0100 Subject: [PATCH 1/4] feat: add admin page and button, update routing and addresses --- src/components/common/AdminBtn/index.tsx | 17 +++++++++ src/components/pages/home/hero.tsx | 2 + src/constants/addresses.ts | 4 +- src/hooks/api/use-status.ts | 21 +++++++++++ src/lib/router.ts | 5 +++ src/pages/admin-page.tsx | 48 ++++++++++++++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/components/common/AdminBtn/index.tsx create mode 100644 src/hooks/api/use-status.ts create mode 100644 src/pages/admin-page.tsx diff --git a/src/components/common/AdminBtn/index.tsx b/src/components/common/AdminBtn/index.tsx new file mode 100644 index 0000000..3d34920 --- /dev/null +++ b/src/components/common/AdminBtn/index.tsx @@ -0,0 +1,17 @@ +import { Link } from 'react-router'; + +import { Button } from '@/components/ui/button'; +import { routes } from '@/lib/router'; + +export const AdminBtn = () => { + return ( + + + + ); +}; diff --git a/src/components/pages/home/hero.tsx b/src/components/pages/home/hero.tsx index d8b0bd3..d0aac47 100644 --- a/src/components/pages/home/hero.tsx +++ b/src/components/pages/home/hero.tsx @@ -1,3 +1,4 @@ +import { AdminBtn } from '@/components/common/AdminBtn'; import { GoToTwitterBtn } from '@/components/common/GoToTwitterBtn'; import { PredictFutureBtn } from '@/components/common/PredictFutureBtn'; @@ -8,6 +9,7 @@ export const Hero = () => {
+
{ + return useQuery({ + queryKey: ['status'], + queryFn: async () => { + const client = authClient(); + + return await get<{ + isShutDown: boolean; + id: number; + } | null>(client, 'shutdown/status'); + }, + }); +}; + +export default useStatus; diff --git a/src/lib/router.ts b/src/lib/router.ts index e156e36..1f4cc66 100644 --- a/src/lib/router.ts +++ b/src/lib/router.ts @@ -7,6 +7,7 @@ import { MainLayout } from '@/layouts/main-layout'; export const routes = { HOME: '/', GAME: '/game', + ADMIN: '/admin', } as const; export const router = createBrowserRouter([ @@ -21,6 +22,10 @@ export const router = createBrowserRouter([ path: routes.GAME, Component: lazy(() => import('@/pages/game-page')), }, + { + path: routes.ADMIN, + Component: lazy(() => import('@/pages/admin-page')), + }, ], }, { diff --git a/src/pages/admin-page.tsx b/src/pages/admin-page.tsx new file mode 100644 index 0000000..822972b --- /dev/null +++ b/src/pages/admin-page.tsx @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { useWallet } from '@solana/wallet-adapter-react'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; + +import { Header } from '@/components/common/Header'; +import { Button } from '@/components/ui/button'; +import { OwnerAddress } from '@/constants/addresses'; +import useStatus from '@/hooks/api/use-status'; +import { network } from '@/lib/solana'; + +export default function AdminPage() { + const { publicKey } = useWallet(); + const navigate = useNavigate(); + const { data: status } = useStatus(); + + useEffect(() => { + if (!publicKey?.equals(OwnerAddress[network])) { + console.log(publicKey?.toBase58(), OwnerAddress[network].toBase58()); + navigate('/'); + } + }, [navigate, publicKey]); + + return ( + <> +
+ +
+

Status: {status?.isShutDown === false ? 'Disabled' : 'Enabled'}

+ +
+ + +
+
+ + ); +} From e1c9aa4ad083c542960701a3b267bd5fc57226e2 Mon Sep 17 00:00:00 2001 From: Yarre Date: Tue, 14 Jan 2025 15:52:01 +0100 Subject: [PATCH 2/4] feat: update Devnet owner address and implement toast notifications for prediction process --- src/constants/addresses.ts | 2 +- src/hooks/api/use-controll-service.ts | 33 +++++++ .../contracts/write/use-make-prediction.ts | 87 ++++++++++++++----- 3 files changed, 100 insertions(+), 22 deletions(-) create mode 100644 src/hooks/api/use-controll-service.ts diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index b6735de..55333ee 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -5,5 +5,5 @@ export const wSolMint = new PublicKey('So111111111111111111111111111111111111111 export const OwnerAddress = { [WalletAdapterNetwork.Mainnet]: new PublicKey('2Mko2nLhSiehXGJDseYCj6hYQdrK3cKNMetcGaXtbrJk'), - [WalletAdapterNetwork.Devnet]: new PublicKey('dBLGG3ERvVsAexwfLeRaPUSzQQryKsabNEFKpuMqQqQ'), + [WalletAdapterNetwork.Devnet]: new PublicKey('9Her7ga9XNUdjGj8utwkze9v2d1k6aZ9tac9SzJweED8'), }; diff --git a/src/hooks/api/use-controll-service.ts b/src/hooks/api/use-controll-service.ts new file mode 100644 index 0000000..30dd55b --- /dev/null +++ b/src/hooks/api/use-controll-service.ts @@ -0,0 +1,33 @@ +import { useWallet } from '@solana/wallet-adapter-react'; +import { useMutation } from '@tanstack/react-query'; + +import { post } from './utils'; + +import { authClient } from '@/lib/fetcher'; +import { TarotCard } from '@/types/tarot'; + +const useSubmitTarotCards = () => { + const { publicKey } = useWallet(); + + return useMutation({ + async mutationFn({ tarots, hash, question }: { tarots: TarotCard[]; hash: string; question: string }) { + if (!publicKey) { + return; + } + + const client = authClient(); + + return await post<{ response: string }>(client, `tarot/generate-response`, { + tarots, + hash, + question, + }); + }, + + onError(error) { + console.trace(error); + }, + }); +}; + +export default useSubmitTarotCards; diff --git a/src/hooks/contracts/write/use-make-prediction.ts b/src/hooks/contracts/write/use-make-prediction.ts index fd0df8a..40ac3d7 100644 --- a/src/hooks/contracts/write/use-make-prediction.ts +++ b/src/hooks/contracts/write/use-make-prediction.ts @@ -1,17 +1,53 @@ import { useWallet } from '@solana/wallet-adapter-react'; import { SystemProgram, Transaction } from '@solana/web3.js'; import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { OwnerAddress } from '@/constants/addresses'; import { env } from '@/env'; import useSubmitTarotCards from '@/hooks/api/use-submit-cards'; import { network } from '@/lib/solana'; import { sendAndConfirmTransaction } from '@/lib/solana/utils'; -import { getRandomTarotCards, showTxToast } from '@/lib/utils'; +import { getRandomTarotCards } from '@/lib/utils'; + +const notify = (setToastId: React.Dispatch>) => { + const id = toast('Making prediction...', { + autoClose: false, + closeOnClick: false, + draggable: false, + isLoading: true, + type: 'default', + }); + setToastId(id); +}; + +const updateToast = (toastId: string | number | null) => { + if (toastId !== null) { + toast.update(toastId, { + render: 'Done!', + type: 'success', + autoClose: 3000, + isLoading: false, + }); + } +}; + +const handleErrorToast = (toastId: string | number | null) => { + if (toastId !== null) { + toast.update(toastId, { + render: 'Error occurred!', + type: 'error', + autoClose: 3000, + isLoading: false, + }); + } +}; const useMakePrediction = () => { const { publicKey, sendTransaction } = useWallet(); const { mutateAsync: submitCards } = useSubmitTarotCards(); + const [toastId, setToastId] = useState(null); return useMutation({ async mutationFn(question: string) { @@ -19,30 +55,39 @@ const useMakePrediction = () => { return; } - return await showTxToast('Making prediction', async () => { - const rawTx = new Transaction(); - - rawTx.add( - SystemProgram.transfer({ - fromPubkey: publicKey, - toPubkey: OwnerAddress[network], - lamports: Number(env.VITE_DEPOSIT_AMOUNT_SOL) * 1e9, - }), - ); - - const txHash = await sendAndConfirmTransaction(publicKey, rawTx, sendTransaction); - const tarots = getRandomTarotCards(txHash + publicKey.toBase58()); - const result = await submitCards({ tarots, hash: txHash, question }); - - return { - tarots, - answer: result?.response ?? '', - }; - }); + notify(setToastId); + + const rawTx = new Transaction(); + + rawTx.add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: OwnerAddress[network], + lamports: Number(env.VITE_DEPOSIT_AMOUNT_SOL) * 1e9, + }), + ); + + const txHash = await sendAndConfirmTransaction(publicKey, rawTx, sendTransaction); + + const tarots = getRandomTarotCards(txHash + publicKey.toBase58()); + + const result = await submitCards({ tarots, hash: txHash, question }); + + console.log(result); + + updateToast(toastId); + + console.log('updateToast'); + + return { + tarots, + answer: result?.response ?? '', + }; }, onError(error) { console.trace(error); + handleErrorToast(toastId); }, }); }; From c94c2faa756bb24442efd1db4554f8a37c229b80 Mon Sep 17 00:00:00 2001 From: Yarre Date: Wed, 15 Jan 2025 12:16:17 +0100 Subject: [PATCH 3/4] feat: update formatting and improve control service functionality --- .prettierrc.cjs | 4 +-- src/components/common/About/index.tsx | 4 +-- src/components/pages/game/game.tsx | 4 ++- src/components/pages/home/faq.tsx | 5 ++- src/hooks/api/use-control-service.ts | 48 +++++++++++++++++++++++++++ src/hooks/api/use-controll-service.ts | 33 ------------------ src/hooks/api/use-status.ts | 1 + src/lib/fetcher.ts | 4 +++ src/lib/utils.ts | 5 ++- src/pages/admin-page.tsx | 14 +++++++- 10 files changed, 81 insertions(+), 41 deletions(-) create mode 100644 src/hooks/api/use-control-service.ts delete mode 100644 src/hooks/api/use-controll-service.ts diff --git a/.prettierrc.cjs b/.prettierrc.cjs index e643a35..77c651e 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -1,7 +1,7 @@ module.exports = { singleQuote: true, - trailingComma: "all", + trailingComma: 'all', printWidth: 120, semi: true, - plugins: ["prettier-plugin-tailwindcss"], + plugins: ['prettier-plugin-tailwindcss'], }; diff --git a/src/components/common/About/index.tsx b/src/components/common/About/index.tsx index 78a890b..dc7491f 100644 --- a/src/components/common/About/index.tsx +++ b/src/components/common/About/index.tsx @@ -36,8 +36,8 @@ const INSTRUCTIONS = [ subtitle: 'Ask another question:', description: (

- To start a new session, click "Make a New Forecast" or refresh the - page + To start a new session, click " + Make a New Forecast" or refresh the page

), }, diff --git a/src/components/pages/game/game.tsx b/src/components/pages/game/game.tsx index d759302..4c3a603 100644 --- a/src/components/pages/game/game.tsx +++ b/src/components/pages/game/game.tsx @@ -20,7 +20,9 @@ const TarotRequestSchema = z.object({ .min(3, 'Min 3 symbols') .max(1000, 'Max 1000 symbols') // .regex(/^[a-zA-Z0-9.,!?-\s]+$/, 'Only English letters and numbers are allowed') - .refine((value) => value.trim() !== '', { message: 'String cannot consist of only spaces' }), + .refine((value) => value.trim() !== '', { + message: 'String cannot consist of only spaces', + }), }); const DEFAULT_IMAGE = 'images/tarot-game/bord.png'; diff --git a/src/components/pages/home/faq.tsx b/src/components/pages/home/faq.tsx index 44f20f8..456bafd 100644 --- a/src/components/pages/home/faq.tsx +++ b/src/components/pages/home/faq.tsx @@ -28,7 +28,10 @@ const FAQ_MOCK = [ title: 'Which wallets are supported?', description: 'We currently support two Solana-compatible wallets: Phantom and Solflare', }, - { title: 'What payment tokens can I use?', description: 'You can choose SOL for your payment.' }, + { + title: 'What payment tokens can I use?', + description: 'You can choose SOL for your payment.', + }, { title: 'Can I use this service anonymously?', description: diff --git a/src/hooks/api/use-control-service.ts b/src/hooks/api/use-control-service.ts new file mode 100644 index 0000000..108742c --- /dev/null +++ b/src/hooks/api/use-control-service.ts @@ -0,0 +1,48 @@ +import { useWallet } from '@solana/wallet-adapter-react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import bs58 from 'bs58'; + +import { post } from './utils'; + +import { authClient } from '@/lib/fetcher'; + +const useControlService = () => { + const { publicKey, signMessage } = useWallet(); + + const client = useQueryClient(); + + return useMutation({ + async mutationFn({ status }: { status: boolean }) { + if (!publicKey || !signMessage) { + return; + } + + const client = authClient(); + + const uuid = crypto.randomUUID(); + + const message = `I am the admin of Tarot ${uuid}`; + + const encodedMessage = new TextEncoder().encode(message); + const signature = bs58.encode(await signMessage(encodedMessage)); + + client.setHeader('x-admin-signature', signature); + client.setHeader('x-admin-uuid', uuid); + + return await post<{ + isShutDown: boolean; + id: number; + }>(client, status ? `shutdown/enable` : 'shutdown/shutdown'); + }, + async onSuccess() { + await client.invalidateQueries({ + queryKey: ['status'], + }); + }, + onError(error) { + console.trace(error); + }, + }); +}; + +export default useControlService; diff --git a/src/hooks/api/use-controll-service.ts b/src/hooks/api/use-controll-service.ts deleted file mode 100644 index 30dd55b..0000000 --- a/src/hooks/api/use-controll-service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useWallet } from '@solana/wallet-adapter-react'; -import { useMutation } from '@tanstack/react-query'; - -import { post } from './utils'; - -import { authClient } from '@/lib/fetcher'; -import { TarotCard } from '@/types/tarot'; - -const useSubmitTarotCards = () => { - const { publicKey } = useWallet(); - - return useMutation({ - async mutationFn({ tarots, hash, question }: { tarots: TarotCard[]; hash: string; question: string }) { - if (!publicKey) { - return; - } - - const client = authClient(); - - return await post<{ response: string }>(client, `tarot/generate-response`, { - tarots, - hash, - question, - }); - }, - - onError(error) { - console.trace(error); - }, - }); -}; - -export default useSubmitTarotCards; diff --git a/src/hooks/api/use-status.ts b/src/hooks/api/use-status.ts index 6ede026..2922f44 100644 --- a/src/hooks/api/use-status.ts +++ b/src/hooks/api/use-status.ts @@ -15,6 +15,7 @@ const useStatus = () => { id: number; } | null>(client, 'shutdown/status'); }, + refetchInterval: 10000, }); }; diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index 51d85f4..7b2027d 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -16,6 +16,10 @@ export class Fetcher { this._headers = headers; } + public setHeader(key: string, value: string) { + this._headers[key] = value; + } + public async get(url: string) { return this._processResponse( fetch(new URL(url, this._baseURL), { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c6a745e..dee146f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -43,7 +43,10 @@ export const getRandomTarotCards = (hash: string): TarotCard[] => { .map((sum) => sum % 50 === 0) .slice(0, 3); - return tarotIds.map((tarot, idx) => ({ id: tarot, reverted: isReverted[idx] })); + return tarotIds.map((tarot, idx) => ({ + id: tarot, + reverted: isReverted[idx], + })); }; const calculateByteSum = (str: string): number => stringToBytes(str).reduce((sum, byte) => sum + byte, 0); diff --git a/src/pages/admin-page.tsx b/src/pages/admin-page.tsx index 822972b..7a14822 100644 --- a/src/pages/admin-page.tsx +++ b/src/pages/admin-page.tsx @@ -6,14 +6,18 @@ import { useNavigate } from 'react-router'; import { Header } from '@/components/common/Header'; import { Button } from '@/components/ui/button'; import { OwnerAddress } from '@/constants/addresses'; +import useControlService from '@/hooks/api/use-control-service'; import useStatus from '@/hooks/api/use-status'; import { network } from '@/lib/solana'; export default function AdminPage() { const { publicKey } = useWallet(); const navigate = useNavigate(); + const { data: status } = useStatus(); + const { mutateAsync } = useControlService(); + useEffect(() => { if (!publicKey?.equals(OwnerAddress[network])) { console.log(publicKey?.toBase58(), OwnerAddress[network].toBase58()); @@ -21,21 +25,29 @@ export default function AdminPage() { } }, [navigate, publicKey]); + const handleControl = async (status: boolean) => { + await mutateAsync({ + status: status, + }); + }; + return ( <>
-

Status: {status?.isShutDown === false ? 'Disabled' : 'Enabled'}

+

Status: {status?.isShutDown === true ? 'Disabled' : 'Enabled'}