From 7c5fa00c98b862e13922031c057245bdfe5d225f Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Mon, 15 Apr 2024 15:58:48 +0200 Subject: [PATCH 1/8] :construction: --- components/loginButton.tsx | 11 +- context/tezos-toolkit.tsx | 52 +++ context/wallet.tsx | 148 ++++++++ package-lock.json | 695 ++++++++++++++++++------------------- package.json | 12 +- pages/_app.tsx | 147 ++++---- 6 files changed, 625 insertions(+), 440 deletions(-) create mode 100644 context/tezos-toolkit.tsx create mode 100644 context/wallet.tsx diff --git a/components/loginButton.tsx b/components/loginButton.tsx index 15d9adaa..47f0398d 100644 --- a/components/loginButton.tsx +++ b/components/loginButton.tsx @@ -1,19 +1,22 @@ import { useContext } from "react"; import { AppDispatchContext, AppStateContext } from "../context/state"; +import { useWallet } from "../context/wallet"; import { connectWallet } from "../utils/connectWallet"; const LoginButton = () => { const state = useContext(AppStateContext)!; const dispatch = useContext(AppDispatchContext)!; + const { + state: { wallet }, + connectWallet, + } = useWallet(); return ( - - {isFetching || !state.attemptedInitialLogin ? ( -
- + + + + + +
+ -
-
-
-
-
+
+
+ + + + + ); } From 5325f7f8ef34ea34ee216e519a6ed910288517c2 Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Thu, 18 Apr 2024 16:20:55 +0200 Subject: [PATCH 2/8] :boom: :recycle: extract wallet and tezostoolkit into context --- components/Banner.tsx | 7 +- components/ContractExecution.tsx | 8 +- components/ExecuteContractForm.tsx | 131 +++++++++++ components/FA1_2.tsx | 6 +- components/FA2Transfer.tsx | 13 +- components/Layout.tsx | 243 +++++++++++++++++++++ components/LoginModal.tsx | 34 +-- components/PoeModal.tsx | 57 +++-- components/ProposalCard.tsx | 10 +- components/Sidebar.tsx | 23 +- components/create/basic.tsx | 10 +- components/create/createLoader.tsx | 16 +- components/create/settings.tsx | 16 +- components/import/aliases.tsx | 5 +- components/import/basic.tsx | 17 +- components/import/importLoader.tsx | 14 +- components/loginButton.tsx | 2 - components/modal.tsx | 6 +- components/navbar.tsx | 31 +-- components/proposalSignForm.tsx | 22 +- components/proposals.tsx | 38 ++-- components/signersForm.tsx | 32 ++- components/textInputWithComplete.tsx | 2 +- components/topUpForm.tsx | 45 ++-- components/transferForm.tsx | 36 +-- context/aliases.tsx | 5 +- context/{state.ts => state.tsx} | 160 +++++--------- context/wallet.tsx | 5 +- pages/[walletAddress]/beacon.tsx | 8 +- pages/[walletAddress]/dashboard.tsx | 5 +- pages/[walletAddress]/fund-wallet.tsx | 31 +-- pages/[walletAddress]/history.tsx | 19 +- pages/[walletAddress]/new-proposal.tsx | 6 +- pages/[walletAddress]/proposals.tsx | 39 ++-- pages/[walletAddress]/settings.tsx | 10 +- pages/_app.tsx | 289 +------------------------ pages/address-book.tsx | 11 +- pages/new-wallet.tsx | 16 +- utils/useIsOwner.ts | 23 +- utils/useWalletTokens.ts | 6 +- 40 files changed, 777 insertions(+), 680 deletions(-) create mode 100644 components/ExecuteContractForm.tsx create mode 100644 components/Layout.tsx rename context/{state.ts => state.tsx} (68%) diff --git a/components/Banner.tsx b/components/Banner.tsx index b03c5d87..e0206311 100644 --- a/components/Banner.tsx +++ b/components/Banner.tsx @@ -1,14 +1,13 @@ import { Cross1Icon } from "@radix-ui/react-icons"; -import { useContext } from "react"; -import { AppDispatchContext, AppStateContext } from "../context/state"; +import { useAppDispatch, useAppState } from "../context/state"; type props = { children: React.ReactNode; }; const Banner = ({ children }: props) => { - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; + const state = useAppState(); + const dispatch = useAppDispatch(); return state.hasBanner ? (
diff --git a/components/ContractExecution.tsx b/components/ContractExecution.tsx index d0364d7e..3c9eaf34 100644 --- a/components/ContractExecution.tsx +++ b/components/ContractExecution.tsx @@ -9,6 +9,7 @@ import { } from "formik"; import React, { useContext, useEffect } from "react"; import { AppStateContext } from "../context/state"; +import { useTezosToolkit } from "../context/tezos-toolkit"; import { parseContract, genLambda, @@ -485,10 +486,11 @@ function ExecuteForm( onShapeChange: (v: object) => void; }> ) { - const state = useContext(AppStateContext)!; + const state = useAppState(); + + const { tezos } = useTezosToolkit(); const address = props.address; - const conn = state.connection; const setLoading = props.setLoading; const loading = props.loading; @@ -497,7 +499,7 @@ function ExecuteForm( (async () => { try { setLoading(true); - const c = await conn.contract.at(address); + const c = await tezos.contract.at(address); const initTokenTable: Record = {}; const token: token = parseContract(c, initTokenTable); diff --git a/components/ExecuteContractForm.tsx b/components/ExecuteContractForm.tsx new file mode 100644 index 00000000..7dc61e09 --- /dev/null +++ b/components/ExecuteContractForm.tsx @@ -0,0 +1,131 @@ +import { Field, useFormikContext } from "formik"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useWallet } from "../context/wallet"; +import { tezToMutez } from "../utils/tez"; +import ExecuteForm from "./ContractExecution"; +import ContractLoader from "./contractLoader"; +import renderError from "./formUtils"; +import { state, Basic } from "./transferForm"; + +export function ExecuteContractForm( + props: React.PropsWithoutRef<{ + setField: (lambda: string, metadata: string) => void; + getFieldProps: (name: string) => { value: string }; + id: number; + defaultState?: state; + onReset: () => void; + onChange: (state: state) => void; + }> +) { + const { submitCount, setFieldValue } = useFormikContext(); + const submitCountRef = useRef(submitCount); + const { + state: { userAddress }, + } = useWallet(); + + const [state, setState] = useState( + () => props.defaultState ?? { address: "", amount: 0, shape: {} } + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const setLoader = useCallback((x: boolean) => setLoading(x), []); + + useEffect(() => { + props.onChange(state); + }, [state, props.onChange]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+

+ + #{(props.id + 1).toString().padStart(2, "0")} + + Execute Contract +

+ setState({ ...x, shape: {} })} + onAmountChange={amount => { + setState({ ...state, amount: tezToMutez(Number(amount)) }); + setFieldValue(`transfers.${props.id}.amount`, amount); + }} + onAddressChange={address => { + setState({ ...state, address }); + }} + withContinue={!userAddress} + address={userAddress} + defaultValues={{ + amount: undefined, + address: undefined, + }} + /> + {!!userAddress && ( + { + setState(v => ({ + ...v, + shape: { ...v.shape, init: shape }, + })); + }} + setState={shape => { + setState(v => ({ ...v, shape })); + }} + reset={() => setState({ address: "", amount: 0, shape: {} })} + address={userAddress} + amount={state.amount} + setField={(lambda: string, metadata: string) => { + props.setField(lambda, metadata); + }} + onReset={() => { + setState({ address: "", amount: 0, shape: {} }); + props.onReset(); + }} + /> + )} + { + // This is a tricky way to detect when the submition happened + // We want this message to show only on submit, not on every change + if (!!v) { + submitCountRef.current = submitCount; + setError(""); + return; + } + + if (submitCountRef.current === submitCount - 1) { + setError("Please fill contract"); + submitCountRef.current += 1; + } + + // Returning a value to prevent submition + return true; + }} + /> + { + if (!!v) return; + + // Returning a value to prevent submition + return true; + }} + /> + {!!error && renderError(error)} +
+ ); +} diff --git a/components/FA1_2.tsx b/components/FA1_2.tsx index 5cc25c66..db73da2d 100644 --- a/components/FA1_2.tsx +++ b/components/FA1_2.tsx @@ -1,7 +1,7 @@ import { Field, useFormikContext } from "formik"; -import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { TZKT_API_URL, THUMBNAIL_URL } from "../context/config"; -import { AppStateContext } from "../context/state"; +import { useAppState } from "../context/state"; import { debounce, promiseWithTimeout } from "../utils/timeout"; import { proposals } from "../versioned/interface"; import ErrorMessage from "./ErrorMessage"; @@ -75,7 +75,7 @@ const tokenToOption = (fa1_2Token: fa1_2Token) => { }; const FA1_2 = ({ index, remove, children }: props) => { - const state = useContext(AppStateContext)!; + const state = useAppState(); const { setFieldValue, getFieldProps } = useFormikContext(); const [isFetching, setIsFetching] = useState(true); diff --git a/components/FA2Transfer.tsx b/components/FA2Transfer.tsx index 1642d24e..4fcc1bd7 100644 --- a/components/FA2Transfer.tsx +++ b/components/FA2Transfer.tsx @@ -2,17 +2,10 @@ import { PlusIcon } from "@radix-ui/react-icons"; import { validateAddress, ValidationResult } from "@taquito/utils"; import BigNumber from "bignumber.js"; import { Field, FieldProps, useFormikContext } from "formik"; -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { v4 as uuidV4 } from "uuid"; import { TZKT_API_URL, THUMBNAIL_URL } from "../context/config"; -import { AppStateContext } from "../context/state"; +import { useAppState } from "../context/state"; import { debounce } from "../utils/timeout"; import { proposals } from "../versioned/interface"; import ErrorMessage from "./ErrorMessage"; @@ -90,7 +83,7 @@ const FA2Transfer = ({ toExclude, autoSetField = true, }: fa2TransferProps) => { - const state = useContext(AppStateContext)!; + const state = useAppState(); const { getFieldProps, setFieldValue, errors } = useFormikContext(); diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 00000000..9151f759 --- /dev/null +++ b/components/Layout.tsx @@ -0,0 +1,243 @@ +import { LocalStorage, NetworkType } from "@airgap/beacon-sdk"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { validateAddress, ValidationResult } from "@taquito/utils"; +import { AppProps } from "next/app"; +import { usePathname } from "next/navigation"; +import router, { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import P2PClient from "../context/P2PClient"; +import { PREFERED_NETWORK } from "../context/config"; +import { init, useAppDispatch, useAppState } from "../context/state"; +import { useTezosToolkit } from "../context/tezos-toolkit"; +import { useWallet } from "../context/wallet"; +import { contractStorage } from "../types/Proposal0_3_1"; +import { fetchContract } from "../utils/fetchContract"; +import Banner from "./Banner"; +import LoginModal from "./LoginModal"; +import PoeModal from "./PoeModal"; +import Sidebar from "./Sidebar"; +import Spinner from "./Spinner"; +import Footer from "./footer"; +import NavBar from "./navbar"; + +export default function Layout({ + Component, + pageProps, +}: Pick) { + const state = useAppState(); + const dispatch = useAppDispatch(); + const { tezos } = useTezosToolkit(); + const { + state: { wallet }, + } = useWallet(); + + const [data, setData] = useState(); + const [hasSidebar, setHasSidebar] = useState(false); + const [isFetching, setIsFetching] = useState(true); + const router = useRouter(); + const path = usePathname(); + const isSidebarHidden = + Object.values(state.contracts).length === 0 && + (path === "/" || + path === "/new-wallet" || + path === "/import-wallet" || + path === "/address-book"); + + useEffect(() => { + if (!path) return; + + const queryParams = new URLSearchParams(window.location.search); + + const isPairing = queryParams.has("type") && queryParams.has("data"); + + if (isPairing) { + setData(queryParams.get("data")!); + } + + const contracts = Object.keys(state.contracts); + + if ((path === "/" || path === "") && contracts.length > 0) { + const contract = contracts[0]; + + router.replace(`/${contract}/dashboard`); + return; + } else if (path === "/" || path === "") { + // Get rid of query in case it comes from beacon + router.replace("/"); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.currentContract, path, state.contracts]); + + useEffect(() => { + (async () => { + if ( + router.pathname.includes("[walletAddress]") && + !router.query.walletAddress + ) + return; + + if ( + !router.query.walletAddress || + Array.isArray(router.query.walletAddress) || + (router.query.walletAddress === state.currentContract && + !!state.currentStorage) + ) { + setIsFetching(false); + return; + } + + if (!!state.contracts[router.query.walletAddress]) { + dispatch({ + type: "setCurrentContract", + payload: router.query.walletAddress, + }); + setIsFetching(false); + return; + } + + if ( + validateAddress(router.query.walletAddress) !== ValidationResult.VALID + ) { + setIsFetching(false); + router.replace( + `/invalid-contract?address=${router.query.walletAddress}` + ); + return; + } + + if (state.currentStorage?.address === router.query.walletAddress) { + setIsFetching(false); + return; + } + + try { + const storage = await fetchContract(tezos, router.query.walletAddress); + + if (!storage) { + setIsFetching(false); + router.replace( + `/invalid-contract?address=${router.query.walletAddress}` + ); + return; + } + + storage.address = router.query.walletAddress; + + dispatch({ + type: "setCurrentStorage", + payload: storage as contractStorage & { address: string }, + }); + + dispatch({ + type: "setCurrentContract", + payload: router.query.walletAddress, + }); + + setIsFetching(false); + } catch (e) { + setIsFetching(false); + + router.replace( + `/invalid-contract?address=${router.query.walletAddress}` + ); + } + })(); + }, [ + router.query.walletAddress, + state.currentContract, + dispatch, + router, + state.currentStorage, + tezos, + state.contracts, + ]); + useEffect(() => { + (async () => { + if (wallet === null) { + let a = init(); + dispatch({ type: "init", payload: a }); + + const p2pClient = new P2PClient({ + name: "TzSafe", + storage: new LocalStorage("P2P"), + }); + + await p2pClient.init(); + await p2pClient.connect(p2pClient.handleMessages); + + // Connect stored peers + Object.entries(a.connectedDapps).forEach(async ([address, dapps]) => { + Object.values(dapps).forEach(data => { + p2pClient + .addPeer(data) + .catch(_ => console.log("Failed to connect to peer", data)); + }); + }); + + dispatch!({ type: "p2pConnect", payload: p2pClient }); + } + })(); + }, [wallet]); + + useEffect(() => { + setHasSidebar(false); + }, [path]); + + return ( +
+ + ); +} diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx index bef94a31..3539705a 100644 --- a/components/LoginModal.tsx +++ b/components/LoginModal.tsx @@ -1,6 +1,7 @@ -import { useContext, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Event } from "../context/P2PClient"; -import { AppDispatchContext, AppStateContext } from "../context/state"; +import { useAppDispatch, useAppState } from "../context/state"; +import { useWallet } from "../context/wallet"; import { decodeData } from "../pages/[walletAddress]/beacon"; import { connectWallet } from "../utils/connectWallet"; import { signers } from "../versioned/apis"; @@ -19,20 +20,23 @@ enum State { } const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; + const state = useAppState(); + const dispatch = useAppDispatch(); const [parsedData, setParsedData] = useState(); const [error, setError] = useState(); + const { + state: { userAddress, wallet }, + } = useWallet(); + const options = useMemo(() => { - if (!state.address) return []; + if (!userAddress) return []; return Object.keys(state.contracts).flatMap(address => { if (!hasTzip27Support(state.contracts[address].version)) return []; - if (!signers(state.contracts[address]).includes(state.address!)) - return []; + if (!signers(state.contracts[address]).includes(userAddress!)) return []; return [ { @@ -42,7 +46,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { }, ]; }); - }, [state.contracts, state.address]); + }, [state.contracts, userAddress]); const [selectedWallet, setSelectedWallet] = useState< { id: string; value: string; label: string } | undefined @@ -51,7 +55,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { const [currentState, setCurrentState] = useState(() => State.LOADING); useEffect(() => { - if (!state.p2pClient || !state.attemptedInitialLogin) return; + if (!state.p2pClient) return; try { const decoded = decodeData(data); @@ -59,7 +63,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { setParsedData(decoded); state.p2pClient!.on(Event.PERMISSION_REQUEST, () => { - if (state.attemptedInitialLogin && !state.address) { + if (!userAddress) { setCurrentState(State.LOGIN); } else if ( decoded.name.toLowerCase().includes("tzsafe") || @@ -77,13 +81,13 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { setError((e as Error).message); setCurrentState(State.ERROR); } - }, [data, state.p2pClient, state.attemptedInitialLogin]); + }, [data, state.p2pClient]); useEffect(() => { - if (currentState === State.LOGIN && !!state.address) { + if (currentState === State.LOGIN && !!userAddress) { setCurrentState(State.INITIAL); } - }, [state.address]); + }, [userAddress]); return (
@@ -259,9 +263,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { }} type="button" className={`rounded bg-primary px-4 py-2 font-medium text-white hover:bg-red-500 hover:outline-none focus:bg-red-500 ${ - !state.beaconWallet - ? "pointer-events-none opacity-50" - : "" + !wallet ? "pointer-events-none opacity-50" : "" }`} > Connect{" "} diff --git a/components/PoeModal.tsx b/components/PoeModal.tsx index 7724ed87..1e0c15d0 100644 --- a/components/PoeModal.tsx +++ b/components/PoeModal.tsx @@ -17,15 +17,17 @@ import { tzip16 } from "@taquito/tzip16"; import { validateAddress, ValidationResult } from "@taquito/utils"; import BigNumber from "bignumber.js"; import { usePathname } from "next/navigation"; -import { ChangeEvent, useContext, useEffect, useMemo, useState } from "react"; +import { ChangeEvent, useEffect, useMemo, useState } from "react"; import { Event } from "../context/P2PClient"; import { PREFERED_NETWORK } from "../context/config"; import { generateDelegateMichelson, generateExecuteContractMichelson, } from "../context/generateLambda"; -import { AppDispatchContext, AppStateContext } from "../context/state"; +import { useAppDispatch, useAppState } from "../context/state"; +import { useTezosToolkit } from "../context/tezos-toolkit"; import fetchVersion from "../context/version"; +import { useWallet } from "../context/wallet"; import { CustomView, customViewMatchers } from "../dapps"; import { State } from "../pages/[walletAddress]/beacon"; import { proposalContent } from "../types/display"; @@ -71,8 +73,14 @@ export const transferToProposalContent = ( } }; const PoeModal = () => { - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; + const state = useAppState(); + const dispatch = useAppDispatch(); + const { + state: { userAddress }, + } = useWallet(); + + const { tezos } = useTezosToolkit(); + const path = usePathname(); const walletTokens = useWalletTokens(); @@ -107,7 +115,7 @@ const PoeModal = () => { try { for (let i = 0; i < customViewMatchers.length; ++i) { - dapp = customViewMatchers[i](rows as transaction[], state.connection); + dapp = customViewMatchers[i](rows as transaction[], tezos); if (!!dapp) break; } } catch (e) { @@ -161,9 +169,7 @@ const PoeModal = () => { case TezosOperationType.TRANSACTION: if (!!detail.parameters) { try { - const contract = await state.connection.contract.at( - detail.destination - ); + const contract = await tezos.contract.at(detail.destination); if ( !contract.entrypoints.entrypoints[ @@ -308,10 +314,7 @@ const PoeModal = () => { const signPayloadCb = async (message: SignPayloadRequest) => { try { - const contract = await state.connection.wallet.at( - message.sourceAddress, - tzip16 - ); + const contract = await tezos.wallet.at(message.sourceAddress, tzip16); const storage: any = await contract.storage(); let version = await fetchVersion(contract!); @@ -327,7 +330,7 @@ const PoeModal = () => { let v = toStorage(version, storage, BigNumber(0)); - if (!signers(v).includes(state.address ?? "")) { + if (!signers(v).includes(userAddress ?? "")) { state.p2pClient?.abortRequest( message.id, "Current user isn't a signer" @@ -338,13 +341,11 @@ const PoeModal = () => { const signed = //@ts-expect-error For a reason I don't know I can't access client like in taquito documentation // See: https://tezostaquito.io/docs/signing/#generating-a-signature-with-beacon-sdk - await state.connection.wallet.walletProvider.client.requestSignPayload( - { - signingType: message.signingType, - payload: message.payload, - sourceAddress: state.address, - } - ); + await tezos.wallet.walletProvider.client.requestSignPayload({ + signingType: message.signingType, + payload: message.payload, + sourceAddress: userAddress, + }); await state.p2pClient?.signResponse( message.id, message.signingType, @@ -378,8 +379,8 @@ const PoeModal = () => { try { const ops = await api.generateSpoeOps( message.payload, - await state.connection.wallet.at(message.contractAddress), - state.connection + await tezos.wallet.at(message.contractAddress), + tezos ); await state.p2pClient?.spoeResponse(message.id, ops); @@ -413,7 +414,7 @@ const PoeModal = () => { simulatedProofOfEventCb ); }; - }, [state.p2pClient, state.address]); + }, [state.p2pClient, userAddress]); if (!message && !transfers) return null; @@ -712,9 +713,7 @@ const PoeModal = () => { let hash; try { - const cc = await state.connection.wallet.at( - address - ); + const cc = await tezos.wallet.at(address); const versioned = VersionedApi( state.contracts[address].version, address @@ -722,7 +721,7 @@ const PoeModal = () => { const submitTimeoutAndHash = await versioned.submitTxProposals( cc, - state.connection, + tezos, { transfers }, false, undefined, @@ -825,7 +824,7 @@ const PoeModal = () => { try { setCurrentState(State.TRANSACTION); - const cc = await state.connection.wallet.at(address); + const cc = await tezos.wallet.at(address); const versioned = VersionedApi( state.contracts[address].version, address @@ -834,7 +833,7 @@ const PoeModal = () => { setTimeoutAndHash( await versioned.submitTxProposals( cc, - state.connection, + tezos, { transfers: [ { diff --git a/components/ProposalCard.tsx b/components/ProposalCard.tsx index 7719c79b..de515933 100644 --- a/components/ProposalCard.tsx +++ b/components/ProposalCard.tsx @@ -1,7 +1,8 @@ import { InfoCircledIcon, TriangleDownIcon } from "@radix-ui/react-icons"; import * as Switch from "@radix-ui/react-switch"; -import { useContext, useState, useMemo } from "react"; -import { AppStateContext } from "../context/state"; +import { useState, useMemo } from "react"; +import { useAppState } from "../context/state"; +import { useTezosToolkit } from "../context/tezos-toolkit"; import { CustomView, customViewMatchers } from "../dapps"; import { proposalContent } from "../types/display"; import { walletToken } from "../utils/useWalletTokens"; @@ -45,7 +46,8 @@ const ProposalCard = ({ isSignable = false, shouldResolve = false, }: ProposalCardProps) => { - const state = useContext(AppStateContext)!; + const state = useAppState(); + const { tezos } = useTezosToolkit(); const currentContract = state.currentContract ?? ""; const [isOpen, setIsOpen] = useState(false); @@ -75,7 +77,7 @@ const ProposalCard = ({ try { for (let i = 0; i < customViewMatchers.length; ++i) { - dapp = customViewMatchers[i](rows as transaction[], state.connection); + dapp = customViewMatchers[i](rows as transaction[], tezos); if (!!dapp) break; } } catch (e) { diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 15bb55b3..14adc217 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -18,12 +18,10 @@ import React, { useState, } from "react"; import { PREFERED_NETWORK } from "../context/config"; -import { - AppDispatchContext, - AppStateContext, - contractStorage, -} from "../context/state"; +import { contractStorage, useAppDispatch, useAppState } from "../context/state"; +import { TezosToolkitContext } from "../context/tezos-toolkit"; import fetchVersion from "../context/version"; +import { useWallet } from "../context/wallet"; import { version } from "../types/display"; import useIsOwner from "../utils/useIsOwner"; import { signers, toStorage } from "../versioned/apis"; @@ -140,8 +138,13 @@ const Sidebar = ({ const [isClient, setIsClient] = useState(false); - let state = useContext(AppStateContext)!; - let dispatch = useContext(AppDispatchContext)!; + let state = useAppState(); + let dispatch = useAppDispatch(); + const { + state: { userAddress }, + } = useWallet(); + + const { tezos } = useContext(TezosToolkitContext); const isOwner = useIsOwner(); @@ -159,8 +162,8 @@ const Sidebar = ({ (async () => { if (!state.currentContract) return; - let c = await state.connection.wallet.at(state.currentContract, tzip16); - let balance = await state.connection.tz.getBalance(state.currentContract); + let c = await tezos.wallet.at(state.currentContract, tzip16); + let balance = await tezos.tz.getBalance(state.currentContract); const storage = (await c.storage()) as contractStorage; let version = await (state.contracts[state.currentContract] @@ -439,7 +442,7 @@ const Sidebar = ({ href={`/${state.currentContract}/fund-wallet`} className={linkClass( path?.includes("/fund-wallet") ?? false, - !state.address || isLoading + !userAddress || isLoading )} onClick={onClose} > diff --git a/components/create/basic.tsx b/components/create/basic.tsx index f6a3beff..9a0738da 100644 --- a/components/create/basic.tsx +++ b/components/create/basic.tsx @@ -2,13 +2,17 @@ import { ErrorMessage, Field, Form, Formik } from "formik"; import Link from "next/link"; import { useContext } from "react"; import FormContext from "../../context/formContext"; -import { AppStateContext } from "../../context/state"; +import { useAppState } from "../../context/state"; +import { useWallet } from "../../context/wallet"; import renderError from "../formUtils"; function Basic() { const { activeStepIndex, setActiveStepIndex, formState, setFormState } = useContext(FormContext)!; - const state = useContext(AppStateContext)!; + const state = useAppState(); + const { + state: { userAddress }, + } = useWallet(); const byName = Object.fromEntries( Object.entries(state?.aliases || {}).map(([k, v]) => [v, k]) @@ -98,7 +102,7 @@ function Basic() { - {state?.address == null ? ( + {userAddress == null ? (
@@ -109,7 +110,7 @@ const NavBar = (_: React.PropsWithChildren) => { Open user menu
@@ -125,7 +126,7 @@ const NavBar = (_: React.PropsWithChildren) => {
diff --git a/components/transferForm.tsx b/components/transferForm.tsx index ffbf7135..2d4f46fc 100644 --- a/components/transferForm.tsx +++ b/components/transferForm.tsx @@ -25,7 +25,9 @@ import React, { useState, } from "react"; import { MODAL_TIMEOUT, PREFERED_NETWORK } from "../context/config"; -import { AppStateContext, contractStorage } from "../context/state"; +import { contractStorage, useAppState } from "../context/state"; +import { TezosToolkitContext } from "../context/tezos-toolkit"; +import { useWallet } from "../context/wallet"; import { tezToMutez } from "../utils/tez"; import { VersionedApi } from "../versioned/apis"; import { Versioned, proposals, transfer } from "../versioned/interface"; @@ -45,7 +47,7 @@ import TextInputWithCompletion from "./textInputWithComplete"; type Nullable = T | null | undefined; -function Basic({ +export function Basic({ id, setFormState, defaultValues, @@ -69,7 +71,8 @@ function Basic({ { setTouched: setAddressTouched }, ] = useField(`transfers.${id}.walletAddress`); - const state = useContext(AppStateContext)!; + const { tezos } = useContext(TezosToolkitContext); + const [localFormState, setLocalFormState] = useState<{ amount: number | undefined; address: string; @@ -127,7 +130,7 @@ function Basic({ setContractLoading(true); const exists = await (async () => { try { - await state.connection.contract.at(address.trim()); + await tezos.contract.at(address.trim()); return true; } catch (e) { return false; @@ -216,7 +219,7 @@ function Basic({ ); } -type state = { +export type state = { address: string; amount: number; shape: object; @@ -233,6 +236,9 @@ function ExecuteContractForm( ) { const { submitCount, setFieldValue } = useFormikContext(); const submitCountRef = useRef(submitCount); + const { + state: { userAddress }, + } = useWallet(); const [state, setState] = useState( () => props.defaultState ?? { address: "", amount: 0, shape: {} } @@ -272,14 +278,14 @@ function ExecuteContractForm( onAddressChange={address => { setState({ ...state, address }); }} - withContinue={!state.address} - address={state.address} + withContinue={!userAddress} + address={userAddress} defaultValues={{ amount: undefined, address: undefined, }} /> - {!!state.address && ( + {!!userAddress && ( ({ ...v, shape })); }} reset={() => setState({ address: "", amount: 0, shape: {} })} - address={state.address} + address={userAddress} amount={state.amount} setField={(lambda: string, metadata: string) => { props.setField(lambda, metadata); @@ -370,8 +376,12 @@ function TransferForm( contract: contractStorage; }> ) { - const state = useContext(AppStateContext)!; + const state = useAppState(); const router = useRouter(); + const { + state: { userAddress }, + } = useWallet(); + const { tezos } = useContext(TezosToolkitContext); const portalIdx = useRef(0); const [isMenuOpen, setIsMenuOpen] = useState(true); @@ -381,7 +391,7 @@ function TransferForm( const [formState, setFormState] = useState(() => initialProps); const executeContractStateRef = useRef<{ [k: number]: state }>({}); - if (state?.address == null) { + if (userAddress == null) { return null; } @@ -508,14 +518,14 @@ function TransferForm( setFormState(values); setLoading(true); try { - const cc = await state.connection.wallet.at(props.address); + const cc = await tezos.wallet.at(props.address); const versioned = VersionedApi(props.contract.version, props.address); const proposals: proposals = { transfers: values.transfers }; setTimeoutAndHash( await versioned.submitTxProposals( cc, - state.connection, + tezos, proposals, undefined, undefined, diff --git a/context/aliases.tsx b/context/aliases.tsx index e5756e04..8907edd4 100644 --- a/context/aliases.tsx +++ b/context/aliases.tsx @@ -1,6 +1,7 @@ import React, { useRef } from "react"; import { createContext } from "react"; import { TZKT_API_URL } from "./config"; +import { useAppState } from "./state"; type AliasesContextType = { getAlias(address: string, defaultAlias: string): Promise; @@ -12,13 +13,13 @@ export const AliasesContext = createContext({ export const AliasesProvider = ({ children, - aliasesFromState, }: { children: React.ReactNode; - aliasesFromState: { [address: string]: string }; }) => { const aliases = useRef>>({}); + const aliasesFromState = useAppState().aliases; + const getTzktAlias = async (address: string) => { // address can be empty string... if (address === "") return undefined; diff --git a/context/state.ts b/context/state.tsx similarity index 68% rename from context/state.ts rename to context/state.tsx index c0616a5b..7a886e07 100644 --- a/context/state.ts +++ b/context/state.tsx @@ -1,32 +1,22 @@ -import { AccountInfo, getSenderId } from "@airgap/beacon-sdk"; -import { BeaconWallet } from "@taquito/beacon-wallet"; -import { PollingSubscribeProvider, TezosToolkit } from "@taquito/taquito"; -import { Tzip12Module } from "@taquito/tzip12"; -import { - Handler, - IpfsHttpHandler, - MetadataProvider, - TezosStorageHandler, - Tzip16Module, -} from "@taquito/tzip16"; +import { getSenderId } from "@airgap/beacon-sdk"; import BigNumber from "bignumber.js"; -import { Context, createContext, Dispatch } from "react"; +import { + Context, + createContext, + Dispatch, + useContext, + useReducer, +} from "react"; import { contractStorage } from "../types/app"; import { Trie } from "../utils/radixTrie"; import { p2pData } from "../versioned/interface"; import P2PClient from "./P2PClient"; -import { IPFS_NODE, RPC_URL } from "./config"; -import { BeaconSigner } from "./signer"; +import { useWallet } from "./wallet"; type tezosState = { - connection: TezosToolkit; - beaconWallet: BeaconWallet | null; p2pClient: P2PClient | null; - address: string | null; - balance: string | null; currentContract: string | null; currentStorage: contractStorage | null; - accountInfo: AccountInfo | null; contracts: { [address: string]: contractStorage }; aliases: { [address: string]: string }; aliasTrie: Trie; @@ -39,7 +29,6 @@ type tezosState = { }; // Increasing this number will trigger a useEffect in the proposal page proposalRefresher: number; - attemptedInitialLogin: boolean; }; type storage = { contracts: { [address: string]: contractStorage }; @@ -47,54 +36,23 @@ type storage = { }; let emptyState = (): tezosState => { - const connection = new TezosToolkit(RPC_URL); - - const customHandler = new Map([ - ["ipfs", new IpfsHttpHandler(IPFS_NODE)], - ["tezos-storage", new TezosStorageHandler()], - ]); - - const customMetadataProvider = new MetadataProvider(customHandler); - connection.addExtension(new Tzip16Module(customMetadataProvider)); - connection.addExtension(new Tzip12Module()); - - connection.setStreamProvider( - connection.getFactory(PollingSubscribeProvider)({ - shouldObservableSubscriptionRetry: true, - pollingIntervalMilliseconds: 500, - }) - ); - return { - beaconWallet: null, p2pClient: null, contracts: {}, aliases: {}, - balance: null, - address: null, currentContract: null, currentStorage: null, - accountInfo: null, - connection, aliasTrie: new Trie(), hasBanner: true, delegatorAddresses: undefined, connectedDapps: {}, proposalRefresher: 0, - attemptedInitialLogin: false, }; }; type action = - | { type: "beaconConnect"; payload: BeaconWallet } | { type: "p2pConnect"; payload: P2PClient } | { type: "init"; payload: tezosState } - | { - type: "login"; - accountInfo: AccountInfo; - address: string; - balance: string; - } | { type: "addContract"; payload: { @@ -116,7 +74,6 @@ type action = payload: string; } | { type: "removeContract"; address: string } - | { type: "logout" } | { type: "loadStorage"; payload: storage } | { type: "writeStorage"; payload: storage } | { type: "setDelegatorAddresses"; payload: string[] } @@ -150,9 +107,9 @@ type action = payload: boolean; }; -const saveState = (state: tezosState) => { +const saveState = (state: tezosState, userAddress: string) => { localStorage.setItem( - `app_state:${state.address}`, + `app_state:${userAddress}`, JSON.stringify({ contracts: state.contracts, aliases: state.aliases, @@ -162,28 +119,22 @@ const saveState = (state: tezosState) => { ); }; -function reducer(state: tezosState, action: action): tezosState { +function reducer( + state: tezosState, + action: action, + { userAddress }: { userAddress: string } +): tezosState { switch (action.type) { - case "beaconConnect": { - state.connection.setProvider({ - rpc: RPC_URL, - wallet: action.payload, - }); - state.connection.setSignerProvider(new BeaconSigner(action.payload)); - return { ...state, beaconWallet: action.payload }; - } case "p2pConnect": { return { ...state, p2pClient: action.payload }; } case "addDapp": { - if (!state.address) return state; - state.connectedDapps[action.payload.address] ??= {}; state.connectedDapps[action.payload.address][action.payload.data.appUrl] = action.payload.data; - saveState(state); + saveState(state, userAddress); return state; } @@ -198,7 +149,7 @@ function reducer(state: tezosState, action: action): tezosState { delete newState.connectedDapps[state.currentContract][action.payload]; - saveState(newState); + saveState(newState, userAddress); return newState; } @@ -219,7 +170,7 @@ function reducer(state: tezosState, action: action): tezosState { aliasTrie: Trie.fromAliases(Object.entries(aliases)), }; - saveState(newState); + saveState(newState, userAddress); return newState; } @@ -239,7 +190,7 @@ function reducer(state: tezosState, action: action): tezosState { aliasTrie: Trie.fromAliases(Object.entries(aliases)), }; - saveState(newState); + saveState(newState, userAddress); return newState; } @@ -253,7 +204,8 @@ function reducer(state: tezosState, action: action): tezosState { contracts, }; - if (state.contracts[action.payload.address]) saveState(newState); + if (state.contracts[action.payload.address]) + saveState(newState, userAddress); return newState; } @@ -263,7 +215,7 @@ function reducer(state: tezosState, action: action): tezosState { currentContract: action.payload, }; - saveState(newState); + saveState(newState, userAddress); return newState; case "setCurrentStorage": @@ -287,42 +239,13 @@ function reducer(state: tezosState, action: action): tezosState { return { ...action.payload, contracts, - attemptedInitialLogin: state.attemptedInitialLogin, currentContract: state.currentContract ?? action.payload.currentContract, currentStorage: state.currentStorage, aliasTrie: Trie.fromAliases(Object.entries(action.payload.aliases)), }; } - case "login": { - const rawStorage = window!.localStorage.getItem( - `app_state:${action.address}` - )!; - const storage: storage = JSON.parse(rawStorage); - return { - ...state, - ...storage, - balance: action.balance, - accountInfo: action.accountInfo, - address: action.address, - attemptedInitialLogin: true, - }; - } - case "logout": { - let { connection } = emptyState(); - - const newState: tezosState = { - ...state, - beaconWallet: null, - balance: null, - accountInfo: null, - address: null, - connection: connection, - p2pClient: null, - }; - return newState; - } case "removeContract": { const { [action.address]: _, ...contracts } = state.contracts; const { [action.address]: __, ...aliases } = state.aliases; @@ -352,7 +275,7 @@ function reducer(state: tezosState, action: action): tezosState { connectedDapps, }; - saveState(newState); + saveState(newState, userAddress); return newState; } @@ -365,8 +288,7 @@ function reducer(state: tezosState, action: action): tezosState { return { ...state, delegatorAddresses: action.payload }; case "refreshProposals": return { ...state, proposalRefresher: state.proposalRefresher + 1 }; - case "setAttemptedInitialLogin": - return { ...state, attemptedInitialLogin: action.payload }; + default: { throw `notImplemented: ${action.type}`; } @@ -376,10 +298,33 @@ function init(): tezosState { return emptyState(); } -let AppStateContext: Context = - createContext(null); -let AppDispatchContext: Context | null> = +const AppStateContext = createContext<{ + state: tezosState; + dispatch: React.Dispatch; +}>({ state: emptyState(), dispatch: () => {} }); +const AppDispatchContext: Context | null> = createContext | null>(null); + +const AppStateProvider = ({ children }: { children: React.ReactNode }) => { + const { + state: { userAddress }, + } = useWallet(); + const [state, dispatch]: [tezosState, React.Dispatch] = useReducer( + (state: tezosState, action: action) => + reducer(state, action, { userAddress: userAddress || "" }), + emptyState() + ); + + return ( + + {children} + + ); +}; + +const useAppState = () => useContext(AppStateContext).state; +const useAppDispatch = () => useContext(AppStateContext).dispatch; + export { type tezosState, type action, @@ -389,4 +334,7 @@ export { AppDispatchContext, emptyState, reducer, + AppStateProvider, + useAppState, + useAppDispatch, }; diff --git a/context/wallet.tsx b/context/wallet.tsx index 7c406da9..82d72581 100644 --- a/context/wallet.tsx +++ b/context/wallet.tsx @@ -11,6 +11,7 @@ type WalletState = { wallet: BeaconWallet | undefined; userAccount: AccountInfo | undefined; userAddress: string | undefined; + userBalance: number | undefined; }; type WalletContextType = { @@ -39,6 +40,7 @@ const initialState: WalletState = { wallet: undefined, userAccount: undefined, userAddress: undefined, + userBalance: undefined, }; export const WalletContext = createContext({ @@ -60,8 +62,9 @@ const connectWallet = async (tezos: TezosToolkit) => { return await wallet.client.getActiveAccount().then(async userAccount => { const userAddress = await wallet.getPKH(); + const userBalance = (await tezos.tz.getBalance(userAddress)).toNumber(); - return { wallet, userAddress, userAccount }; + return { wallet, userAddress, userAccount, userBalance }; }); }; diff --git a/pages/[walletAddress]/beacon.tsx b/pages/[walletAddress]/beacon.tsx index 42cad593..e56f2889 100644 --- a/pages/[walletAddress]/beacon.tsx +++ b/pages/[walletAddress]/beacon.tsx @@ -3,13 +3,13 @@ import { Cross1Icon } from "@radix-ui/react-icons"; import bs58check from "bs58check"; import { useSearchParams } from "next/navigation"; import { useRouter } from "next/router"; -import { useContext, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Spinner from "../../components/Spinner"; import renderError from "../../components/formUtils"; import Meta from "../../components/meta"; import { Event } from "../../context/P2PClient"; import { MODAL_TIMEOUT } from "../../context/config"; -import { AppDispatchContext, AppStateContext } from "../../context/state"; +import { useAppDispatch, useAppState } from "../../context/state"; import useIsOwner from "../../utils/useIsOwner"; import { p2pData } from "../../versioned/interface"; import { hasTzip27Support } from "../../versioned/util"; @@ -38,8 +38,8 @@ export function decodeData(data: string): p2pData { } const Beacon = () => { - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; + const state = useAppState(); + const dispatch = useAppDispatch(); const router = useRouter(); const isOwner = useIsOwner(); diff --git a/pages/[walletAddress]/dashboard.tsx b/pages/[walletAddress]/dashboard.tsx index 8b0ba3ad..dc4ebe38 100644 --- a/pages/[walletAddress]/dashboard.tsx +++ b/pages/[walletAddress]/dashboard.tsx @@ -1,7 +1,6 @@ -import { useContext } from "react"; import Dashboard from "../../components/dashboard"; import Meta from "../../components/meta"; -import { AppStateContext } from "../../context/state"; +import { useAppState } from "../../context/state"; import { useTzktBalance, useTzktDefiTokens, @@ -9,7 +8,7 @@ import { } from "../../utils/tzktHooks"; const DashboardPage = () => { - const state = useContext(AppStateContext)!; + const state = useAppState(); const address = state.currentContract; // Get balance diff --git a/pages/[walletAddress]/fund-wallet.tsx b/pages/[walletAddress]/fund-wallet.tsx index db4dbe4d..e09f7a18 100644 --- a/pages/[walletAddress]/fund-wallet.tsx +++ b/pages/[walletAddress]/fund-wallet.tsx @@ -8,14 +8,20 @@ import renderError, { renderWarning } from "../../components/formUtils"; import Meta from "../../components/meta"; import TopUp from "../../components/topUpForm"; import { TZKT_API_URL, PREFERED_NETWORK } from "../../context/config"; -import { AppDispatchContext, AppStateContext } from "../../context/state"; +import { useAppDispatch, useAppState } from "../../context/state"; +import { TezosToolkitContext } from "../../context/tezos-toolkit"; +import { useWallet } from "../../context/wallet"; import { makeWertWidget } from "../../context/wert"; import { mutezToTez } from "../../utils/tez"; import { signers } from "../../versioned/apis"; const TopUpPage = () => { - const state = useContext(AppStateContext)!; - const disptach = useContext(AppDispatchContext)!; + const state = useAppState(); + const disptach = useAppDispatch(); + const { + state: { userAddress }, + } = useWallet(); + const { tezos } = useContext(TezosToolkitContext); const router = useRouter(); const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(false); @@ -43,7 +49,7 @@ const TopUpPage = () => { } try { - const sent = await state.connection.wallet + const sent = await tezos.wallet .transfer({ to: state.currentContract, amount }) .send(); @@ -68,16 +74,16 @@ const TopUpPage = () => { const wertWidgetRef = useRef( makeWertWidget({ - wallet: state.address ?? "", + wallet: userAddress ?? "", onSuccess, }) ); useEffect(() => { - if (!state.currentContract || !state.address) return; + if (!state.currentContract || !userAddress) return; wertWidgetRef.current = makeWertWidget({ - wallet: state.address ?? "", + wallet: userAddress ?? "", onSuccess, }); }, [state.currentContract]); @@ -86,8 +92,7 @@ const TopUpPage = () => { if ( !router.query.walletAddress || Array.isArray(router.query.walletAddress) || - !!state.address || - !state.attemptedInitialLogin + !!userAddress ) return; @@ -99,7 +104,7 @@ const TopUpPage = () => { } router.replace(`/${router.query.walletAddress}/proposals`); - }, [router.query.walletAddress, state.address, state.attemptedInitialLogin]); + }, [router.query.walletAddress, userAddress]); return (
@@ -116,7 +121,7 @@ const TopUpPage = () => { {!signers( state.contracts[state.currentContract ?? ""] ?? state.currentStorage - ).includes(state.address ?? "") && + ).includes(userAddress ?? "") && renderWarning("You're not the owner of this wallet")}
@@ -156,7 +161,7 @@ const TopUpPage = () => { ) : isSuccess ? ( Transferred the funds from{" "} - to{" "} + to{" "} {" "} ) : null} @@ -169,7 +174,7 @@ const TopUpPage = () => { ) : ( <>

- Send from + Send from

(!!og.resolver ? og.resolver.timestamp : og.proposer.timestamp); const History = () => { - const globalState = useContext(AppStateContext)!; - const globalDispatch = useContext(AppDispatchContext)!; + const globalState = useAppState(); + const globalDispatch = useAppDispatch(); + const { tezos } = useContext(TezosToolkitContext); const walletTokens = useWalletTokens(); const [state, dispatch] = useReducer(reducer, { @@ -202,13 +204,8 @@ const History = () => { (async () => { if (!globalState.currentContract) return; - const c = await globalState.connection.wallet.at( - globalState.currentContract, - tzip16 - ); - const balance = await globalState.connection.tz.getBalance( - globalState.currentContract - ); + const c = await tezos.wallet.at(globalState.currentContract, tzip16); + const balance = await tezos.tz.getBalance(globalState.currentContract); const storage = (await c.storage()) as contractStorage; diff --git a/pages/[walletAddress]/new-proposal.tsx b/pages/[walletAddress]/new-proposal.tsx index 77b95b63..d9f1cdd9 100644 --- a/pages/[walletAddress]/new-proposal.tsx +++ b/pages/[walletAddress]/new-proposal.tsx @@ -1,12 +1,12 @@ import { useRouter } from "next/router"; -import { useContext, useEffect } from "react"; +import { useEffect } from "react"; import Meta from "../../components/meta"; import TransferForm from "../../components/transferForm"; -import { AppStateContext } from "../../context/state"; +import { useAppState } from "../../context/state"; import useIsOwner from "../../utils/useIsOwner"; const CreateProposal = () => { - const state = useContext(AppStateContext)!; + const state = useAppState(); const router = useRouter(); const isOwner = useIsOwner(); diff --git a/pages/[walletAddress]/proposals.tsx b/pages/[walletAddress]/proposals.tsx index 19920c41..5bf2266d 100644 --- a/pages/[walletAddress]/proposals.tsx +++ b/pages/[walletAddress]/proposals.tsx @@ -1,3 +1,4 @@ +import { TezosToolkit } from "@taquito/taquito"; import { tzip16 } from "@taquito/tzip16"; import { validateContractAddress, ValidationResult } from "@taquito/utils"; import { Dispatch, useContext, useEffect, useReducer, useRef } from "react"; @@ -7,13 +8,15 @@ import Meta from "../../components/meta"; import Modal from "../../components/modal"; import ProposalSignForm from "../../components/proposalSignForm"; import { - AppDispatchContext, - AppStateContext, tezosState, action as globalAction, contractStorage, + useAppDispatch, + useAppState, } from "../../context/state"; +import { TezosToolkitContext } from "../../context/tezos-toolkit"; import fetchVersion from "../../context/version"; +import { useWallet } from "../../context/wallet"; import { proposal, version } from "../../types/display"; import { canExecute, canReject } from "../../utils/proposals"; import useIsOwner from "../../utils/useIsOwner"; @@ -139,14 +142,12 @@ async function getProposals( globalState: tezosState, globalDispatch: Dispatch, dispatch: Dispatch, - state: state + state: state, + tezos: TezosToolkit ) { if (!globalState.currentContract) return; - const c = await globalState.connection.wallet.at( - globalState.currentContract, - tzip16 - ); + const c = await tezos.wallet.at(globalState.currentContract, tzip16); const storage: contractStorage = await c.storage(); @@ -175,9 +176,7 @@ async function getProposals( }); if (globalState.contracts[globalState.currentContract ?? ""]) { - const balance = await globalState.connection.tz.getBalance( - globalState.currentContract - ); + const balance = await tezos.tz.getBalance(globalState.currentContract); globalDispatch({ type: "updateContract", @@ -192,11 +191,17 @@ async function getProposals( } const Proposals = () => { - const globalState = useContext(AppStateContext)!; - const globalDispatch = useContext(AppDispatchContext)!; + const globalState = useAppState(); + const globalDispatch = useAppDispatch(); const isOwner = useIsOwner(); const walletTokens = useWalletTokens(); + const { + state: { userAddress }, + } = useWallet(); + + const { tezos } = useContext(TezosToolkitContext); + const [state, dispatch] = useReducer(reducer, { isLoading: true, isInvalid: false, @@ -248,7 +253,8 @@ const Proposals = () => { globalState, globalDispatch, dispatch, - state + state, + tezos ); if (!proposals) return; @@ -283,7 +289,8 @@ const Proposals = () => { globalState, globalDispatch, dispatch, - state + state, + tezos ); if (!proposals) return; @@ -401,7 +408,7 @@ const Proposals = () => { hasDeadlinePassed || isExecutable || isRejectable; const hasSigned = !!signatures.find( - x => x.signer == globalState.address + x => x.signer == userAddress ); return ( @@ -441,7 +448,7 @@ const Proposals = () => { proposer={x[1].og.proposer} resolver={x[1].og.resolver} isSignable={ - !!globalState.address && + !!userAddress && !!globalState.currentContract && isOwner && (!hasSigned || shouldResolve) diff --git a/pages/[walletAddress]/settings.tsx b/pages/[walletAddress]/settings.tsx index 4fb96640..c726efd1 100644 --- a/pages/[walletAddress]/settings.tsx +++ b/pages/[walletAddress]/settings.tsx @@ -1,15 +1,13 @@ -import { getSenderId } from "@airgap/beacon-sdk"; -import { Cross1Icon } from "@radix-ui/react-icons"; import { useRouter } from "next/router"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import Meta from "../../components/meta"; import SignersForm from "../../components/signersForm"; -import { AppDispatchContext, AppStateContext } from "../../context/state"; +import { useAppDispatch, useAppState } from "../../context/state"; import useIsOwner from "../../utils/useIsOwner"; const Settings = () => { - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; + const state = useAppState(); + const dispatch = useAppDispatch(); const router = useRouter(); const isOwner = useIsOwner(); diff --git a/pages/_app.tsx b/pages/_app.tsx index 023e826c..6f00edac 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,293 +1,20 @@ -import { LocalStorage, NetworkType } from "@airgap/beacon-sdk"; -import { ArrowRightIcon } from "@radix-ui/react-icons"; -import { BeaconWallet } from "@taquito/beacon-wallet"; -import { validateAddress, ValidationResult } from "@taquito/utils"; import type { AppProps } from "next/app"; -import { usePathname } from "next/navigation"; -import { useRouter } from "next/router"; -import { useReducer, useEffect, useState } from "react"; -import Banner from "../components/Banner"; -import LoginModal from "../components/LoginModal"; -import PoeModal from "../components/PoeModal"; -import Sidebar from "../components/Sidebar"; -import Spinner from "../components/Spinner"; -import Footer from "../components/footer"; -import NavBar from "../components/navbar"; -import P2PClient from "../context/P2PClient"; +import Layout from "../components/Layout"; import { AliasesProvider } from "../context/aliases"; -import { PREFERED_NETWORK } from "../context/config"; -import { - tezosState, - action, - reducer, - emptyState, - init, - AppStateContext, - AppDispatchContext, - contractStorage, -} from "../context/state"; +import { AppStateProvider } from "../context/state"; import { TezosToolkitProvider } from "../context/tezos-toolkit"; -import { WalletProvider, useWallet } from "../context/wallet"; +import { WalletProvider } from "../context/wallet"; import "../styles/globals.css"; -import { fetchContract } from "../utils/fetchContract"; export default function App({ Component, pageProps }: AppProps) { - const { - state: { wallet }, - } = useWallet(); - - const [state, dispatch]: [tezosState, React.Dispatch] = useReducer( - reducer, - emptyState() - ); - - const [isFetching, setIsFetching] = useState(true); - const [hasSidebar, setHasSidebar] = useState(false); - const [data, setData] = useState(); - const path = usePathname(); - const router = useRouter(); - useEffect(() => { - if (!path) return; - - const queryParams = new URLSearchParams(window.location.search); - - const isPairing = queryParams.has("type") && queryParams.has("data"); - - if (isPairing) { - setData(queryParams.get("data")!); - } - - const contracts = Object.keys(state.contracts); - - if ((path === "/" || path === "") && contracts.length > 0) { - const contract = contracts[0]; - - router.replace(`/${contract}/dashboard`); - return; - } else if (path === "/" || path === "") { - // Get rid of query in case it comes from beacon - router.replace("/"); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - state.currentContract, - path, - state.attemptedInitialLogin, - state.contracts, - ]); - - useEffect(() => { - (async () => { - if ( - router.pathname.includes("[walletAddress]") && - !router.query.walletAddress - ) - return; - - if ( - !router.query.walletAddress || - Array.isArray(router.query.walletAddress) || - (router.query.walletAddress === state.currentContract && - !!state.currentStorage) - ) { - setIsFetching(false); - return; - } - - if (!!state.contracts[router.query.walletAddress]) { - dispatch({ - type: "setCurrentContract", - payload: router.query.walletAddress, - }); - setIsFetching(false); - return; - } - - if ( - validateAddress(router.query.walletAddress) !== ValidationResult.VALID - ) { - setIsFetching(false); - router.replace( - `/invalid-contract?address=${router.query.walletAddress}` - ); - return; - } - - if (state.currentStorage?.address === router.query.walletAddress) { - setIsFetching(false); - return; - } - - try { - const storage = await fetchContract( - state.connection, - router.query.walletAddress - ); - - if (!storage) { - setIsFetching(false); - router.replace( - `/invalid-contract?address=${router.query.walletAddress}` - ); - return; - } - - storage.address = router.query.walletAddress; - - dispatch({ - type: "setCurrentStorage", - payload: storage as contractStorage & { address: string }, - }); - - dispatch({ - type: "setCurrentContract", - payload: router.query.walletAddress, - }); - - setIsFetching(false); - } catch (e) { - setIsFetching(false); - - router.replace( - `/invalid-contract?address=${router.query.walletAddress}` - ); - } - })(); - }, [ - router.query.walletAddress, - state.currentContract, - dispatch, - router, - state.currentStorage, - state.connection, - state.contracts, - ]); - useEffect(() => { - (async () => { - if (state!.beaconWallet === null) { - let a = init(); - dispatch({ type: "init", payload: a }); - - const p2pClient = new P2PClient({ - name: "TzSafe", - storage: new LocalStorage("P2P"), - }); - - await p2pClient.init(); - await p2pClient.connect(p2pClient.handleMessages); - - // Connect stored peers - Object.entries(a.connectedDapps).forEach(async ([address, dapps]) => { - Object.values(dapps).forEach(data => { - p2pClient - .addPeer(data) - .catch(_ => console.log("Failed to connect to peer", data)); - }); - }); - - dispatch!({ type: "p2pConnect", payload: p2pClient }); - - if (state.attemptedInitialLogin) return; - - const activeAccount = await wallet?.client.getActiveAccount(); - if (activeAccount && state?.accountInfo == null) { - const userAddress = await wallet?.getPKH(); - const balance = await state?.connection.tz.getBalance( - userAddress || "" - ); - dispatch({ - type: "login", - // TODO: FIX - //@ts-ignore - accountInfo: activeAccount!, - address: userAddress || "", - balance: balance!.toString(), - }); - } else { - dispatch({ - type: "setAttemptedInitialLogin", - payload: true, - }); - } - } - })(); - }, [state.beaconWallet]); - - useEffect(() => { - setHasSidebar(false); - }, [path]); - - const isSidebarHidden = - Object.values(state.contracts).length === 0 && - (path === "/" || - path === "/new-wallet" || - path === "/import-wallet" || - path === "/address-book"); - return ( - - - -
- - - - + + + + + ); diff --git a/pages/address-book.tsx b/pages/address-book.tsx index a483f2b8..bf2c053f 100644 --- a/pages/address-book.tsx +++ b/pages/address-book.tsx @@ -10,7 +10,12 @@ import { import { useContext } from "react"; import renderError from "../components/formUtils"; import Meta from "../components/meta"; -import { AppDispatchContext, AppStateContext } from "../context/state"; +import { + AppDispatchContext, + AppStateContext, + useAppDispatch, + useAppState, +} from "../context/state"; function get( s: string | FormikErrors<{ name: string; address: string }> @@ -26,8 +31,8 @@ function get( } } function Home() { - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; + const state = useAppState(); + const dispatch = useAppDispatch(); const byName = Object.fromEntries( Object.entries(state.aliases).map(([k, v]) => [v, k]) diff --git a/pages/new-wallet.tsx b/pages/new-wallet.tsx index aaadc30f..c6c917f1 100644 --- a/pages/new-wallet.tsx +++ b/pages/new-wallet.tsx @@ -5,27 +5,29 @@ import Meta from "../components/meta"; import Stepper from "../components/stepper"; import FormContext from "../context/formContext"; import { AppDispatchContext, AppStateContext } from "../context/state"; -import { connectWallet } from "../utils/connectWallet"; +import { useWallet } from "../context/wallet"; function Create() { const [formState, setFormState] = useState(null); const [activeStepIndex, setActiveStepIndex] = useState(0); const [formStatus, setFormStatus] = useState(""); - const state = useContext(AppStateContext)!; - const dispatch = useContext(AppDispatchContext)!; - let router = useRouter(); + const router = useRouter(); + const { + state: { userAddress, wallet }, + connectWallet, + } = useWallet(); useEffect(() => { (async () => { - if (!state?.address && state?.beaconWallet) { + if (!userAddress && wallet) { try { - await connectWallet(state, dispatch); + await connectWallet(); } catch (e) { router.replace("/"); } } })(); - }, [router, dispatch, state]); + }, [router, connectWallet, userAddress]); return (
diff --git a/utils/useIsOwner.ts b/utils/useIsOwner.ts index 1e010398..55645bcc 100644 --- a/utils/useIsOwner.ts +++ b/utils/useIsOwner.ts @@ -1,24 +1,23 @@ -import { useContext, useMemo } from "react"; -import { AppStateContext } from "../context/state"; +import { useMemo } from "react"; +import { useAppState } from "../context/state"; +import { useWallet } from "../context/wallet"; import { signers } from "../versioned/apis"; const useIsOwner = () => { - let state = useContext(AppStateContext)!; + let state = useAppState(); + const { + state: { userAddress }, + } = useWallet(); const isOwner = useMemo( () => - !!state.address && + !!userAddress && (state.contracts[state.currentContract ?? ""]?.owners?.includes( - state.address + userAddress ) ?? (!!state.currentStorage && - signers(state.currentStorage).includes(state.address!))), - [ - state.currentContract, - state.address, - state.contracts, - state.currentStorage, - ] + signers(state.currentStorage).includes(userAddress!))), + [state.currentContract, userAddress, state.contracts, state.currentStorage] ); return isOwner; diff --git a/utils/useWalletTokens.ts b/utils/useWalletTokens.ts index 7325a3fc..231b5ffe 100644 --- a/utils/useWalletTokens.ts +++ b/utils/useWalletTokens.ts @@ -1,13 +1,13 @@ -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { fa1_2Token } from "../components/FA1_2"; import { fa2Token } from "../components/FA2Transfer"; import { TZKT_API_URL } from "../context/config"; -import { AppStateContext } from "../context/state"; +import { useAppState } from "../context/state"; export type walletToken = fa1_2Token | fa2Token; const useWalletTokens = () => { - const state = useContext(AppStateContext)!; + const state = useAppState(); const [tokens, setTokens] = useState(); useEffect(() => { From 09933ee0838085afd997904801d4ce14bda3e8ce Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Mon, 22 Apr 2024 14:48:50 +0200 Subject: [PATCH 3/8] :recycle: wallet become flat context instead of state part and functions part --- components/ContractExecution.tsx | 4 ++-- components/ExecuteContractForm.tsx | 4 +--- components/Layout.tsx | 4 +--- components/LoginModal.tsx | 9 ++------ components/PoeModal.tsx | 4 +--- components/Sidebar.tsx | 4 +--- components/create/basic.tsx | 4 +--- components/create/settings.tsx | 4 +--- components/loginButton.tsx | 8 +------ components/navbar.tsx | 5 +---- components/proposalSignForm.tsx | 4 +--- components/proposals.tsx | 8 ++----- components/signersForm.tsx | 4 +--- components/topUpForm.tsx | 8 ++----- components/transferForm.tsx | 8 ++----- context/wallet.tsx | 14 +++++++++---- pages/[walletAddress]/fund-wallet.tsx | 8 +++---- pages/[walletAddress]/proposals.tsx | 4 +--- pages/new-wallet.tsx | 10 +++------ utils/connectWallet.ts | 30 --------------------------- utils/useIsOwner.ts | 4 +--- 21 files changed, 38 insertions(+), 114 deletions(-) delete mode 100644 utils/connectWallet.ts diff --git a/components/ContractExecution.tsx b/components/ContractExecution.tsx index 3c9eaf34..ff66f090 100644 --- a/components/ContractExecution.tsx +++ b/components/ContractExecution.tsx @@ -7,8 +7,8 @@ import { Formik, useFormikContext, } from "formik"; -import React, { useContext, useEffect } from "react"; -import { AppStateContext } from "../context/state"; +import React, { useEffect } from "react"; +import { useAppState } from "../context/state"; import { useTezosToolkit } from "../context/tezos-toolkit"; import { parseContract, diff --git a/components/ExecuteContractForm.tsx b/components/ExecuteContractForm.tsx index 7dc61e09..e0b660b1 100644 --- a/components/ExecuteContractForm.tsx +++ b/components/ExecuteContractForm.tsx @@ -19,9 +19,7 @@ export function ExecuteContractForm( ) { const { submitCount, setFieldValue } = useFormikContext(); const submitCountRef = useRef(submitCount); - const { - state: { userAddress }, - } = useWallet(); + const { userAddress } = useWallet(); const [state, setState] = useState( () => props.defaultState ?? { address: "", amount: 0, shape: {} } diff --git a/components/Layout.tsx b/components/Layout.tsx index 9151f759..6725549b 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -27,9 +27,7 @@ export default function Layout({ const state = useAppState(); const dispatch = useAppDispatch(); const { tezos } = useTezosToolkit(); - const { - state: { wallet }, - } = useWallet(); + const { wallet } = useWallet(); const [data, setData] = useState(); const [hasSidebar, setHasSidebar] = useState(false); diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx index 3539705a..84878c04 100644 --- a/components/LoginModal.tsx +++ b/components/LoginModal.tsx @@ -3,7 +3,6 @@ import { Event } from "../context/P2PClient"; import { useAppDispatch, useAppState } from "../context/state"; import { useWallet } from "../context/wallet"; import { decodeData } from "../pages/[walletAddress]/beacon"; -import { connectWallet } from "../utils/connectWallet"; import { signers } from "../versioned/apis"; import { p2pData } from "../versioned/interface"; import { hasTzip27Support } from "../versioned/util"; @@ -26,9 +25,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { const [parsedData, setParsedData] = useState(); const [error, setError] = useState(); - const { - state: { userAddress, wallet }, - } = useWallet(); + const { userAddress, wallet, connectWallet } = useWallet(); const options = useMemo(() => { if (!userAddress) return []; @@ -258,9 +255,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { Cancel