diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 812699f6..5e93c7ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: - name: Setup Node Environment ⬢ uses: actions/setup-node@v1 with: - node-version: 16 + node-version: 18 - name: Install run: npm install - name: Build @@ -90,10 +90,12 @@ jobs: - name: Setup Node Environment ⬢ uses: actions/setup-node@v1 with: - node-version: 16 + node-version: 18 - name: Install run: npm install - name: Build run: npm run build - name: Test run: npm test + - name: Test Voting + run: npm run test-voting diff --git a/garage/package-lock.json b/garage/package-lock.json index de4615f8..581af663 100644 --- a/garage/package-lock.json +++ b/garage/package-lock.json @@ -37,7 +37,8 @@ "typescript": "^5.1.3", "vite": "^4.3.2", "vite-plugin-ejs": "^1.6.4", - "vite-plugin-svgr": "^2.4.0" + "vite-plugin-svgr": "^2.4.0", + "vite-tsconfig-paths": "^4.2.1" } }, "node_modules/@adraffy/ens-normalize": { @@ -5976,6 +5977,12 @@ "node": ">=4" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -8605,6 +8612,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/tsconfck": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", + "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^14.13.1 || ^16 || >=18" + }, + "peerDependencies": { + "typescript": "^4.3.5 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -9063,6 +9090,25 @@ "vite": "^2.6.0 || 3 || 4" } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.1.tgz", + "integrity": "sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^2.1.0" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/wagmi": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-1.2.2.tgz", diff --git a/garage/package.json b/garage/package.json index 216fcf5d..b632d171 100644 --- a/garage/package.json +++ b/garage/package.json @@ -38,6 +38,7 @@ "typescript": "^5.1.3", "vite": "^4.3.2", "vite-plugin-ejs": "^1.6.4", - "vite-plugin-svgr": "^2.4.0" + "vite-plugin-svgr": "^2.4.0", + "vite-tsconfig-paths": "^4.2.1" } } diff --git a/garage/src/abis/VotingRegistry.ts b/garage/src/abis/VotingRegistry.ts index 3c71bdbf..050c8be2 100644 --- a/garage/src/abis/VotingRegistry.ts +++ b/garage/src/abis/VotingRegistry.ts @@ -76,4 +76,28 @@ export const abi = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "hasRole", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, ] as const; diff --git a/garage/src/components/AboutPilotsModal.tsx b/garage/src/components/AboutPilotsModal.tsx index 7f7a15bf..50514ea7 100644 --- a/garage/src/components/AboutPilotsModal.tsx +++ b/garage/src/components/AboutPilotsModal.tsx @@ -15,7 +15,7 @@ import { Text, VStack, } from "@chakra-ui/react"; -import garage from "../assets/garage.jpg"; +import garage from "~/assets/garage.jpg"; interface ModalProps { isOpen: boolean; diff --git a/garage/src/components/ChainAwareButton.tsx b/garage/src/components/ChainAwareButton.tsx index ce106d51..14b2c66a 100644 --- a/garage/src/components/ChainAwareButton.tsx +++ b/garage/src/components/ChainAwareButton.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Button } from "@chakra-ui/react"; import { Chain, useNetwork } from "wagmi"; import { useChainModal } from "@rainbow-me/rainbowkit"; -import { mainChain } from "../env"; +import { mainChain } from "~/env"; export const ChainAwareButton = ( props: { expectedChain?: Chain } & React.ComponentProps diff --git a/garage/src/components/CreateMissionModal.tsx b/garage/src/components/CreateMissionModal.tsx index 6c43b6bd..460d0d70 100644 --- a/garage/src/components/CreateMissionModal.tsx +++ b/garage/src/components/CreateMissionModal.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Result } from "@tableland/sdk"; import { Button, FormControl, @@ -25,13 +26,14 @@ import { useDisclosure, useToast, } from "@chakra-ui/react"; +import isEqual from "lodash/isEqual"; import { DeleteIcon } from "@chakra-ui/icons"; -import { ChainAwareButton } from "./ChainAwareButton"; import { Database } from "@tableland/sdk"; -import { useSigner } from "../hooks/useSigner"; -import { isPresent } from "../utils/types"; -import { MissionReward, MissionDeliverable } from "../types"; -import { secondaryChain, deployment } from "../env"; +import { useSigner } from "~/hooks/useSigner"; +import { isPresent } from "~/utils/types"; +import { Mission, MissionReward, MissionDeliverable } from "~/types"; +import { secondaryChain, deployment } from "~/env"; +import { ChainAwareButton } from "./ChainAwareButton"; const { missionsTable } = deployment; @@ -176,16 +178,24 @@ const StateImportModal = ({ ); }; -export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { - const toast = useToast(); - const signer = useSigner({ chainId: secondaryChain.id }); - - const db = useMemo(() => { - if (signer) return new Database({ signer }); - }, [signer]); - - const [formState, setFormState] = useState(initialFormState); +type BaseModalProps = ModalProps & { + title: string; + isFormValid: boolean; + formState: FormState; + setFormState: React.Dispatch>; + onMutate?: (formState: FormState) => Promise>; +}; +const BaseMissionModal = ({ + isOpen, + onClose, + title, + formState, + setFormState, + isFormValid, + onMutate, +}: BaseModalProps) => { + const toast = useToast(); const { name, description, @@ -198,34 +208,17 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { maxNumberOfContributions, } = formState; - const isFormValid = useMemo(() => isValid(formState), [formState]); const [txnState, setTxnState] = useState< "idle" | "querying" | "success" | "fail" >("idle"); const onSubmit = useCallback(async () => { - if (!db) return; + if (!onMutate) return; setTxnState("querying"); try { - const { meta: insert } = await db - .prepare( - `INSERT INTO ${missionsTable} (name, description, tags, requirements, deliverables, rewards, contributions_start_block, contributions_end_block, max_number_of_contributions, contributions_disabled) VALUES (?, ?, JSON(?), JSON(?), JSON(?), JSON(?), ?, ?, ?, 0)` - ) - .bind( - name, - description, - JSON.stringify(tags), - JSON.stringify(requirements), - JSON.stringify(deliverables), - JSON.stringify(rewards), - contributionsStartBlock, - contributionsEndBlock, - maxNumberOfContributions - ) - .run(); - + const { meta: insert } = await onMutate(formState); await insert.txn?.wait(); setTxnState("success"); toast({ title: "Success", status: "success", duration: 7_500 }); @@ -244,7 +237,7 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { } } } - }, [db, formState, setTxnState, toast]); + }, [onMutate, formState, setTxnState, toast]); const onNameInputChanged = useCallback( (e: React.ChangeEvent) => { @@ -448,10 +441,9 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { useEffect(() => { if (isOpen) { - setFormState(initialFormState); setTxnState("idle"); } - }, [isOpen, setFormState, setTxnState]); + }, [isOpen, setTxnState]); const { isOpen: exportIsOpen, @@ -481,7 +473,7 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { - Create Mission + {title} @@ -758,3 +750,150 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { ); }; + +export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { + const signer = useSigner({ chainId: secondaryChain.id }); + + const db = useMemo(() => { + if (signer) return new Database({ signer }); + }, [signer]); + + const [formState, setFormState] = useState(initialFormState); + + const isFormValid = useMemo(() => isValid(formState), [formState]); + + const mutate = db + ? (state: FormState) => { + const { + name, + description, + tags, + requirements, + deliverables, + rewards, + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions, + } = state; + + return db + .prepare( + `INSERT INTO ${missionsTable} (name, description, tags, requirements, deliverables, rewards, contributions_start_block, contributions_end_block, max_number_of_contributions, contributions_disabled) VALUES (?, ?, JSON(?), JSON(?), JSON(?), JSON(?), ?, ?, ?, 0)` + ) + .bind( + name, + description, + JSON.stringify(tags), + JSON.stringify(requirements), + JSON.stringify(deliverables), + JSON.stringify(rewards), + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions + ) + .run(); + } + : undefined; + + useEffect(() => { + if (isOpen) { + setFormState(initialFormState); + } + }, [isOpen, setFormState]); + + return ( + + ); +}; + +type EditModalProps = { mission: Mission } & ModalProps; + +export const EditMissionModal = ({ + isOpen, + onClose, + mission, +}: EditModalProps) => { + const { id, contributionsDisabled, ...baseMission } = mission; + const initialState = useMemo( + () => ({ + contributionsStartBlock: 0, + contributionsEndBlock: 0, + maxNumberOfContributions: 0, + ...baseMission, + }), + [baseMission] + ); + + const signer = useSigner({ chainId: secondaryChain.id }); + + const db = useMemo(() => { + if (signer) return new Database({ signer }); + }, [signer]); + + const mutate = db + ? (state: FormState) => { + const { + name, + description, + tags, + requirements, + deliverables, + rewards, + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions, + } = state; + + return db + .prepare( + `UPDATE ${missionsTable} SET name = ?, description = ?, tags = ?, requirements = ?, deliverables = ?, rewards = ?, contributions_start_block = ?, contributions_end_block = ?, max_number_of_contributions = ? WHERE id = ?` + ) + .bind( + name, + description, + JSON.stringify(tags), + JSON.stringify(requirements), + JSON.stringify(deliverables), + JSON.stringify(rewards), + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions, + id + ) + .run(); + } + : undefined; + + const [formState, setFormState] = useState(initialState); + + const isFormValid = useMemo( + () => isValid(formState) && !isEqual(initialState, formState), + [formState] + ); + + useEffect(() => { + if (isOpen) { + setFormState(initialState); + } + }, [isOpen, setFormState]); + + return ( + + ); +}; diff --git a/garage/src/components/CreateProposalModal.tsx b/garage/src/components/CreateProposalModal.tsx index 74ddcfca..f9b4b05a 100644 --- a/garage/src/components/CreateProposalModal.tsx +++ b/garage/src/components/CreateProposalModal.tsx @@ -25,10 +25,10 @@ import { usePrepareContractWrite, useWaitForTransaction, } from "wagmi"; -import { as0xString } from "../utils/types"; +import { as0xString } from "~/utils/types"; +import { deployment } from "~/env"; +import { abi } from "~/abis/VotingRegistry"; import { TransactionStateAlert } from "./TransactionStateAlert"; -import { deployment } from "../env"; -import { abi } from "../abis/VotingRegistry"; const { votingContractAddress } = deployment; @@ -69,10 +69,7 @@ export const CreateProposalModal = ({ ] = useState(initialFormState); const isValid = - name !== "" && - startBlock > 0 && - endBlock > 0 && - options.length > 0; + name !== "" && startBlock > 0 && endBlock > 0 && options.length > 0; const { config } = usePrepareContractWrite({ address: as0xString(votingContractAddress), @@ -282,7 +279,11 @@ export const CreateProposalModal = ({ {v} - } onClick={() => removeOption(i)} /> + } + onClick={() => removeOption(i)} + /> ))} diff --git a/garage/src/components/FlyParkModals.tsx b/garage/src/components/FlyParkModals.tsx index 016b8113..35543479 100644 --- a/garage/src/components/FlyParkModals.tsx +++ b/garage/src/components/FlyParkModals.tsx @@ -36,31 +36,31 @@ import { DropdownIndicatorProps, chakraComponents, } from "chakra-react-select"; +import debounce from "lodash/debounce"; import { useContractWrite, usePrepareContractWrite, useWaitForTransaction, } from "wagmi"; -import { useAccount } from "../hooks/useAccount"; +import { useAccount } from "~/hooks/useAccount"; import { useOwnedNFTs, Collection, NFT, alchemy, toCollection, -} from "../hooks/useNFTs"; -import debounce from "lodash/debounce"; -import { useActivePilotSessions } from "../hooks/useActivePilotSessions"; -import { Rig, WalletAddress } from "../types"; +} from "~/hooks/useNFTs"; +import { useActivePilotSessions } from "~/hooks/useActivePilotSessions"; +import { Rig, WalletAddress } from "~/types"; +import { mainChain, deployment } from "~/env"; +import { abi } from "~/abis/TablelandRigs"; +import { copySet, toggleInSet } from "~/utils/set"; +import { pluralize } from "~/utils/fmt"; +import { isPresent, isValidAddress, as0xString } from "~/utils/types"; +import unknownPilot from "~/assets/unknown-pilot.svg"; import { ChainAwareButton } from "./ChainAwareButton"; import { TransactionStateAlert } from "./TransactionStateAlert"; import { RigDisplay } from "./RigDisplay"; -import { mainChain, deployment } from "../env"; -import { abi } from "../abis/TablelandRigs"; -import { copySet, toggleInSet } from "../utils/set"; -import { pluralize } from "../utils/fmt"; -import { isPresent, isValidAddress, as0xString } from "../utils/types"; -import unknownPilot from "../assets/unknown-pilot.svg"; const { contractAddress } = deployment; diff --git a/garage/src/components/Footer.tsx b/garage/src/components/Footer.tsx index 5d9a4c05..702c1359 100644 --- a/garage/src/components/Footer.tsx +++ b/garage/src/components/Footer.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Flex, Link, useToken } from "@chakra-ui/react"; +import { ReactComponent as TwitterMark } from "~/assets/twitter-mark.svg"; +import { ReactComponent as OpenseaMark } from "~/assets/opensea-mark.svg"; import { RoundSvgIcon } from "./RoundSvgIcon"; -import { ReactComponent as TwitterMark } from "../assets/twitter-mark.svg"; -import { ReactComponent as OpenseaMark } from "../assets/opensea-mark.svg"; export const Footer = () => { const [primaryColor] = useToken("colors", ["primary"]); diff --git a/garage/src/components/GlobalFlyParkModals.tsx b/garage/src/components/GlobalFlyParkModals.tsx index a372d60f..018f15c1 100644 --- a/garage/src/components/GlobalFlyParkModals.tsx +++ b/garage/src/components/GlobalFlyParkModals.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext, useMemo, useState } from "react"; +import { Rig } from "~/types"; import { TrainRigsModal, PilotRigsModal, ParkRigsModal } from "./FlyParkModals"; -import { Rig } from "../types"; type OnTransactionSubmittedCallback = (txHash: string) => void; @@ -23,13 +23,12 @@ const emptyModal: Modal = { closeModal: () => {}, }; -const GlobalFlyParkModalContext = React.createContext< - GlobalFlyParkModalContextData ->({ - trainRigsModal: emptyModal, - pilotRigsModal: emptyModal, - parkRigsModal: emptyModal, -}); +const GlobalFlyParkModalContext = + React.createContext({ + trainRigsModal: emptyModal, + pilotRigsModal: emptyModal, + parkRigsModal: emptyModal, + }); interface ModalState { open: boolean; diff --git a/garage/src/components/NFTsContext.reducer.ts b/garage/src/components/NFTsContext.reducer.ts index 93ddcee2..3751863d 100644 --- a/garage/src/components/NFTsContext.reducer.ts +++ b/garage/src/components/NFTsContext.reducer.ts @@ -1,7 +1,7 @@ import uniqWith from "lodash/uniqWith"; import isEqual from "lodash/isEqual"; -import { NFT } from "../hooks/useNFTs"; -import { findNFT, NFTIsh } from "../utils/nfts"; +import { NFT } from "~/hooks/useNFTs"; +import { findNFT, NFTIsh } from "~/utils/nfts"; interface LoadNFTs { payload: NFTIsh[]; diff --git a/garage/src/components/NFTsContext.tsx b/garage/src/components/NFTsContext.tsx index de0152d8..0c979f1c 100644 --- a/garage/src/components/NFTsContext.tsx +++ b/garage/src/components/NFTsContext.tsx @@ -2,9 +2,9 @@ import React, { useContext, useEffect, useMemo, useReducer } from "react"; import take from "lodash/take"; import isEqual from "lodash/isEqual"; import { NftTokenType } from "alchemy-sdk"; -import { NFT, alchemy, toNFT } from "../hooks/useNFTs"; -import { findNFT, NFTIsh } from "../utils/nfts"; -import { isPresent } from "../utils/types"; +import { NFT, alchemy, toNFT } from "~/hooks/useNFTs"; +import { findNFT, NFTIsh } from "~/utils/nfts"; +import { isPresent } from "~/utils/types"; import { Action, reducer } from "./NFTsContext.reducer"; interface NFTsContextValue { diff --git a/garage/src/components/ProposalStatusBadge.tsx b/garage/src/components/ProposalStatusBadge.tsx index efe08572..787601eb 100644 --- a/garage/src/components/ProposalStatusBadge.tsx +++ b/garage/src/components/ProposalStatusBadge.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useBlockNumber } from "wagmi"; import { Badge } from "@chakra-ui/react"; -import { Proposal, ProposalStatus } from "../types"; +import { Proposal, ProposalStatus } from "~/types"; export const proposalStatus = ( blockNumber: bigint | undefined, diff --git a/garage/src/components/RigAttributeStatsContext.tsx b/garage/src/components/RigAttributeStatsContext.tsx index 880407b2..8e155ba4 100644 --- a/garage/src/components/RigAttributeStatsContext.tsx +++ b/garage/src/components/RigAttributeStatsContext.tsx @@ -1,8 +1,8 @@ import React, { useContext, useState, useEffect } from "react"; import groupBy from "lodash/groupBy"; import mapValues from "lodash/mapValues"; -import { useTablelandConnection } from "../hooks/useTablelandConnection"; -import { selectTraitRarities } from "../utils/queries"; +import { useTablelandConnection } from "~/hooks/useTablelandConnection"; +import { selectTraitRarities } from "~/utils/queries"; interface RigAttributeStats { [traitType: string]: { [traitValue: string]: number }; diff --git a/garage/src/components/RigDisplay.tsx b/garage/src/components/RigDisplay.tsx index 6aa77cd3..9f6322f2 100644 --- a/garage/src/components/RigDisplay.tsx +++ b/garage/src/components/RigDisplay.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Box, Flex, Image, Spinner } from "@chakra-ui/react"; +import { useRigImageUrls } from "~/hooks/useRigImageUrls"; +import { Rig } from "~/types"; +import { NFT } from "~/hooks/useNFTs"; +import unknownPilot from "~/assets/unknown-pilot.svg"; import { TrainerPilot } from "./TrainerPilot"; -import { useRigImageUrls } from "../hooks/useRigImageUrls"; -import { Rig } from "../types"; -import { NFT } from "../hooks/useNFTs"; -import unknownPilot from "../assets/unknown-pilot.svg"; type BorderWidth = React.ComponentProps["borderWidth"]; diff --git a/garage/src/components/SubmitMissionModal.tsx b/garage/src/components/SubmitMissionModal.tsx index 67cc839c..9aa8a3d5 100644 --- a/garage/src/components/SubmitMissionModal.tsx +++ b/garage/src/components/SubmitMissionModal.tsx @@ -18,12 +18,12 @@ import { usePrepareContractWrite, useWaitForTransaction, } from "wagmi"; -import { as0xString } from "../utils/types"; -import { Mission } from "../types"; -import { useTablelandConnection } from "../hooks/useTablelandConnection"; +import { as0xString } from "~/utils/types"; +import { Mission } from "~/types"; +import { secondaryChain, deployment } from "~/env"; +import { abi } from "~/abis/MissionsManager"; +import { useWaitForTablelandTxn } from "~/hooks/useWaitForTablelandTxn"; import { TransactionStateAlert } from "./TransactionStateAlert"; -import { secondaryChain, deployment } from "../env"; -import { abi } from "../abis/MissionsManager"; const { missionContractAddress } = deployment; @@ -48,8 +48,6 @@ export const SubmitMissionModal = ({ onTransactionCompleted, refresh, }: ModalProps) => { - const { validator } = useTablelandConnection(); - const initialState = { deliverables: mission.deliverables.map(({ key }) => ({ key, value: "" })), }; @@ -90,29 +88,7 @@ export const SubmitMissionModal = ({ onTransactionCompleted(isSuccess); }, [onTransactionCompleted, isTxLoading, isSuccess, data]); - useEffect(() => { - if (validator && data?.hash) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: secondaryChain.id, - transactionHash: data?.hash, - }, - { interval: 2000, signal } - ) - .then((_) => { - refresh(); - }) - .catch((_) => {}); - - return () => { - controller.abort(); - }; - } - }, [validator, data, refresh]); + useWaitForTablelandTxn(secondaryChain.id, data?.hash, refresh, refresh); const onInputChanged = useCallback( (e: React.ChangeEvent, atIndex: number) => { diff --git a/garage/src/components/TablelandConnectButton.tsx b/garage/src/components/TablelandConnectButton.tsx index 5ad5443a..280514c7 100644 --- a/garage/src/components/TablelandConnectButton.tsx +++ b/garage/src/components/TablelandConnectButton.tsx @@ -10,7 +10,7 @@ import { } from "@chakra-ui/react"; import { Link } from "react-router-dom"; import { ConnectButton } from "@rainbow-me/rainbowkit"; -import exit from "../assets/exit.svg"; +import exit from "~/assets/exit.svg"; export const TablelandConnectButton = ({ size = "small", diff --git a/garage/src/components/TrainerPilot.tsx b/garage/src/components/TrainerPilot.tsx index a0b6e7b9..44a16c4c 100644 --- a/garage/src/components/TrainerPilot.tsx +++ b/garage/src/components/TrainerPilot.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Box, Flex, Image } from "@chakra-ui/react"; -import pilot from "../assets/trainer-pilot.svg"; +import pilot from "~/assets/trainer-pilot.svg"; export const TrainerPilot = (props: React.ComponentProps) => { return ( diff --git a/garage/src/components/TransactionStateAlert.tsx b/garage/src/components/TransactionStateAlert.tsx index f46a8b45..a0f14096 100644 --- a/garage/src/components/TransactionStateAlert.tsx +++ b/garage/src/components/TransactionStateAlert.tsx @@ -9,7 +9,7 @@ import { Spinner, } from "@chakra-ui/react"; import { useContractWrite, useWaitForTransaction } from "wagmi"; -import { blockExplorerBaseUrl } from "../env"; +import { blockExplorerBaseUrl } from "~/env"; type TransactionStateAlertProps = Omit< ReturnType, diff --git a/garage/src/components/TransferRigModal.tsx b/garage/src/components/TransferRigModal.tsx index bd695348..d74e04b2 100644 --- a/garage/src/components/TransferRigModal.tsx +++ b/garage/src/components/TransferRigModal.tsx @@ -14,20 +14,19 @@ import { Text, } from "@chakra-ui/react"; import { WarningTwoIcon } from "@chakra-ui/icons"; -import { ethers } from "ethers"; import { useAccount, useContractWrite, usePrepareContractWrite, useWaitForTransaction, } from "wagmi"; -import { Rig } from "../types"; -import { isValidAddress, as0xString } from "../utils/types"; +import { Rig } from "~/types"; +import { isValidAddress, as0xString } from "~/utils/types"; +import { mainChain, deployment } from "~/env"; +import { abi } from "~/abis/TablelandRigs"; +import { ChainAwareButton } from "./ChainAwareButton"; import { TransactionStateAlert } from "./TransactionStateAlert"; import { RigDisplay } from "./RigDisplay"; -import { mainChain, deployment } from "../env"; -import { abi } from "../abis/TablelandRigs"; -import { ChainAwareButton } from "./ChainAwareButton"; const { contractAddress } = deployment; diff --git a/garage/src/hooks/useAccount.ts b/garage/src/hooks/useAccount.ts index b8e6c393..980d538c 100644 --- a/garage/src/hooks/useAccount.ts +++ b/garage/src/hooks/useAccount.ts @@ -2,9 +2,9 @@ import { useEffect, useMemo, useState } from "react"; import { DelegateCash } from "delegatecash"; import { providers } from "ethers"; import { useAccount as useWagmiAccount } from "wagmi"; -import { mainChain, deployment } from "../env"; -import { isPresent } from "../utils/types"; -import { useActingAsAddress } from "../components/ActingAsAddressContext"; +import { mainChain, deployment } from "~/env"; +import { isPresent } from "~/utils/types"; +import { useActingAsAddress } from "~/components/ActingAsAddressContext"; const { id, network } = mainChain; diff --git a/garage/src/hooks/useActivePilotSessions.ts b/garage/src/hooks/useActivePilotSessions.ts index 0493ee85..3f6f01e5 100644 --- a/garage/src/hooks/useActivePilotSessions.ts +++ b/garage/src/hooks/useActivePilotSessions.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { PilotSessionWithRigId } from "../types"; +import { PilotSessionWithRigId } from "~/types"; +import { selectActivePilotSessionsForPilots } from "~/utils/queries"; import { useTablelandConnection } from "./useTablelandConnection"; -import { selectActivePilotSessionsForPilots } from "../utils/queries"; export const useActivePilotSessions = ( pilots: { contract: string; tokenId: string }[] diff --git a/garage/src/hooks/useAddressVotingPower.ts b/garage/src/hooks/useAddressVotingPower.ts index 86a434ac..08a146df 100644 --- a/garage/src/hooks/useAddressVotingPower.ts +++ b/garage/src/hooks/useAddressVotingPower.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; +import { deployment } from "~/env"; import { useTablelandConnection } from "./useTablelandConnection"; -import { deployment } from "../env"; const { ftSnapshotTable } = deployment; diff --git a/garage/src/hooks/useCurrentRoute.ts b/garage/src/hooks/useCurrentRoute.ts index 8ee8ad3c..e5997453 100644 --- a/garage/src/hooks/useCurrentRoute.ts +++ b/garage/src/hooks/useCurrentRoute.ts @@ -1,5 +1,5 @@ import { matchRoutes, useLocation } from "react-router-dom"; -import { routes } from "../routes"; +import { routes } from "~/routes"; export const useCurrentRoute = () => { const location = useLocation(); diff --git a/garage/src/hooks/useIsAdmin.ts b/garage/src/hooks/useIsAdmin.ts new file mode 100644 index 00000000..bad9ceca --- /dev/null +++ b/garage/src/hooks/useIsAdmin.ts @@ -0,0 +1,44 @@ +import { useContractReads } from "wagmi"; +import { as0xString } from "~/utils/types"; +import { secondaryChain, deployment } from "~/env"; +import { abi as missionsAbi } from "~/abis/MissionsManager"; +import { abi as votingAbi } from "~/abis/VotingRegistry"; +import { useAccount } from "./useAccount"; + +const { votingContractAddress, missionContractAddress } = deployment; + +const ZERO_ADDR = "0x0000000000000000000000000000000000000000" as const; + +const MISSIONS_ADMIN_ROLE = + "0x6de1decd3455ea3e945a8b81428c6da1e39da9f747dff73b900c35f95d8d9528" as const; +const VOTING_ADMIN_ROLE = + "0x26e5e0c1d827967646b29471a0f5eef941c85bdbb97c194dc3fa6291a994a148" as const; + +export const useIsAdmin = () => { + const { address } = useAccount(); + + const { isLoading, data } = useContractReads({ + allowFailure: false, + enabled: !!address, + contracts: [ + { + chainId: secondaryChain.id, + address: as0xString(votingContractAddress)!, + abi: votingAbi, + functionName: "hasRole", + args: [VOTING_ADMIN_ROLE, address ?? ZERO_ADDR], + }, + { + chainId: secondaryChain.id, + address: as0xString(missionContractAddress)!, + abi: missionsAbi, + functionName: "hasRole", + args: [MISSIONS_ADMIN_ROLE, address ?? ZERO_ADDR], + }, + ], + }); + + if (!data || isLoading) return { isLoading, data: null }; + + return { isLoading, data: { votingAdmin: data[0], missionsAdin: data[1] } }; +}; diff --git a/garage/src/hooks/useKeysDown.ts b/garage/src/hooks/useKeysDown.ts index aa039e36..44635700 100644 --- a/garage/src/hooks/useKeysDown.ts +++ b/garage/src/hooks/useKeysDown.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useReducer } from "react"; -import { copySet } from "../utils/set"; +import { copySet } from "~/utils/set"; interface KeyEvent { action: "up" | "down"; diff --git a/garage/src/hooks/useMissions.ts b/garage/src/hooks/useMissions.ts index 922197b9..dceb7a85 100644 --- a/garage/src/hooks/useMissions.ts +++ b/garage/src/hooks/useMissions.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; -import { Mission, MissionContribution } from "../types"; +import { Mission, MissionContribution } from "~/types"; +import { deployment, secondaryChain } from "~/env"; import { useTablelandConnection } from "./useTablelandConnection"; -import { deployment, secondaryChain } from "../env"; const { missionsTable, missionContributionsTable } = deployment; @@ -200,3 +200,42 @@ export const useContributions = ( return { contributions, refresh }; }; + +export const useOwnerContributions = (owner?: string) => { + const { db } = useTablelandConnection(); + + const [contributions, setContributions] = useState(); + + useEffect(() => { + if (!owner) return; + + let isCancelled = false; + + db.prepare( + `SELECT + id, + mission_id as "missionId", + created_at as "createdAt", + contributor, + data, + (CASE + WHEN accepted IS NULL THEN 'pending_review' + WHEN accepted = 0 THEN 'rejected' + ELSE 'accepted' END) as "status", + acceptance_motivation as "acceptanceMotivation" + FROM ${missionContributionsTable} WHERE lower(contributor) = lower('${owner}') AND accepted = 1` + ) + .all() + .then(({ results }) => { + if (isCancelled) return; + + setContributions(results); + }); + + return () => { + isCancelled = true; + }; + }, [owner, setContributions]); + + return { contributions }; +}; diff --git a/garage/src/hooks/useNFTs.ts b/garage/src/hooks/useNFTs.ts index e58abd98..78be79bb 100644 --- a/garage/src/hooks/useNFTs.ts +++ b/garage/src/hooks/useNFTs.ts @@ -10,8 +10,8 @@ import { NftFilters, GetNftsForOwnerOptions, } from "alchemy-sdk"; -import { mainChain } from "../env"; import { useQuery } from "@tanstack/react-query"; +import { mainChain } from "~/env"; const wagmiChainToNetwork = (c: Chain): Network => { switch (c) { diff --git a/garage/src/hooks/useOwnedRigs.ts b/garage/src/hooks/useOwnedRigs.ts index bcb4ca02..48a5048c 100644 --- a/garage/src/hooks/useOwnedRigs.ts +++ b/garage/src/hooks/useOwnedRigs.ts @@ -1,11 +1,11 @@ import { useCallback, useEffect, useState } from "react"; -import { RigWithPilots } from "../types"; import { useContractRead } from "wagmi"; +import { RigWithPilots } from "~/types"; +import { selectRigs } from "~/utils/queries"; +import { isValidAddress, as0xString } from "~/utils/types"; +import { mainChain, deployment } from "~/env"; +import { abi } from "~/abis/TablelandRigs"; import { useTablelandConnection } from "./useTablelandConnection"; -import { selectRigs } from "../utils/queries"; -import { isValidAddress, as0xString } from "../utils/types"; -import { mainChain, deployment } from "../env"; -import { abi } from "../abis/TablelandRigs"; const { contractAddress } = deployment; diff --git a/garage/src/hooks/useOwnerActivity.ts b/garage/src/hooks/useOwnerActivity.ts index c1bc4f07..b584022a 100644 --- a/garage/src/hooks/useOwnerActivity.ts +++ b/garage/src/hooks/useOwnerActivity.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { selectOwnerActivity } from "../utils/queries"; +import { selectOwnerActivity } from "~/utils/queries"; +import { Event, EventAction } from "~/types"; import { useTablelandConnection } from "./useTablelandConnection"; -import { Event, EventAction } from "../types"; interface DbEvent { rigId: string; diff --git a/garage/src/hooks/useOwnerFTRewards.ts b/garage/src/hooks/useOwnerFTRewards.ts index d85b7963..3b52708e 100644 --- a/garage/src/hooks/useOwnerFTRewards.ts +++ b/garage/src/hooks/useOwnerFTRewards.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { selectOwnerFTRewards } from "../utils/queries"; +import { selectOwnerFTRewards } from "~/utils/queries"; +import { FTReward } from "~/types"; import { useTablelandConnection } from "./useTablelandConnection"; -import { FTReward } from "../types"; export const useOwnerFTRewards = (owner?: string) => { const { db } = useTablelandConnection(); diff --git a/garage/src/hooks/useOwnerPilots.ts b/garage/src/hooks/useOwnerPilots.ts index 9459d5dd..7f735b43 100644 --- a/garage/src/hooks/useOwnerPilots.ts +++ b/garage/src/hooks/useOwnerPilots.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { selectOwnerPilots } from "../utils/queries"; +import { selectOwnerPilots } from "~/utils/queries"; +import { Pilot } from "~/types"; import { useTablelandConnection } from "./useTablelandConnection"; -import { Pilot } from "../types"; export interface PilotWithFT extends Pilot { flightTime: number; diff --git a/garage/src/hooks/useOwnerVotes.ts b/garage/src/hooks/useOwnerVotes.ts index 3c9fc311..80451a29 100644 --- a/garage/src/hooks/useOwnerVotes.ts +++ b/garage/src/hooks/useOwnerVotes.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { selectOwnerVotes } from "../utils/queries"; +import { selectOwnerVotes } from "~/utils/queries"; +import { ProposalWithOptions } from "~/types"; import { useTablelandConnection } from "./useTablelandConnection"; -import { ProposalWithOptions } from "../types"; export interface Vote { proposal: Pick; diff --git a/garage/src/hooks/useProposal.ts b/garage/src/hooks/useProposal.ts index 7d6833a4..cf3efd11 100644 --- a/garage/src/hooks/useProposal.ts +++ b/garage/src/hooks/useProposal.ts @@ -1,14 +1,10 @@ import { useCallback, useEffect, useState } from "react"; +import { deployment } from "~/env"; +import { ProposalWithOptions } from "~/types"; import { useTablelandConnection } from "./useTablelandConnection"; -import { deployment } from "../env"; -import { ProposalWithOptions } from "../types"; - -const { - proposalsTable, - optionsTable, - votesTable, - ftSnapshotTable, -} = deployment; + +const { proposalsTable, optionsTable, votesTable, ftSnapshotTable } = + deployment; export interface Result { optionId: number; diff --git a/garage/src/hooks/useProposals.ts b/garage/src/hooks/useProposals.ts index 2f071d24..3924d51b 100644 --- a/garage/src/hooks/useProposals.ts +++ b/garage/src/hooks/useProposals.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; -import { Proposal } from "../types"; +import { Proposal } from "~/types"; +import { deployment } from "~/env"; import { useTablelandConnection } from "./useTablelandConnection"; -import { deployment } from "../env"; const { proposalsTable } = deployment; diff --git a/garage/src/hooks/useRig.ts b/garage/src/hooks/useRig.ts index c429b9db..4aae09e6 100644 --- a/garage/src/hooks/useRig.ts +++ b/garage/src/hooks/useRig.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; -import { RigWithPilots } from "../types"; +import { RigWithPilots } from "~/types"; import { useTablelandConnection } from "./useTablelandConnection"; -import { selectRigWithPilots } from "../utils/queries"; +import { selectRigWithPilots } from "~/utils/queries"; export const useRig = (id: string) => { const { db } = useTablelandConnection(); diff --git a/garage/src/hooks/useRigImageUrls.ts b/garage/src/hooks/useRigImageUrls.ts index f4b990b0..8c92f6bd 100644 --- a/garage/src/hooks/useRigImageUrls.ts +++ b/garage/src/hooks/useRigImageUrls.ts @@ -1,4 +1,4 @@ -import { Rig } from "../types"; +import { Rig } from "~/types"; const ipfsUriToGatewayUrl = (ipfsUri: string): string => { const match = ipfsUri.match(/^ipfs:\/\/([a-zA-Z0-9]*)\/(.*)$/); diff --git a/garage/src/hooks/useRigStats.ts b/garage/src/hooks/useRigStats.ts index 0aa45967..e6bda40d 100644 --- a/garage/src/hooks/useRigStats.ts +++ b/garage/src/hooks/useRigStats.ts @@ -4,7 +4,8 @@ import { selectAccountStats, selectTopActivePilotCollections, selectTopFtPilotCollections, -} from "../utils/queries"; + selectTopFtEarners, +} from "~/utils/queries"; import { useTablelandConnection } from "./useTablelandConnection"; export interface Stat { @@ -160,3 +161,32 @@ export const useTopFtPilotCollections = () => { return { stats }; }; + +interface FtLeaderboardEntry { + address: string; + ft: number; +} + +export const useFtLeaderboard = (first: number) => { + const { db } = useTablelandConnection(); + + const [stats, setStats] = useState(); + + useEffect(() => { + let isCancelled = false; + + db.prepare(selectTopFtEarners(first)) + .all() + .then(({ results }) => { + if (isCancelled) return; + + setStats(results); + }); + + return () => { + isCancelled = true; + }; + }, [setStats]); + + return { stats }; +}; diff --git a/garage/src/hooks/useRigsActivity.ts b/garage/src/hooks/useRigsActivity.ts index 26c6eda8..d7160ffa 100644 --- a/garage/src/hooks/useRigsActivity.ts +++ b/garage/src/hooks/useRigsActivity.ts @@ -1,8 +1,7 @@ import { useEffect, useState } from "react"; -import { Event } from "../types"; +import { Event, EventAction } from "~/types"; +import { selectRigsActivity } from "~/utils/queries"; import { useTablelandConnection } from "./useTablelandConnection"; -import { selectRigsActivity } from "../utils/queries"; -import { EventAction } from "../types"; interface DbEvent { rigId: string; diff --git a/garage/src/hooks/useTablelandConnection.ts b/garage/src/hooks/useTablelandConnection.ts index fdae8993..ef9a36c5 100644 --- a/garage/src/hooks/useTablelandConnection.ts +++ b/garage/src/hooks/useTablelandConnection.ts @@ -1,5 +1,5 @@ import { Database, Validator, helpers } from "@tableland/sdk"; -import { mainChain as chain } from "../env"; +import { mainChain as chain } from "~/env"; const db = new Database({ baseUrl: helpers.getBaseUrl(chain.id) }); const validator = new Validator(db.config); diff --git a/garage/src/hooks/useWaitForTablelandTxn.ts b/garage/src/hooks/useWaitForTablelandTxn.ts new file mode 100644 index 00000000..5d166e26 --- /dev/null +++ b/garage/src/hooks/useWaitForTablelandTxn.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react"; +import { useTablelandConnection } from "./useTablelandConnection"; + +export const useWaitForTablelandTxn = ( + chainId: number, + transactionHash: string | undefined, + onComplete: () => void, + onCancelled: () => void +) => { + const { validator } = useTablelandConnection(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (validator && transactionHash) { + const controller = new AbortController(); + const signal = controller.signal; + + setIsLoading(true); + + validator + .pollForReceiptByTransactionHash( + { + chainId, + transactionHash, + }, + { interval: 2000, signal } + ) + .then((_) => { + setIsLoading(false); + onComplete(); + }) + .catch((_) => { + setIsLoading(false); + onCancelled(); + }); + + return () => { + setIsLoading(false); + controller.abort(); + }; + } + }, [chainId, transactionHash, onComplete, onCancelled]); + + return { isLoading }; +}; diff --git a/garage/src/pages/Admin/MissionAdmin.tsx b/garage/src/pages/Admin/MissionAdmin.tsx index d9da52ed..e7cf663d 100644 --- a/garage/src/pages/Admin/MissionAdmin.tsx +++ b/garage/src/pages/Admin/MissionAdmin.tsx @@ -29,6 +29,7 @@ import { Tbody, Textarea, useToast, + useDisclosure, } from "@chakra-ui/react"; import { useContractRead, @@ -37,16 +38,17 @@ import { } from "wagmi"; import { useParams, Link as RouterLink } from "react-router-dom"; import { Database, helpers } from "@tableland/sdk"; -import { useSigner } from "../../hooks/useSigner"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { Footer } from "../../components/Footer"; -import { ChainAwareButton } from "../../components/ChainAwareButton"; -import { MissionContribution } from "../../types"; -import { truncateWalletAddress } from "../../utils/fmt"; -import { as0xString } from "../../utils/types"; -import { useMission, useContributions } from "../../hooks/useMissions"; -import { secondaryChain, deployment } from "../../env"; -import { abi } from "../../abis/MissionsManager"; +import { useSigner } from "~/hooks/useSigner"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { Footer } from "~/components/Footer"; +import { ChainAwareButton } from "~/components/ChainAwareButton"; +import { MissionContribution } from "~/types"; +import { truncateWalletAddress } from "~/utils/fmt"; +import { as0xString } from "~/utils/types"; +import { useMission, useContributions } from "~/hooks/useMissions"; +import { secondaryChain, deployment } from "~/env"; +import { EditMissionModal } from "~/components/CreateMissionModal"; +import { abi } from "~/abis/MissionsManager"; const { missionContributionsTable, missionContractAddress } = deployment; @@ -320,6 +322,8 @@ export const MissionAdmin = () => { if (isSuccess && !isTxLoading) refreshContributionsStatus(); }, [isSuccess, isTxLoading, refreshContributionsStatus]); + const { isOpen, onClose, onOpen } = useDisclosure(); + return ( <> { contribution={reviewContribution} onTransactionCompleted={refreshContributions} /> + {mission && ( + + )} { # {mission.id} – {mission.description} - Contributions + + Contributions Contributions are:{" "} {contributionsDisabled ? "disabled" : "enabled"} @@ -384,14 +394,14 @@ export const MissionAdmin = () => { )} {contributions && ( <> - Contributions pending review + Contributions pending review status === "pending_review" )} onReviewContribution={setReviewContribution} /> - Reviewed + Reviewed status !== "pending_review" diff --git a/garage/src/pages/Admin/index.tsx b/garage/src/pages/Admin/index.tsx index ae337764..592c74d0 100644 --- a/garage/src/pages/Admin/index.tsx +++ b/garage/src/pages/Admin/index.tsx @@ -20,19 +20,22 @@ import { Th, Tbody, HStack, + Spinner, + Text, } from "@chakra-ui/react"; import { Link } from "react-router-dom"; import { Database } from "@tableland/sdk"; -import { useSigner } from "../../hooks/useSigner"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { Footer } from "../../components/Footer"; -import { ChainAwareButton } from "../../components/ChainAwareButton"; -import { isValidAddress } from "../../utils/types"; -import { CreateMissionModal } from "../../components/CreateMissionModal"; -import { CreateProposalModal } from "../../components/CreateProposalModal"; -import { useProposals } from "../../hooks/useProposals"; -import { useAdminMisisons } from "../../hooks/useMissions"; -import { secondaryChain, deployment } from "../../env"; +import { useSigner } from "~/hooks/useSigner"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { Footer } from "~/components/Footer"; +import { ChainAwareButton } from "~/components/ChainAwareButton"; +import { isValidAddress } from "~/utils/types"; +import { CreateMissionModal } from "~/components/CreateMissionModal"; +import { CreateProposalModal } from "~/components/CreateProposalModal"; +import { useProposals } from "~/hooks/useProposals"; +import { useAdminMisisons } from "~/hooks/useMissions"; +import { secondaryChain, deployment } from "~/env"; +import { useIsAdmin } from "~/hooks/useIsAdmin"; const { ftRewardsTable } = deployment; @@ -169,7 +172,7 @@ const GiveFtRewardForm = (props: React.ComponentProps) => { }; const ListProposalsForm = (props: React.ComponentProps) => { - const { proposals } = useProposals(); + const { proposals, refresh } = useProposals(); const [proposalDialogOpen, setCreateProposalDialogOpen] = useState(false); @@ -177,7 +180,10 @@ const ListProposalsForm = (props: React.ComponentProps) => { <> setCreateProposalDialogOpen(false)} + onClose={() => { + refresh(); + setCreateProposalDialogOpen(false); + }} /> ) => { }; const ListMissionsForm = (props: React.ComponentProps) => { - const { missions } = useAdminMisisons(); + const { missions, refresh } = useAdminMisisons(); const [missionDialogOpen, setMissionDialogOpen] = useState(false); @@ -229,7 +235,10 @@ const ListMissionsForm = (props: React.ComponentProps) => { <> setMissionDialogOpen(false)} + onClose={() => { + refresh(); + setMissionDialogOpen(false); + }} /> ) => { ); }; +const NoPermissionsForm = ({ + title, + body, + ...props +}: { title: string; body: string } & React.ComponentProps) => { + return ( + + {title} + + {body} + + + ); +}; + export const Admin = () => { + const { isLoading, data } = useIsAdmin(); + return ( <> { minHeight={`calc(100vh - ${TOPBAR_HEIGHT})`} > - - - + {isLoading || !data ? ( + + ) : ( + <> + + {data.votingAdmin ? ( + + ) : ( + + )} + {data.missionsAdin ? ( + + ) : ( + + )} + + )} diff --git a/garage/src/pages/Dashboard/index.tsx b/garage/src/pages/Dashboard/index.tsx index bebdc2d2..f3409eda 100644 --- a/garage/src/pages/Dashboard/index.tsx +++ b/garage/src/pages/Dashboard/index.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Box, Flex } from "@chakra-ui/react"; -import { TOPBAR_HEIGHT } from "../../Topbar"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { Footer } from "~/components/Footer"; import { RigsInventory } from "./modules/RigsInventory"; import { Stats } from "./modules/Stats"; import { Activity } from "./modules/Activity"; -import { Footer } from "../../components/Footer"; const GRID_GAP = 4; diff --git a/garage/src/pages/Dashboard/modules/Activity.tsx b/garage/src/pages/Dashboard/modules/Activity.tsx index fd301825..3fb94a59 100644 --- a/garage/src/pages/Dashboard/modules/Activity.tsx +++ b/garage/src/pages/Dashboard/modules/Activity.tsx @@ -13,10 +13,10 @@ import { Text, Tr, } from "@chakra-ui/react"; -import { EventAction } from "../../../types"; -import { useRigImageUrls } from "../../../hooks/useRigImageUrls"; -import { useRigsActivity } from "../../../hooks/useRigsActivity"; -import { useNFTCollections } from "../../../hooks/useNFTs"; +import { EventAction } from "~/types"; +import { useRigImageUrls } from "~/hooks/useRigImageUrls"; +import { useRigsActivity } from "~/hooks/useRigsActivity"; +import { useNFTCollections } from "~/hooks/useNFTs"; const getPilotedTitle = ( lookups: Record, diff --git a/garage/src/pages/Dashboard/modules/RigsInventory.tsx b/garage/src/pages/Dashboard/modules/RigsInventory.tsx index ce562033..38aa72f7 100644 --- a/garage/src/pages/Dashboard/modules/RigsInventory.tsx +++ b/garage/src/pages/Dashboard/modules/RigsInventory.tsx @@ -19,22 +19,22 @@ import { } from "@chakra-ui/react"; import { CheckIcon, QuestionIcon } from "@chakra-ui/icons"; import { useBlockNumber } from "wagmi"; -import { useAccount } from "../../../hooks/useAccount"; -import { useOwnedRigs } from "../../../hooks/useOwnedRigs"; -import { useTablelandConnection } from "../../../hooks/useTablelandConnection"; -import { NFT } from "../../../hooks/useNFTs"; -import { useNFTsCached } from "../../../components/NFTsContext"; -import { Rig, RigWithPilots, Pilot } from "../../../types"; -import { RigDisplay } from "../../../components/RigDisplay"; -import { useGlobalFlyParkModals } from "../../../components/GlobalFlyParkModals"; -import { TablelandConnectButton } from "../../../components/TablelandConnectButton"; -import { ChainAwareButton } from "../../../components/ChainAwareButton"; -import { AboutPilotsModal } from "../../../components/AboutPilotsModal"; -import { findNFT } from "../../../utils/nfts"; -import { sleep } from "../../../utils/async"; -import { prettyNumber } from "../../../utils/fmt"; -import { firstSetValue, copySet, toggleInSet } from "../../../utils/set"; -import { mainChain } from "../../../env"; +import { useAccount } from "~/hooks/useAccount"; +import { useOwnedRigs } from "~/hooks/useOwnedRigs"; +import { NFT } from "~/hooks/useNFTs"; +import { useNFTsCached } from "~/components/NFTsContext"; +import { Rig, RigWithPilots, Pilot } from "~/types"; +import { RigDisplay } from "~/components/RigDisplay"; +import { useGlobalFlyParkModals } from "~/components/GlobalFlyParkModals"; +import { TablelandConnectButton } from "~/components/TablelandConnectButton"; +import { ChainAwareButton } from "~/components/ChainAwareButton"; +import { AboutPilotsModal } from "~/components/AboutPilotsModal"; +import { findNFT } from "~/utils/nfts"; +import { sleep } from "~/utils/async"; +import { prettyNumber } from "~/utils/fmt"; +import { firstSetValue, copySet, toggleInSet } from "~/utils/set"; +import { mainChain } from "~/env"; +import { useWaitForTablelandTxn } from "~/hooks/useWaitForTablelandTxn"; interface RigListItemProps { rig: RigWithPilots; @@ -141,9 +141,8 @@ const isSelectable = (rig: Rig, selectable: Selectable): boolean => { }; export const RigsInventory = (props: React.ComponentProps) => { - const { address, actingAsAddress, delegations } = useAccount(); + const { actingAsAddress } = useAccount(); const { rigs, refresh } = useOwnedRigs(actingAsAddress); - const { validator } = useTablelandConnection(); const { data: currentBlockNumber } = useBlockNumber(); const pilots = useMemo(() => { if (!rigs) return; @@ -191,39 +190,15 @@ export const RigsInventory = (props: React.ComponentProps) => { if (!pendingTx) setSelectedRigs(new Set()); }, [pendingTx]); - // Effect that waits until a tableland receipt is available for a tx hash - // and then refreshes the rig data - useEffect(() => { - if (validator && pendingTx) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: mainChain.id, - transactionHash: pendingTx, - }, - { interval: 2000, signal } - ) - .then((_) => { - refreshRigsAndClearPendingTx(); - }) - .catch((_) => { - clearPendingTx(); - }); - - return () => { - controller.abort(); - }; - } - }, [pendingTx, refreshRigsAndClearPendingTx, validator, clearPendingTx]); + useWaitForTablelandTxn( + mainChain.id, + pendingTx, + refreshRigsAndClearPendingTx, + clearPendingTx + ); - const { - trainRigsModal, - pilotRigsModal, - parkRigsModal, - } = useGlobalFlyParkModals(); + const { trainRigsModal, pilotRigsModal, parkRigsModal } = + useGlobalFlyParkModals(); const openTrainModal = useCallback(() => { if (rigs?.length && selectedRigs.size) { diff --git a/garage/src/pages/Dashboard/modules/Stats.tsx b/garage/src/pages/Dashboard/modules/Stats.tsx index 81f27831..16209010 100644 --- a/garage/src/pages/Dashboard/modules/Stats.tsx +++ b/garage/src/pages/Dashboard/modules/Stats.tsx @@ -1,4 +1,5 @@ -import React, { useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; import { Box, Heading, @@ -21,17 +22,22 @@ import { TabPanel, TabPanels, Text, + useBreakpointValue, + Show, } from "@chakra-ui/react"; -import { useAccount } from "../../../hooks/useAccount"; +import { usePublicClient } from "wagmi"; +import { useAccount } from "~/hooks/useAccount"; import { useAccountStats, useStats, useTopActivePilotCollections, useTopFtPilotCollections, + useFtLeaderboard, Stat, -} from "../../../hooks/useRigStats"; -import { useNFTCollections, Collection } from "../../../hooks/useNFTs"; -import { prettyNumber } from "../../../utils/fmt"; +} from "~/hooks/useRigStats"; +import { useNFTCollections, Collection } from "~/hooks/useNFTs"; +import { prettyNumber, truncateWalletAddress } from "~/utils/fmt"; +import { isValidAddress } from "~/utils/types"; const StatItem = ({ name, value }: { name: string; value: number }) => { return ( @@ -107,12 +113,88 @@ const CollectionToplist = ({ ); }; +const FTLeaderboard = ({ + data, +}: { + data: { address: string; ft: number }[]; +}) => { + const publicClient = usePublicClient(); + + const [ensNames, setEnsNames] = useState>({}); + + // Effect that reverse-resolves address->ens for all FT leaderboard entries + // in one batch multicall request + useEffect(() => { + let isCancelled = false; + if (data.length > 0) { + Promise.all( + data + .map(({ address }) => address) + .filter(isValidAddress) + .map(async (address) => { + const ens = await publicClient.getEnsName({ address }); + return [address, ens]; + }) + ).then((v) => { + if (!isCancelled) + setEnsNames(Object.fromEntries(v.filter(([, ens]) => ens))); + }); + } + return () => { + isCancelled = true; + }; + }, [data]); + + const { actingAsAddress } = useAccount(); + + const shouldTruncate = useBreakpointValue({ + base: true, + md: false, + }); + + return ( + + + + + + + + + {data?.slice(0, 15).map(({ address, ft }, index) => { + const isUser = + !!actingAsAddress && + actingAsAddress.toLowerCase() === address?.toLowerCase(); + + const truncatedAddress = address + ? truncateWalletAddress(address) + : ""; + const name = isUser + ? "You" + : ensNames[address] ?? + (shouldTruncate ? truncatedAddress : address); + + return ( + + + + + ); + })} + +
AddressFT
+ {name} + {prettyNumber(ft)}
+ ); +}; + export const Stats = (props: React.ComponentProps) => { const { actingAsAddress } = useAccount(); const { stats } = useStats(); const { stats: accountStats } = useAccountStats(actingAsAddress); const { stats: pilotStats } = useTopActivePilotCollections(); const { stats: ftStats } = useTopFtPilotCollections(); + const { stats: ftLeaderboard } = useFtLeaderboard(15); const contracts = useMemo(() => { if (!pilotStats || !ftStats) return; @@ -139,6 +221,10 @@ export const Stats = (props: React.ComponentProps) => { Global {actingAsAddress && You} Pilots + + FT Leaderboard + FT + @@ -187,6 +273,12 @@ export const Stats = (props: React.ComponentProps) => { + + + Top FT earners + {ftLeaderboard && } + + diff --git a/garage/src/pages/Enter.tsx b/garage/src/pages/Enter.tsx index ac40a440..4a752e2a 100644 --- a/garage/src/pages/Enter.tsx +++ b/garage/src/pages/Enter.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useCallback } from "react"; import { useAccount } from "wagmi"; import { Button, Heading, Flex, Stack, Text } from "@chakra-ui/react"; import { useNavigate } from "react-router-dom"; -import desert from "../assets/desert-bg.png"; -import { TOPBAR_HEIGHT } from "../Topbar"; +import desert from "~/assets/desert-bg.png"; +import { TOPBAR_HEIGHT } from "~/Topbar"; export const Enter = () => { const { isConnected } = useAccount(); diff --git a/garage/src/pages/Gallery/index.tsx b/garage/src/pages/Gallery/index.tsx index 0a199c9e..c261538b 100644 --- a/garage/src/pages/Gallery/index.tsx +++ b/garage/src/pages/Gallery/index.tsx @@ -17,16 +17,16 @@ import { useBreakpointValue, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { RigDisplay } from "../../components/RigDisplay"; -import { Footer } from "../../components/Footer"; -import { Rig } from "../../types"; -import { useTablelandConnection } from "../../hooks/useTablelandConnection"; -import { NFT } from "../../hooks/useNFTs"; -import { selectFilteredRigs } from "../../utils/queries"; -import { isPresent } from "../../utils/types"; -import { findNFT } from "../../utils/nfts"; -import { useNFTsCached } from "../../components/NFTsContext"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { RigDisplay } from "~/components/RigDisplay"; +import { Footer } from "~/components/Footer"; +import { Rig } from "~/types"; +import { useTablelandConnection } from "~/hooks/useTablelandConnection"; +import { NFT } from "~/hooks/useNFTs"; +import { selectFilteredRigs } from "~/utils/queries"; +import { isPresent } from "~/utils/types"; +import { findNFT } from "~/utils/nfts"; +import { useNFTsCached } from "~/components/NFTsContext"; import { ActiveFiltersBar, FilterPanel, toggleValue } from "./modules/Filters"; const GRID_GAP = 4; diff --git a/garage/src/pages/Gallery/modules/Filters.tsx b/garage/src/pages/Gallery/modules/Filters.tsx index 7d94060d..a4bba68c 100644 --- a/garage/src/pages/Gallery/modules/Filters.tsx +++ b/garage/src/pages/Gallery/modules/Filters.tsx @@ -21,9 +21,9 @@ import { VStack, } from "@chakra-ui/react"; import { SmallCloseIcon } from "@chakra-ui/icons"; -import { useDebounce } from "../../../hooks/useDebounce"; -import { copySet, toggleInSet, intersection } from "../../../utils/set"; -import traitData from "../../../traits.json"; +import { useDebounce } from "~/hooks/useDebounce"; +import { copySet, toggleInSet, intersection } from "~/utils/set"; +import traitData from "~/traits.json"; // TODO can we fetch this data dynamically or does that make the loading experience annoying? const { diff --git a/garage/src/pages/Mission/index.tsx b/garage/src/pages/Mission/index.tsx index df52ee33..6ee54bbc 100644 --- a/garage/src/pages/Mission/index.tsx +++ b/garage/src/pages/Mission/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { Box, Button, @@ -20,14 +21,14 @@ import { useDisclosure, } from "@chakra-ui/react"; import { useParams } from "react-router-dom"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { truncateWalletAddress } from "../../utils/fmt"; -import { Mission, MissionContribution } from "../../types"; -import { useMission, useContributions } from "../../hooks/useMissions"; -import { useAccount } from "../../hooks/useAccount"; -import { usePersistentState } from "../../hooks/usePersistentState"; -import { SubmitMissionModal } from "../../components/SubmitMissionModal"; -import { SignManifestoModal } from "../../components/SignManifestoModal"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { truncateWalletAddress } from "~/utils/fmt"; +import { Mission, MissionContribution } from "~/types"; +import { useMission, useContributions } from "~/hooks/useMissions"; +import { useAccount } from "~/hooks/useAccount"; +import { usePersistentState } from "~/hooks/usePersistentState"; +import { SubmitMissionModal } from "~/components/SubmitMissionModal"; +import { SignManifestoModal } from "~/components/SignManifestoModal"; const GRID_GAP = 4; @@ -136,7 +137,11 @@ const Contributions = ({ : truncateWalletAddress(contribution.contributor); return ( - {contributor} + + + {contributor} + + {prettySubmissionStatus(contribution.status)} diff --git a/garage/src/pages/MissionBoard/index.tsx b/garage/src/pages/MissionBoard/index.tsx index f36941c3..8ebc12d9 100644 --- a/garage/src/pages/MissionBoard/index.tsx +++ b/garage/src/pages/MissionBoard/index.tsx @@ -12,12 +12,12 @@ import { VStack, } from "@chakra-ui/react"; import { Link } from "react-router-dom"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { prettyNumber } from "../../utils/fmt"; -import { useOpenMissions } from "../../hooks/useMissions"; -import { usePersistentState } from "../../hooks/usePersistentState"; -import { Mission } from "../../types"; -import { SignManifestoModal } from "../../components/SignManifestoModal"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { prettyNumber } from "~/utils/fmt"; +import { useOpenMissions } from "~/hooks/useMissions"; +import { usePersistentState } from "~/hooks/usePersistentState"; +import { Mission } from "~/types"; +import { SignManifestoModal } from "~/components/SignManifestoModal"; const GRID_GAP = 4; diff --git a/garage/src/pages/OwnerDetails/index.tsx b/garage/src/pages/OwnerDetails/index.tsx index d30066f2..210c69ac 100644 --- a/garage/src/pages/OwnerDetails/index.tsx +++ b/garage/src/pages/OwnerDetails/index.tsx @@ -2,20 +2,22 @@ import React, { useMemo } from "react"; import { Flex, Heading, Text, VStack } from "@chakra-ui/react"; import { useParams } from "react-router-dom"; import { useEnsName } from "wagmi"; -import { useOwnedRigs } from "../../hooks/useOwnedRigs"; -import { useOwnerPilots } from "../../hooks/useOwnerPilots"; -import { useOwnerActivity } from "../../hooks/useOwnerActivity"; -import { useOwnerFTRewards } from "../../hooks/useOwnerFTRewards"; -import { useOwnerVotes } from "../../hooks/useOwnerVotes"; -import { useNFTsCached } from "../../components/NFTsContext"; -import { TOPBAR_HEIGHT } from "../../Topbar"; +import { useOwnerContributions } from "~/hooks/useMissions"; +import { useOwnedRigs } from "~/hooks/useOwnedRigs"; +import { useOwnerPilots } from "~/hooks/useOwnerPilots"; +import { useOwnerActivity } from "~/hooks/useOwnerActivity"; +import { useOwnerFTRewards } from "~/hooks/useOwnerFTRewards"; +import { useOwnerVotes } from "~/hooks/useOwnerVotes"; +import { useNFTsCached } from "~/components/NFTsContext"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { prettyNumber } from "~/utils/fmt"; +import { isValidAddress } from "~/utils/types"; import { RigsGrid } from "./modules/RigsInventory"; import { ActivityLog } from "./modules/Activity"; import { Pilots } from "./modules/Pilots"; import { FTRewards } from "./modules/FTRewards"; import { Votes } from "./modules/Votes"; -import { prettyNumber } from "../../utils/fmt"; -import { isValidAddress } from "../../utils/types"; +import { MBContributions } from "./modules/MBContributions"; const GRID_GAP = 4; @@ -48,6 +50,7 @@ export const OwnerDetails = () => { const { nfts } = useNFTsCached(pilots); const { rewards } = useOwnerFTRewards(owner); const { votes } = useOwnerVotes(owner); + const { contributions } = useOwnerContributions(owner); const { data: ens } = useEnsName({ address: isValidAddress(owner) ? owner : undefined, @@ -104,6 +107,7 @@ export const OwnerDetails = () => { /> + { events?: Event[]; diff --git a/garage/src/pages/OwnerDetails/modules/FTRewards.tsx b/garage/src/pages/OwnerDetails/modules/FTRewards.tsx index bfc0fac9..8b8e6461 100644 --- a/garage/src/pages/OwnerDetails/modules/FTRewards.tsx +++ b/garage/src/pages/OwnerDetails/modules/FTRewards.tsx @@ -13,8 +13,8 @@ import { Td, VStack, } from "@chakra-ui/react"; -import { FTReward } from "../../../types"; -import { prettyNumber } from "../../../utils/fmt"; +import { FTReward } from "~/types"; +import { prettyNumber } from "~/utils/fmt"; interface FTRewardsProps extends React.ComponentProps { rewards?: FTReward[]; diff --git a/garage/src/pages/OwnerDetails/modules/MBContributions.tsx b/garage/src/pages/OwnerDetails/modules/MBContributions.tsx new file mode 100644 index 00000000..9d5831a8 --- /dev/null +++ b/garage/src/pages/OwnerDetails/modules/MBContributions.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { + Box, + Flex, + Heading, + Spinner, + Table, + Tbody, + Thead, + Th, + Text, + Tr, + Td, + VStack, + useBreakpointValue, + Show, +} from "@chakra-ui/react"; +import { MissionContribution } from "../../../types"; + +interface MBContributionsProps extends React.ComponentProps { + contributions?: MissionContribution[]; +} + +const noBorderBottom = { borderBottom: "none" }; + +export const MBContributions = ({ + contributions, + p, + ...props +}: MBContributionsProps) => { + const isMobile = useBreakpointValue({ + base: true, + sm: false, + }); + + const mainRowColAttrs = isMobile ? noBorderBottom : {}; + + return ( + + MB Contributions + + + + + + + + + + + + {contributions && + contributions.map( + ({ missionId, createdAt, acceptanceMotivation }, index) => { + return ( + + + + + + + + + + + + + + + ); + } + )} + +
+ Block number + Approval Message + Mission ID +
+ {createdAt} + {acceptanceMotivation} + {missionId} +
+ {acceptanceMotivation} +
+ {!contributions && ( + + + + )} + {contributions?.length === 0 && ( + + This wallet has not contributed to any missions yet. + + )} +
+ ); +}; diff --git a/garage/src/pages/OwnerDetails/modules/Pilots.tsx b/garage/src/pages/OwnerDetails/modules/Pilots.tsx index fcd387ed..451494f3 100644 --- a/garage/src/pages/OwnerDetails/modules/Pilots.tsx +++ b/garage/src/pages/OwnerDetails/modules/Pilots.tsx @@ -16,11 +16,11 @@ import { Td, VStack, } from "@chakra-ui/react"; -import { NFT } from "../../../hooks/useNFTs"; -import { PilotWithFT } from "../../../hooks/useOwnerPilots"; -import { TrainerPilot } from "../../../components/TrainerPilot"; -import { findNFT } from "../../../utils/nfts"; -import { prettyNumber } from "../../../utils/fmt"; +import { NFT } from "~/hooks/useNFTs"; +import { PilotWithFT } from "~/hooks/useOwnerPilots"; +import { TrainerPilot } from "~/components/TrainerPilot"; +import { findNFT } from "~/utils/nfts"; +import { prettyNumber } from "~/utils/fmt"; interface PilotProps extends React.ComponentProps { nfts?: NFT[]; diff --git a/garage/src/pages/OwnerDetails/modules/RigsInventory.tsx b/garage/src/pages/OwnerDetails/modules/RigsInventory.tsx index b1e32502..3aacb505 100644 --- a/garage/src/pages/OwnerDetails/modules/RigsInventory.tsx +++ b/garage/src/pages/OwnerDetails/modules/RigsInventory.tsx @@ -10,10 +10,10 @@ import { VStack, } from "@chakra-ui/react"; import { Link } from "react-router-dom"; -import { Rig } from "../../../types"; -import { NFT } from "../../../hooks/useNFTs"; -import { RigDisplay } from "../../../components/RigDisplay"; -import { findNFT } from "../../../utils/nfts"; +import { Rig } from "~/types"; +import { NFT } from "~/hooks/useNFTs"; +import { RigDisplay } from "~/components/RigDisplay"; +import { findNFT } from "~/utils/nfts"; interface RigsGridProps extends React.ComponentProps { rigs?: Rig[]; diff --git a/garage/src/pages/OwnerDetails/modules/Votes.tsx b/garage/src/pages/OwnerDetails/modules/Votes.tsx index be9b35c0..9779e18e 100644 --- a/garage/src/pages/OwnerDetails/modules/Votes.tsx +++ b/garage/src/pages/OwnerDetails/modules/Votes.tsx @@ -15,9 +15,9 @@ import { Show, useBreakpointValue, } from "@chakra-ui/react"; -import { prettyNumber } from "../../../utils/fmt"; -import { Vote } from "../../../hooks/useOwnerVotes"; import { Link } from "react-router-dom"; +import { prettyNumber } from "~/utils/fmt"; +import { Vote } from "~/hooks/useOwnerVotes"; interface VotesProps extends React.ComponentProps { votes?: Vote[]; diff --git a/garage/src/pages/PilotDetails/index.tsx b/garage/src/pages/PilotDetails/index.tsx index 024d5087..68d15587 100644 --- a/garage/src/pages/PilotDetails/index.tsx +++ b/garage/src/pages/PilotDetails/index.tsx @@ -22,18 +22,18 @@ import { } from "@chakra-ui/react"; import { useParams, Link as RouterLink } from "react-router-dom"; import { useAccount, useBlockNumber, useContractRead, useEnsName } from "wagmi"; -import { RoundSvgIcon } from "../../components/RoundSvgIcon"; -import { useTablelandConnection } from "../../hooks/useTablelandConnection"; -import { useNFTs, NFT } from "../../hooks/useNFTs"; -import { useRigImageUrls } from "../../hooks/useRigImageUrls"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { prettyNumber, truncateWalletAddress } from "../../utils/fmt"; -import { mainChain, openseaBaseUrl } from "../../env"; -import { PilotSessionWithRigId } from "../../types"; -import { ReactComponent as OpenseaMark } from "../../assets/opensea-mark.svg"; -import { selectPilotSessionsForPilot } from "../../utils/queries"; -import { isValidAddress, as0xString } from "../../utils/types"; -import { abi } from "../../abis/ERC721"; +import { RoundSvgIcon } from "~/components/RoundSvgIcon"; +import { useTablelandConnection } from "~/hooks/useTablelandConnection"; +import { useNFTs, NFT } from "~/hooks/useNFTs"; +import { useRigImageUrls } from "~/hooks/useRigImageUrls"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { prettyNumber, truncateWalletAddress } from "~/utils/fmt"; +import { mainChain, openseaBaseUrl } from "~/env"; +import { PilotSessionWithRigId } from "~/types"; +import { ReactComponent as OpenseaMark } from "~/assets/opensea-mark.svg"; +import { selectPilotSessionsForPilot } from "~/utils/queries"; +import { isValidAddress, as0xString } from "~/utils/types"; +import { abi } from "~/abis/ERC721"; const GRID_GAP = 4; diff --git a/garage/src/pages/Proposal/index.tsx b/garage/src/pages/Proposal/index.tsx index 84a3a920..4b7fe313 100644 --- a/garage/src/pages/Proposal/index.tsx +++ b/garage/src/pages/Proposal/index.tsx @@ -36,20 +36,20 @@ import { useWaitForTransaction, } from "wagmi"; import { useParams, Link } from "react-router-dom"; -import { TransactionStateAlert } from "../../components/TransactionStateAlert"; +import { TransactionStateAlert } from "~/components/TransactionStateAlert"; import { ProposalStatusBadge, proposalStatus, -} from "../../components/ProposalStatusBadge"; -import { useProposal, Result, Vote } from "../../hooks/useProposal"; -import { useTablelandConnection } from "../../hooks/useTablelandConnection"; -import { useAddressVotingPower } from "../../hooks/useAddressVotingPower"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { prettyNumber, truncateWalletAddress } from "../../utils/fmt"; -import { as0xString } from "../../utils/types"; -import { ProposalWithOptions, ProposalStatus } from "../../types"; -import { deployment, secondaryChain } from "../../env"; -import { abi } from "../../abis/VotingRegistry"; +} from "~/components/ProposalStatusBadge"; +import { useProposal, Result, Vote } from "~/hooks/useProposal"; +import { useAddressVotingPower } from "~/hooks/useAddressVotingPower"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { prettyNumber, truncateWalletAddress } from "~/utils/fmt"; +import { as0xString } from "~/utils/types"; +import { ProposalWithOptions, ProposalStatus } from "~/types"; +import { deployment, secondaryChain } from "~/env"; +import { abi } from "~/abis/VotingRegistry"; +import { useWaitForTablelandTxn } from "~/hooks/useWaitForTablelandTxn"; const ipfsGatewayBaseUrl = "https://nftstorage.link"; @@ -87,7 +87,6 @@ const CastVote = ({ (v) => v.address.toLowerCase() === address?.toLowerCase() ); - const { validator } = useTablelandConnection(); const { votingPower } = useAddressVotingPower(address, proposal.id); const { data: blockNumber } = useBlockNumber(); @@ -155,34 +154,29 @@ const CastVote = ({ hash: data?.hash, }); - useEffect(() => { - if (validator && data?.hash) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: secondaryChain.id, - transactionHash: data?.hash, - }, - { interval: 2000, signal } - ) - .then((_) => { - toast({ - title: "Vote successful", - status: "success", - duration: 5_000, - }); - refresh(); - }) - .catch((_) => {}); - - return () => { - controller.abort(); - }; - } - }, [validator, data, refresh, toast]); + const onTxnCompleted = useCallback(() => { + toast({ + title: "Vote successful", + status: "success", + duration: 5_000, + }); + refresh(); + }, [toast, refresh]); + + const onTxnFailed = useCallback(() => { + toast({ + title: "Not able to fetch tableland receipt, please refresh the page.", + status: "warning", + duration: 5_000, + }); + }, [toast, refresh]); + + useWaitForTablelandTxn( + secondaryChain.id, + data?.hash, + onTxnCompleted, + onTxnFailed + ); const isMobile = useBreakpointValue( { base: true, lg: false }, diff --git a/garage/src/pages/Proposals/index.tsx b/garage/src/pages/Proposals/index.tsx index 3abaf6a8..144317c9 100644 --- a/garage/src/pages/Proposals/index.tsx +++ b/garage/src/pages/Proposals/index.tsx @@ -11,14 +11,14 @@ import { } from "@chakra-ui/react"; import { useBlockNumber } from "wagmi"; import { Link } from "react-router-dom"; -import { useProposals } from "../../hooks/useProposals"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { prettyNumber } from "../../utils/fmt"; -import { Proposal, ProposalStatus } from "../../types"; +import { useProposals } from "~/hooks/useProposals"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { prettyNumber } from "~/utils/fmt"; +import { Proposal, ProposalStatus } from "~/types"; import { proposalStatus, ProposalStatusBadge, -} from "../../components/ProposalStatusBadge"; +} from "~/components/ProposalStatusBadge"; const GRID_GAP = 4; diff --git a/garage/src/pages/RigDetails/index.tsx b/garage/src/pages/RigDetails/index.tsx index c859633d..93d61f64 100644 --- a/garage/src/pages/RigDetails/index.tsx +++ b/garage/src/pages/RigDetails/index.tsx @@ -29,29 +29,29 @@ import { import { ArrowForwardIcon, ExternalLinkIcon } from "@chakra-ui/icons"; import { useParams, Link as RouterLink } from "react-router-dom"; import { useBlockNumber, useContractReads, useEnsName } from "wagmi"; -import { useAccount } from "../../hooks/useAccount"; -import { useGlobalFlyParkModals } from "../../components/GlobalFlyParkModals"; -import { ChainAwareButton } from "../../components/ChainAwareButton"; -import { RoundSvgIcon } from "../../components/RoundSvgIcon"; -import { TransferRigModal } from "../../components/TransferRigModal"; -import { useNFTsCached } from "../../components/NFTsContext"; -import { TOPBAR_HEIGHT } from "../../Topbar"; -import { RigDisplay } from "../../components/RigDisplay"; +import { useAccount } from "~/hooks/useAccount"; +import { useGlobalFlyParkModals } from "~/components/GlobalFlyParkModals"; +import { ChainAwareButton } from "~/components/ChainAwareButton"; +import { RoundSvgIcon } from "~/components/RoundSvgIcon"; +import { TransferRigModal } from "~/components/TransferRigModal"; +import { useNFTsCached } from "~/components/NFTsContext"; +import { TOPBAR_HEIGHT } from "~/Topbar"; +import { RigDisplay } from "~/components/RigDisplay"; import { FlightLog } from "./modules/FlightLog"; import { Pilots } from "./modules/Pilots"; import { RigAttributes } from "./modules/RigAttributes"; -import { useTablelandConnection } from "../../hooks/useTablelandConnection"; -import { useRig } from "../../hooks/useRig"; -import { findNFT } from "../../utils/nfts"; -import { prettyNumber, truncateWalletAddress } from "../../utils/fmt"; -import { sleep } from "../../utils/async"; -import { isValidAddress, as0xString } from "../../utils/types"; -import { mainChain, openseaBaseUrl, deployment } from "../../env"; -import { RigWithPilots } from "../../types"; -import { abi } from "../../abis/TablelandRigs"; -import { ReactComponent as OpenseaMark } from "../../assets/opensea-mark.svg"; -import { ReactComponent as TablelandMark } from "../../assets/tableland.svg"; -import { ReactComponent as FilecoinMark } from "../../assets/filecoin-mark.svg"; +import { useRig } from "~/hooks/useRig"; +import { findNFT } from "~/utils/nfts"; +import { prettyNumber, truncateWalletAddress } from "~/utils/fmt"; +import { sleep } from "~/utils/async"; +import { isValidAddress, as0xString } from "~/utils/types"; +import { mainChain, openseaBaseUrl, deployment } from "~/env"; +import { RigWithPilots } from "~/types"; +import { abi } from "~/abis/TablelandRigs"; +import { ReactComponent as OpenseaMark } from "~/assets/opensea-mark.svg"; +import { ReactComponent as TablelandMark } from "~/assets/tableland.svg"; +import { ReactComponent as FilecoinMark } from "~/assets/filecoin-mark.svg"; +import { useWaitForTablelandTxn } from "~/hooks/useWaitForTablelandTxn"; const { contractAddress } = deployment; @@ -267,7 +267,6 @@ export const RigDetails = () => { const { actingAsAddress } = useAccount(); const { data: currentBlockNumber } = useBlockNumber(); const { rig, refresh: refreshRig } = useRig(id || ""); - const { validator } = useTablelandConnection(); const { data: contractData, refetch } = useContractReads({ allowFailure: false, @@ -325,42 +324,18 @@ export const RigDetails = () => { sleep(500).then((_) => setPendingTx(undefined)); }, [refresh, setPendingTx]); - // Effect that waits until a tableland receipt is available for a tx hash - // and then refreshes the rig data - useEffect(() => { - if (validator && pendingTx) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: mainChain.id, - transactionHash: pendingTx, - }, - { interval: 2000, signal } - ) - .then((_) => { - refreshRigAndClearPendingTx(); - }) - .catch((_) => { - clearPendingTx(); - }); - - return () => { - controller.abort(); - }; - } - }, [pendingTx, refreshRigAndClearPendingTx, validator, clearPendingTx]); + useWaitForTablelandTxn( + mainChain.id, + pendingTx, + refreshRigAndClearPendingTx, + clearPendingTx + ); const currentNFT = rig?.currentPilot && nfts && findNFT(rig.currentPilot, nfts); - const { - trainRigsModal, - pilotRigsModal, - parkRigsModal, - } = useGlobalFlyParkModals(); + const { trainRigsModal, pilotRigsModal, parkRigsModal } = + useGlobalFlyParkModals(); const onOpenTrainModal = useCallback(() => { if (rig) trainRigsModal.openModal([rig], setPendingTx); diff --git a/garage/src/pages/RigDetails/modules/Badges.tsx b/garage/src/pages/RigDetails/modules/Badges.tsx index 3d338315..00147774 100644 --- a/garage/src/pages/RigDetails/modules/Badges.tsx +++ b/garage/src/pages/RigDetails/modules/Badges.tsx @@ -14,9 +14,9 @@ import { Tr, VStack, } from "@chakra-ui/react"; -import { RigWithPilots } from "../../../types"; -import { NFT } from "../../../hooks/useNFTs"; -import { education, code } from "../../../assets/badges/"; +import { RigWithPilots } from "~/types"; +import { NFT } from "~/hooks/useNFTs"; +import { education, code } from "~/assets/badges/"; type BadgesProps = React.ComponentProps & { rig: RigWithPilots; diff --git a/garage/src/pages/RigDetails/modules/FlightLog.tsx b/garage/src/pages/RigDetails/modules/FlightLog.tsx index 2d603a26..1cf05ab8 100644 --- a/garage/src/pages/RigDetails/modules/FlightLog.tsx +++ b/garage/src/pages/RigDetails/modules/FlightLog.tsx @@ -11,10 +11,10 @@ import { VStack, } from "@chakra-ui/react"; import { Link } from "react-router-dom"; -import { RigWithPilots } from "../../../types"; -import { NFT } from "../../../hooks/useNFTs"; -import { findNFT } from "../../../utils/nfts"; -import { truncateWalletAddress } from "../../../utils/fmt"; +import { RigWithPilots } from "~/types"; +import { NFT } from "~/hooks/useNFTs"; +import { findNFT } from "~/utils/nfts"; +import { truncateWalletAddress } from "~/utils/fmt"; type FlightLogProps = React.ComponentProps & { rig: RigWithPilots; @@ -26,15 +26,20 @@ export const FlightLog = ({ rig, nfts, p, ...props }: FlightLogProps) => { .flatMap(({ startTime, endTime, owner, contract, tokenId }) => { const { name = "Trainer" } = findNFT({ tokenId, contract }, nfts) || {}; - let events = [{ type: `Piloted ${name}`, startTime, timestamp: startTime, owner }]; + let events = [ + { type: `Piloted ${name}`, startTime, timestamp: startTime, owner }, + ]; if (endTime) { - events = [...events, { type: "Parked", startTime, timestamp: endTime, owner }]; + events = [ + ...events, + { type: "Parked", startTime, timestamp: endTime, owner }, + ]; } return events; }) - .sort((a, b) => (b.timestamp - a.timestamp) || (b.startTime - a.startTime)); + .sort((a, b) => b.timestamp - a.timestamp || b.startTime - a.startTime); return ( diff --git a/garage/src/pages/RigDetails/modules/Pilots.tsx b/garage/src/pages/RigDetails/modules/Pilots.tsx index 3bd6d790..93759e48 100644 --- a/garage/src/pages/RigDetails/modules/Pilots.tsx +++ b/garage/src/pages/RigDetails/modules/Pilots.tsx @@ -19,15 +19,15 @@ import { useDisclosure, } from "@chakra-ui/react"; import { QuestionIcon } from "@chakra-ui/icons"; -import { RigWithPilots, PilotSession } from "../../../types"; -import { TrainerPilot } from "../../../components/TrainerPilot"; -import { ChainAwareButton } from "../../../components/ChainAwareButton"; -import { AboutPilotsModal } from "../../../components/AboutPilotsModal"; import { useBlockNumber } from "wagmi"; -import { NFT } from "../../../hooks/useNFTs"; -import { findNFT } from "../../../utils/nfts"; -import { prettyNumber, pluralize } from "../../../utils/fmt"; -import { mainChain, deployment } from "../../../env"; +import { RigWithPilots, PilotSession } from "~/types"; +import { TrainerPilot } from "~/components/TrainerPilot"; +import { ChainAwareButton } from "~/components/ChainAwareButton"; +import { AboutPilotsModal } from "~/components/AboutPilotsModal"; +import { NFT } from "~/hooks/useNFTs"; +import { findNFT } from "~/utils/nfts"; +import { prettyNumber, pluralize } from "~/utils/fmt"; +import { mainChain, deployment } from "~/env"; const getPilots = ( rig: RigWithPilots, diff --git a/garage/src/pages/RigDetails/modules/RigAttributes.tsx b/garage/src/pages/RigDetails/modules/RigAttributes.tsx index b584990b..c86eb2c1 100644 --- a/garage/src/pages/RigDetails/modules/RigAttributes.tsx +++ b/garage/src/pages/RigDetails/modules/RigAttributes.tsx @@ -10,9 +10,9 @@ import { Tr, VStack, } from "@chakra-ui/react"; -import { RigWithPilots, Attribute } from "../../../types"; -import { useRigAttributeStats } from "../../../components/RigAttributeStatsContext"; -import { toPercent } from "../../../utils/fmt"; +import { RigWithPilots, Attribute } from "~/types"; +import { useRigAttributeStats } from "~/components/RigAttributeStatsContext"; +import { toPercent } from "~/utils/fmt"; type RigAttributesProps = React.ComponentProps & { rig: RigWithPilots; diff --git a/garage/src/utils/queries.ts b/garage/src/utils/queries.ts index be6ba1da..4d4f22a6 100644 --- a/garage/src/utils/queries.ts +++ b/garage/src/utils/queries.ts @@ -1,4 +1,4 @@ -import { mainChain as chain, deployment } from "../env"; +import { mainChain as chain, deployment } from "~/env"; const { rigsTable, @@ -293,6 +293,31 @@ export const selectTopFtPilotCollections = (): string => { ORDER BY ft DESC`; }; +export const selectTopFtEarners = ( + first: number, + offset: number = 0 +): string => { + return ` + SELECT + address, + sum(ft) AS "ft" + FROM ( + SELECT + owner AS "address", + (coalesce(end_time, BLOCK_NUM(${chain.id})) - start_time) as "ft" + FROM ${pilotSessionsTable} + UNION ALL + SELECT + recipient AS "address", + amount AS "ft" + FROM ${ftRewardsTable} + ) + GROUP BY address + ORDER BY ft DESC + LIMIT ${first} + OFFSET ${offset}`; +}; + export const selectPilotSessionsForPilot = ( contract: string, tokenId: string diff --git a/garage/src/utils/types.ts b/garage/src/utils/types.ts index cb495a8a..eef7e0d9 100644 --- a/garage/src/utils/types.ts +++ b/garage/src/utils/types.ts @@ -1,4 +1,4 @@ -import { WalletAddress } from "../types"; +import { WalletAddress } from "~/types"; export const isPresent = (t: T | undefined | null): t is T => t !== undefined && t !== null; diff --git a/garage/tsconfig.json b/garage/tsconfig.json index 46ff09c3..71a2e0c5 100644 --- a/garage/tsconfig.json +++ b/garage/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "baseUrl": ".", "target": "ESNext", "types": ["node", "vite/client", "vite-plugin-svgr/client"], "useDefineForClassFields": true, @@ -15,7 +16,10 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "paths": { + "~/*": ["src/*"] + } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/garage/vite.config.ts b/garage/vite.config.ts index ca0071d3..e58a4829 100644 --- a/garage/vite.config.ts +++ b/garage/vite.config.ts @@ -2,10 +2,11 @@ import { defineConfig } from "vite"; import { ViteEjsPlugin } from "vite-plugin-ejs"; import svgr from "vite-plugin-svgr"; import react from "@vitejs/plugin-react"; +import tsconfigPaths from "vite-tsconfig-paths"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [svgr(), react(), ViteEjsPlugin()], + plugins: [svgr(), react(), ViteEjsPlugin(), tsconfigPaths()], optimizeDeps: { exclude: ["@tableland/sqlparser"], }, diff --git a/pkg/storage/local/impl/store.go b/pkg/storage/local/impl/store.go index 4b98c691..ed8e2c99 100644 --- a/pkg/storage/local/impl/store.go +++ b/pkg/storage/local/impl/store.go @@ -111,11 +111,11 @@ const ( drop table if exists rigs; drop table if exists rig_parts; drop table if exists rig_deals; - // TODO: Investigate what we want here. - // drop table if exists cids; - // drop table if exists table_names; - // drop table if exists txns; ` + // TODO: Investigate what we want here. + // drop table if exists cids; + // drop table if exists table_names; + // drop table if exists txns;. ) // Store provides local data storage. diff --git a/viewer/package.json b/viewer/package.json index 17f6f7ad..436c2142 100644 --- a/viewer/package.json +++ b/viewer/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "nuxt", "start": "nuxt start", - "generate": "nuxt generate", + "generate": "NODE_OPTIONS=--openssl-legacy-provider nuxt generate", "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", "lint": "npm run lint:js", "test": "jest --passWithNoTests"